From 5c9c15ae6e6a30b7635b4d9888ee58e987e3bd0b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 28 May 2015 17:06:26 +0100 Subject: [PATCH 1/7] Add support for extracting H.265/HEVC data in MP4 files. --- .../android/exoplayer/extractor/mp4/Atom.java | 5 +- .../exoplayer/extractor/mp4/AtomParsers.java | 107 +++++++++++------- .../extractor/mp4/FragmentedMp4Extractor.java | 4 +- .../exoplayer/extractor/mp4/Mp4Extractor.java | 4 +- .../exoplayer/extractor/ts/H264Reader.java | 8 +- .../android/exoplayer/util/MimeTypes.java | 1 + .../util/{H264Util.java => NalUnitUtil.java} | 8 +- ...H264UtilTest.java => NalUnitUtilTest.java} | 40 +++---- 8 files changed, 103 insertions(+), 74 deletions(-) rename library/src/main/java/com/google/android/exoplayer/util/{H264Util.java => NalUnitUtil.java} (97%) rename library/src/test/java/com/google/android/exoplayer/util/{H264UtilTest.java => NalUnitUtilTest.java} (75%) diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java index aa0861dc40..a2b3524ddb 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Atom.java @@ -39,7 +39,8 @@ import java.util.List; public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp"); public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1"); public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3"); - public static final int TYPE_esds = Util.getIntegerCodeForString("esds"); + public static final int TYPE_hvc1 = Util.getIntegerCodeForString("hvc1"); + public static final int TYPE_hev1 = Util.getIntegerCodeForString("hev1"); public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat"); public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a"); public static final int TYPE_ac_3 = Util.getIntegerCodeForString("ac-3"); @@ -58,6 +59,8 @@ import java.util.List; public static final int TYPE_minf = Util.getIntegerCodeForString("minf"); public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl"); public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC"); + public static final int TYPE_hvcC = Util.getIntegerCodeForString("hvcC"); + public static final int TYPE_esds = Util.getIntegerCodeForString("esds"); public static final int TYPE_moof = Util.getIntegerCodeForString("moof"); public static final int TYPE_traf = Util.getIntegerCodeForString("traf"); public static final int TYPE_mvex = Util.getIntegerCodeForString("mvex"); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java index 13a45058ab..846c71ad9b 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/AtomParsers.java @@ -20,8 +20,8 @@ import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.util.Ac3Util; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.CodecSpecificDataUtil; -import com.google.android.exoplayer.util.H264Util; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.NalUnitUtil; import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.Util; @@ -331,25 +331,22 @@ import java.util.List; Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = stsd.readInt(); if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 - || childAtomType == Atom.TYPE_encv) { - parseAvcFromParent(stsd, childStartPosition, childAtomSize, durationUs, holder, i); + || childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v + || childAtomType == Atom.TYPE_hvc1 || childAtomType == Atom.TYPE_hev1) { + parseVideoSampleEntry(stsd, childStartPosition, childAtomSize, durationUs, holder, i); } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca || childAtomType == Atom.TYPE_ac_3) { parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, durationUs, holder, i); } else if (childAtomType == Atom.TYPE_TTML) { holder.mediaFormat = MediaFormat.createTtmlFormat(); - } else if (childAtomType == Atom.TYPE_mp4v) { - holder.mediaFormat = parseMp4vFromParent(stsd, childStartPosition, childAtomSize, - durationUs); } stsd.setPosition(childStartPosition + childAtomSize); } return holder; } - /** Returns the media format for an avc1 box. */ - private static void parseAvcFromParent(ParsableByteArray parent, int position, int size, + private static void parseVideoSampleEntry(ParsableByteArray parent, int position, int size, long durationUs, StsdDataHolder out, int entryIndex) { parent.setPosition(position + Atom.HEADER_SIZE); @@ -361,6 +358,7 @@ import java.util.List; List initializationData = null; int childPosition = parent.getPosition(); + String mimeType = null; while (childPosition - position < size) { parent.setPosition(childPosition); int childStartPosition = parent.getPosition(); @@ -372,9 +370,22 @@ import java.util.List; Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_avcC) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_H264; Pair, Integer> avcCData = parseAvcCFromParent(parent, childStartPosition); initializationData = avcCData.first; out.nalUnitLengthFieldLength = avcCData.second; + } else if (childAtomType == Atom.TYPE_hvcC) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_H265; + Pair, Integer> hvcCData = parseHvcCFromParent(parent, childStartPosition); + initializationData = hvcCData.first; + out.nalUnitLengthFieldLength = hvcCData.second; + } else if (childAtomType == Atom.TYPE_esds) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_MP4V; + initializationData = + Collections.singletonList(parseEsdsFromParent(parent, childStartPosition)); } else if (childAtomType == Atom.TYPE_sinf) { out.trackEncryptionBoxes[entryIndex] = parseSinfFromParent(parent, childStartPosition, childAtomSize); @@ -383,9 +394,8 @@ import java.util.List; } childPosition += childAtomSize; } - - out.mediaFormat = MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE, - durationUs, width, height, pixelWidthHeightRatio, initializationData); + out.mediaFormat = MediaFormat.createVideoFormat(mimeType, MediaFormat.NO_VALUE, durationUs, + width, height, pixelWidthHeightRatio, initializationData); } private static Pair, Integer> parseAvcCFromParent(ParsableByteArray parent, @@ -401,15 +411,58 @@ import java.util.List; // expose the AVC profile and level somewhere useful; Most likely in MediaFormat. int numSequenceParameterSets = parent.readUnsignedByte() & 0x1F; for (int j = 0; j < numSequenceParameterSets; j++) { - initializationData.add(H264Util.parseChildNalUnit(parent)); + initializationData.add(NalUnitUtil.parseChildNalUnit(parent)); } int numPictureParameterSets = parent.readUnsignedByte(); for (int j = 0; j < numPictureParameterSets; j++) { - initializationData.add(H264Util.parseChildNalUnit(parent)); + initializationData.add(NalUnitUtil.parseChildNalUnit(parent)); } return Pair.create(initializationData, nalUnitLengthFieldLength); } + private static Pair, Integer> parseHvcCFromParent(ParsableByteArray parent, + int position) { + // Skip to the NAL unit length size field. + parent.setPosition(position + Atom.HEADER_SIZE + 21); + int lengthSizeMinusOne = parent.readUnsignedByte() & 0x03; + + // Calculate the combined size of all VPS/SPS/PPS bitstreams. + int numberOfArrays = parent.readUnsignedByte(); + int csdLength = 0; + int csdStartPosition = parent.getPosition(); + for (int i = 0; i < numberOfArrays; i++) { + parent.skipBytes(1); // completeness (1), nal_unit_type (7) + int numberOfNalUnits = parent.readUnsignedShort(); + for (int j = 0; j < numberOfNalUnits; j++) { + int nalUnitLength = parent.readUnsignedShort(); + csdLength += 4 + nalUnitLength; // Start code and NAL unit. + parent.skipBytes(nalUnitLength); + } + } + + // Concatenate the codec-specific data into a single buffer. + parent.setPosition(csdStartPosition); + byte[] buffer = new byte[csdLength]; + int bufferPosition = 0; + for (int i = 0; i < numberOfArrays; i++) { + parent.skipBytes(1); // completeness (1), nal_unit_type (7) + int numberOfNalUnits = parent.readUnsignedShort(); + for (int j = 0; j < numberOfNalUnits; j++) { + int nalUnitLength = parent.readUnsignedShort(); + System.arraycopy(NalUnitUtil.NAL_START_CODE, 0, buffer, bufferPosition, + NalUnitUtil.NAL_START_CODE.length); + bufferPosition += NalUnitUtil.NAL_START_CODE.length; + System.arraycopy(parent.data, parent.getPosition(), buffer, bufferPosition, nalUnitLength); + bufferPosition += nalUnitLength; + parent.skipBytes(nalUnitLength); + } + } + + List initializationData = csdLength == 0 ? Collections.emptyList() + : Collections.singletonList(buffer); + return Pair.create(initializationData, lengthSizeMinusOne + 1); + } + private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; @@ -462,34 +515,6 @@ import java.util.List; return null; } - /** Returns the media format for an mp4v box. */ - private static MediaFormat parseMp4vFromParent(ParsableByteArray parent, int position, int size, - long durationUs) { - parent.setPosition(position + Atom.HEADER_SIZE); - - parent.skipBytes(24); - int width = parent.readUnsignedShort(); - int height = parent.readUnsignedShort(); - parent.skipBytes(50); - - List initializationData = new ArrayList<>(1); - int childPosition = parent.getPosition(); - while (childPosition - position < size) { - parent.setPosition(childPosition); - int childStartPosition = parent.getPosition(); - int childAtomSize = parent.readInt(); - Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); - int childAtomType = parent.readInt(); - if (childAtomType == Atom.TYPE_esds) { - initializationData.add(parseEsdsFromParent(parent, childStartPosition)); - } - childPosition += childAtomSize; - } - - return MediaFormat.createVideoFormat( - MimeTypes.VIDEO_MP4V, MediaFormat.NO_VALUE, durationUs, width, height, initializationData); - } - private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position, int size, long durationUs, StsdDataHolder out, int entryIndex) { parent.setPosition(position + Atom.HEADER_SIZE); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java index 896d69cc30..d0987decbf 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java @@ -26,8 +26,8 @@ import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer.extractor.mp4.Atom.LeafAtom; import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.H264Util; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.NalUnitUtil; import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.Util; @@ -106,7 +106,7 @@ public final class FragmentedMp4Extractor implements Extractor { public FragmentedMp4Extractor(int workaroundFlags) { this.workaroundFlags = workaroundFlags; atomHeader = new ParsableByteArray(Atom.HEADER_SIZE); - nalStartCode = new ParsableByteArray(H264Util.NAL_START_CODE); + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); encryptionSignalByte = new ParsableByteArray(1); extendedTypeScratch = new byte[16]; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java index fb436790ef..1c8b2b4faa 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/Mp4Extractor.java @@ -23,7 +23,7 @@ import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.H264Util; +import com.google.android.exoplayer.util.NalUnitUtil; import com.google.android.exoplayer.util.ParsableByteArray; import java.io.IOException; @@ -72,7 +72,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { public Mp4Extractor() { atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); containerAtoms = new Stack<>(); - nalStartCode = new ParsableByteArray(H264Util.NAL_START_CODE); + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); parserState = STATE_READING_ATOM_HEADER; } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java index 3178a4c7a0..1125b44f3f 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/H264Reader.java @@ -19,8 +19,8 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.H264Util; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.NalUnitUtil; import com.google.android.exoplayer.util.ParsableBitArray; import com.google.android.exoplayer.util.ParsableByteArray; @@ -104,7 +104,7 @@ import java.util.List; @Override public void seek() { seiReader.seek(); - H264Util.clearPrefixFlags(prefixFlags); + NalUnitUtil.clearPrefixFlags(prefixFlags); sps.reset(); pps.reset(); sei.reset(); @@ -128,7 +128,7 @@ import java.util.List; // Scan the appended data, processing NAL units as they are encountered while (offset < limit) { - int nextNalUnitOffset = H264Util.findNalUnit(dataArray, offset, limit, prefixFlags); + int nextNalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); if (nextNalUnitOffset < limit) { // We've seen the start of a NAL unit. @@ -139,7 +139,7 @@ import java.util.List; feedNalUnitTargetBuffersData(dataArray, offset, nextNalUnitOffset); } - int nalUnitType = H264Util.getNalUnitType(dataArray, nextNalUnitOffset); + int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nextNalUnitOffset); int bytesWrittenPastNalUnit = limit - nextNalUnitOffset; switch (nalUnitType) { case NAL_UNIT_TYPE_IDR: diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java index 3a6ea01c6f..21aa81a965 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java @@ -33,6 +33,7 @@ public class MimeTypes { public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4"; public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm"; public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc"; + public static final String VIDEO_H265 = BASE_TYPE_VIDEO + "/hevc"; public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8"; public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9"; public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es"; diff --git a/library/src/main/java/com/google/android/exoplayer/util/H264Util.java b/library/src/main/java/com/google/android/exoplayer/util/NalUnitUtil.java similarity index 97% rename from library/src/main/java/com/google/android/exoplayer/util/H264Util.java rename to library/src/main/java/com/google/android/exoplayer/util/NalUnitUtil.java index 0cde8b472f..be4ecc476f 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/H264Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/NalUnitUtil.java @@ -18,11 +18,11 @@ package com.google.android.exoplayer.util; import java.nio.ByteBuffer; /** - * Utility methods for handling H264 data. + * Utility methods for handling H.264/AVC and H.265/HEVC NAL units. */ -public final class H264Util { +public final class NalUnitUtil { - /** Four initial bytes that must prefix H.264/AVC NAL units for decoding. */ + /** Four initial bytes that must prefix NAL units for decoding. */ public static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; /** @@ -173,7 +173,7 @@ public final class H264Util { return result; } - private H264Util() { + private NalUnitUtil() { // Prevent instantiation. } diff --git a/library/src/test/java/com/google/android/exoplayer/util/H264UtilTest.java b/library/src/test/java/com/google/android/exoplayer/util/NalUnitUtilTest.java similarity index 75% rename from library/src/test/java/com/google/android/exoplayer/util/H264UtilTest.java rename to library/src/test/java/com/google/android/exoplayer/util/NalUnitUtilTest.java index 78bfa0c3fb..ecd97cbff0 100644 --- a/library/src/test/java/com/google/android/exoplayer/util/H264UtilTest.java +++ b/library/src/test/java/com/google/android/exoplayer/util/NalUnitUtilTest.java @@ -20,9 +20,9 @@ import junit.framework.TestCase; import java.util.Arrays; /** - * Tests for {@link H264Util}. + * Tests for {@link NalUnitUtil}. */ -public class H264UtilTest extends TestCase { +public class NalUnitUtilTest extends TestCase { private static final int TEST_PARTIAL_NAL_POSITION = 4; private static final int TEST_NAL_POSITION = 10; @@ -31,19 +31,19 @@ public class H264UtilTest extends TestCase { byte[] data = buildTestData(); // Should find NAL unit. - int result = H264Util.findNalUnit(data, 0, data.length, null); + int result = NalUnitUtil.findNalUnit(data, 0, data.length, null); assertEquals(TEST_NAL_POSITION, result); // Should find NAL unit whose prefix ends one byte before the limit. - result = H264Util.findNalUnit(data, 0, TEST_NAL_POSITION + 4, null); + result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 4, null); assertEquals(TEST_NAL_POSITION, result); // Shouldn't find NAL unit whose prefix ends at the limit (since the limit is exclusive). - result = H264Util.findNalUnit(data, 0, TEST_NAL_POSITION + 3, null); + result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 3, null); assertEquals(TEST_NAL_POSITION + 3, result); // Should find NAL unit whose prefix starts at the offset. - result = H264Util.findNalUnit(data, TEST_NAL_POSITION, data.length, null); + result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION, data.length, null); assertEquals(TEST_NAL_POSITION, result); // Shouldn't find NAL unit whose prefix starts one byte past the offset. - result = H264Util.findNalUnit(data, TEST_NAL_POSITION + 1, data.length, null); + result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION + 1, data.length, null); assertEquals(data.length, result); } @@ -54,9 +54,9 @@ public class H264UtilTest extends TestCase { boolean[] prefixFlags = new boolean[3]; byte[] data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); byte[] data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, data.length); - int result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags); + int result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); assertEquals(data1.length, result); - result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); assertEquals(-1, result); assertPrefixFlagsCleared(prefixFlags); @@ -64,9 +64,9 @@ public class H264UtilTest extends TestCase { prefixFlags = new boolean[3]; data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 3); data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 3, data.length); - result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags); + result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); assertEquals(data1.length, result); - result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); assertEquals(-3, result); assertPrefixFlagsCleared(prefixFlags); @@ -75,11 +75,11 @@ public class H264UtilTest extends TestCase { data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2); byte[] data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length); - result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags); + result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); assertEquals(data1.length, result); - result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); assertEquals(data2.length, result); - result = H264Util.findNalUnit(data3, 0, data3.length, prefixFlags); + result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags); assertEquals(-2, result); assertPrefixFlagsCleared(prefixFlags); @@ -89,13 +89,13 @@ public class H264UtilTest extends TestCase { data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2); data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, TEST_NAL_POSITION + 3); byte[] data4 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length); - result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags); + result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); assertEquals(data1.length, result); - result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); assertEquals(data2.length, result); - result = H264Util.findNalUnit(data3, 0, data3.length, prefixFlags); + result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags); assertEquals(data3.length, result); - result = H264Util.findNalUnit(data4, 0, data4.length, prefixFlags); + result = NalUnitUtil.findNalUnit(data4, 0, data4.length, prefixFlags); assertEquals(-3, result); assertPrefixFlagsCleared(prefixFlags); @@ -103,9 +103,9 @@ public class H264UtilTest extends TestCase { prefixFlags = new boolean[3]; data1 = Arrays.copyOfRange(data, 0, TEST_PARTIAL_NAL_POSITION + 2); data2 = Arrays.copyOfRange(data, TEST_PARTIAL_NAL_POSITION + 2, data.length); - result = H264Util.findNalUnit(data1, 0, data1.length, prefixFlags); + result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); assertEquals(data1.length, result); - result = H264Util.findNalUnit(data2, 0, data2.length, prefixFlags); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); assertEquals(4, result); assertPrefixFlagsCleared(prefixFlags); } From 99edc6a4b447399d62f578d0aeed0eabbeddd98c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 28 May 2015 17:07:12 +0100 Subject: [PATCH 2/7] Avoid using OMX.qcom.audio.decoder.mp3 on HTC Butterfly. This OMX component is listed but can't be instantiated on this device. According to the GitHub issue, some other devices are also affected, so these will have to be added too. Issue: #377 --- .../android/exoplayer/MediaCodecUtil.java | 25 +++++++++++++++++-- .../google/android/exoplayer/util/Util.java | 6 +++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java index c3f3fc85f2..22e2dde9ce 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java @@ -134,8 +134,7 @@ public class MediaCodecUtil { for (int i = 0; i < numberOfCodecs; i++) { MediaCodecInfo info = mediaCodecList.getCodecInfoAt(i); String codecName = info.getName(); - if (!info.isEncoder() && codecName.startsWith("OMX.") - && (secureDecodersExplicit || !codecName.endsWith(".secure"))) { + if (isCodecUsableDecoder(info, codecName, secureDecodersExplicit)) { String[] supportedTypes = info.getSupportedTypes(); for (int j = 0; j < supportedTypes.length; j++) { String supportedType = supportedTypes[j]; @@ -166,6 +165,28 @@ public class MediaCodecUtil { return null; } + /** + * Returns whether the specified codec is usable for decoding on the current device. + */ + private static boolean isCodecUsableDecoder(MediaCodecInfo info, String name, + boolean secureDecodersExplicit) { + if (info.isEncoder() || !name.startsWith("OMX.") + || (!secureDecodersExplicit && name.endsWith(".secure"))) { + return false; + } + + // Workaround an issue where creating a particular MP3 decoder on some HTC devices on platform + // API version 16 crashes mediaserver. + if (Util.SDK_INT == 16 + && ("dlxu".equals(Util.PRODUCT) // HTC Butterfly + || "protou".equals(Util.PRODUCT)) // HTC Desire X + && name.equals("OMX.qcom.audio.decoder.mp3")) { + return false; + } + + return true; + } + private static boolean isAdaptive(CodecCapabilities capabilities) { if (Util.SDK_INT >= 19) { return isAdaptiveV19(capabilities); diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 03fc20564d..5dc2993ef6 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -59,6 +59,12 @@ public final class Util { */ public static final int SDK_INT = android.os.Build.VERSION.SDK_INT; + /** + * Like {@link android.os.Build#PRODUCT}, but in a place where it can be conveniently overridden + * for local testing. + */ + public static final String PRODUCT = android.os.Build.PRODUCT; + private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile( "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" + "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?" From aa249e9f7f07d138da68ba794393c8e89b1cd710 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 28 May 2015 17:08:39 +0100 Subject: [PATCH 3/7] Fixed issue in which the segment/chunk shift value could become incorrect. --- .../exoplayer/dash/DashChunkSource.java | 22 ++++++++++++++++--- .../SmoothStreamingChunkSource.java | 19 ++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 8bf7199da8..76b40483d8 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -309,10 +309,26 @@ public class DashChunkSource implements ChunkSource { RepresentationHolder representationHolder = representationHolders.get(representation.format.id); DashSegmentIndex oldIndex = representationHolder.segmentIndex; + int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(); + long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) + + oldIndex.getDurationUs(oldIndexLastSegmentNum); DashSegmentIndex newIndex = representation.getIndex(); - int newFirstSegmentNum = newIndex.getFirstSegmentNum(); - int segmentNumShift = oldIndex.getSegmentNum(newIndex.getTimeUs(newFirstSegmentNum)) - - newFirstSegmentNum; + int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); + long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); + if (oldIndexEndTimeUs < newIndexStartTimeUs) { + // There's a gap between the old manifest and the new one which means we've slipped behind + // the live window and can't proceed. + fatalError = new BehindLiveWindowException(); + return; + } + int segmentNumShift; + if (oldIndexEndTimeUs == newIndexStartTimeUs) { + // The new manifest continues where the old one ended, with no overlap. + segmentNumShift = oldIndex.getLastSegmentNum() + 1 - newIndexFirstSegmentNum; + } else { + // The new manifest overlaps with the old one. + segmentNumShift = oldIndex.getSegmentNum(newIndexStartTimeUs) - newIndexFirstSegmentNum; + } representationHolder.segmentNumShift += segmentNumShift; representationHolder.segmentIndex = newIndex; } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index c7a3f2e517..77b187d464 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -217,11 +217,22 @@ public class SmoothStreamingChunkSource implements ChunkSource { SmoothStreamingManifest newManifest = manifestFetcher.getManifest(); if (currentManifest != newManifest && newManifest != null) { StreamElement currentElement = getElement(currentManifest); + int currentElementChunkCount = currentElement.chunkCount; StreamElement newElement = getElement(newManifest); - if (newElement.chunkCount == 0) { - currentManifestChunkOffset += currentElement.chunkCount; - } else if (currentElement.chunkCount > 0) { - currentManifestChunkOffset += currentElement.getChunkIndex(newElement.getStartTimeUs(0)); + if (currentElementChunkCount == 0 || newElement.chunkCount == 0) { + // There's no overlap between the old and new elements because at least one is empty. + currentManifestChunkOffset += currentElementChunkCount; + } else { + long currentElementEndTimeUs = currentElement.getStartTimeUs(currentElementChunkCount - 1) + + currentElement.getChunkDurationUs(currentElementChunkCount - 1); + long newElementStartTimeUs = newElement.getStartTimeUs(0); + if (currentElementEndTimeUs <= newElementStartTimeUs) { + // There's no overlap between the old and new elements. + currentManifestChunkOffset += currentElementChunkCount; + } else { + // The new element overlaps with the old one. + currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs); + } } currentManifest = newManifest; finishedCurrentManifest = false; From ea29c71d94cb013115b3bff26e971f693770bdde Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 28 May 2015 17:11:28 +0100 Subject: [PATCH 4/7] Temporarily revert bounded live seeking. It's causing issues in its current form. --- .../demo/player/DashRendererBuilder.java | 8 +- .../exoplayer/demo/player/DemoPlayer.java | 10 +- .../exoplayer/dash/DashChunkSource.java | 192 +++----- .../exoplayer/dash/DashChunkSourceTest.java | 449 +++++------------- 4 files changed, 193 insertions(+), 466 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java index bfd203c5fa..b875ac9ab7 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java @@ -234,8 +234,7 @@ public class DashRendererBuilder implements RendererBuilder, DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex, videoRepresentationIndices, videoDataSource, - new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset, - mainHandler, player); + new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_VIDEO); @@ -259,7 +258,7 @@ public class DashRendererBuilder implements RendererBuilder, format.audioSamplingRate + "Hz)"); audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex, new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS, - elapsedRealtimeOffset, mainHandler, player)); + elapsedRealtimeOffset)); codecs.add(format.codecs); } @@ -316,8 +315,7 @@ public class DashRendererBuilder implements RendererBuilder, Representation representation = representations.get(j); textTrackNameList.add(representation.format.id); textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j}, - textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset, - mainHandler, player)); + textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset)); } } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index 8b7c85c1ed..b475915d6a 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.MultiTrackChunkSource; -import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; @@ -55,7 +54,7 @@ import java.util.concurrent.CopyOnWriteArrayList; public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, - StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer { + StreamingDrmSessionManager.EventListener, TextRenderer { /** * Builds renderers for the player. @@ -516,13 +515,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi processCues(cues); } - @Override - public void onSeekRangeChanged(TimeRange seekRange) { - if (infoListener != null) { - infoListener.onSeekRangeChanged(seekRange); - } - } - /* package */ MetadataTrackRenderer.MetadataRenderer> getId3MetadataRenderer() { return new MetadataTrackRenderer.MetadataRenderer>() { diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 76b40483d8..08068f1a63 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer.dash; import com.google.android.exoplayer.BehindLiveWindowException; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.TimeRange; import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.Chunk; @@ -51,8 +50,6 @@ import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.SystemClock; -import android.os.Handler; - import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -66,20 +63,6 @@ import java.util.List; */ public class DashChunkSource implements ChunkSource { - /** - * Interface definition for a callback to be notified of {@link DashChunkSource} events. - */ - public interface EventListener { - - /** - * Invoked when the available seek range of the stream has changed. - * - * @param seekRange The range which specifies available content that can be seeked to. - */ - public void onSeekRangeChanged(TimeRange seekRange); - - } - /** * Thrown when an AdaptationSet is missing from the MPD. */ @@ -96,9 +79,6 @@ public class DashChunkSource implements ChunkSource { */ public static final int USE_ALL_TRACKS = -1; - private final Handler eventHandler; - private final EventListener eventListener; - private final TrackInfo trackInfo; private final DataSource dataSource; private final FormatEvaluator formatEvaluator; @@ -121,11 +101,6 @@ public class DashChunkSource implements ChunkSource { private boolean finishedCurrentManifest; private DrmInitData drmInitData; - private TimeRange seekRange; - private long[] seekRangeValues; - private int firstAvailableSegmentNum; - private int lastAvailableSegmentNum; - private boolean lastChunkWasInitialization; private IOException fatalError; @@ -167,7 +142,7 @@ public class DashChunkSource implements ChunkSource { public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex, int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator) { this(null, manifest, adaptationSetIndex, representationIndices, dataSource, formatEvaluator, - new SystemClock(), 0, 0, null, null); + new SystemClock(), 0, 0); } /** @@ -192,24 +167,19 @@ public class DashChunkSource implements ChunkSource { * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified * as the server's unix time minus the local elapsed time. It unknown, set to 0. - * @param eventHandler A handler to use when delivering events to {@code EventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. */ public DashChunkSource(ManifestFetcher manifestFetcher, int adaptationSetIndex, int[] representationIndices, DataSource dataSource, - FormatEvaluator formatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs, - Handler eventHandler, EventListener eventListener) { + FormatEvaluator formatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs) { this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices, dataSource, formatEvaluator, new SystemClock(), liveEdgeLatencyMs * 1000, - elapsedRealtimeOffsetMs * 1000, eventHandler, eventListener); + elapsedRealtimeOffsetMs * 1000); } /* package */ DashChunkSource(ManifestFetcher manifestFetcher, MediaPresentationDescription initialManifest, int adaptationSetIndex, int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator, - Clock systemClock, long liveEdgeLatencyUs, long elapsedRealtimeOffsetUs, - Handler eventHandler, EventListener eventListener) { + Clock systemClock, long liveEdgeLatencyUs, long elapsedRealtimeOffsetUs) { this.manifestFetcher = manifestFetcher; this.currentManifest = initialManifest; this.adaptationSetIndex = adaptationSetIndex; @@ -219,11 +189,8 @@ public class DashChunkSource implements ChunkSource { this.systemClock = systemClock; this.liveEdgeLatencyUs = liveEdgeLatencyUs; this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetUs; - this.eventHandler = eventHandler; - this.eventListener = eventListener; this.evaluation = new Evaluation(); this.headerBuilder = new StringBuilder(); - this.seekRangeValues = new long[2]; drmInitData = getDrmInitData(currentManifest, adaptationSetIndex); Representation[] representations = getFilteredRepresentations(currentManifest, @@ -262,11 +229,6 @@ public class DashChunkSource implements ChunkSource { return trackInfo; } - // VisibleForTesting - /* package */ TimeRange getSeekRange() { - return seekRange; - } - @Override public void enable() { fatalError = null; @@ -274,16 +236,6 @@ public class DashChunkSource implements ChunkSource { if (manifestFetcher != null) { manifestFetcher.enable(); } - DashSegmentIndex segmentIndex = - representationHolders.get(formats[0].id).representation.getIndex(); - if (segmentIndex == null) { - seekRange = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, currentManifest.duration * 1000); - notifySeekRangeChanged(seekRange); - } else { - long nowUs = getNowUs(); - updateAvailableSegmentBounds(segmentIndex, nowUs); - updateSeekRange(segmentIndex, nowUs); - } } @Override @@ -292,7 +244,6 @@ public class DashChunkSource implements ChunkSource { if (manifestFetcher != null) { manifestFetcher.disable(); } - seekRange = null; } @Override @@ -334,10 +285,6 @@ public class DashChunkSource implements ChunkSource { } currentManifest = newManifest; finishedCurrentManifest = false; - - long nowUs = getNowUs(); - updateAvailableSegmentBounds(newRepresentations[0].getIndex(), nowUs); - updateSeekRange(newRepresentations[0].getIndex(), nowUs); } // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where @@ -407,21 +354,36 @@ public class DashChunkSource implements ChunkSource { return; } + long nowUs; + if (elapsedRealtimeOffsetUs != 0) { + nowUs = (systemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs; + } else { + nowUs = System.currentTimeMillis() * 1000; + } + + int firstAvailableSegmentNum = segmentIndex.getFirstSegmentNum(); + int lastAvailableSegmentNum = segmentIndex.getLastSegmentNum(); + boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; + if (indexUnbounded) { + // The index is itself unbounded. We need to use the current time to calculate the range of + // available segments. + long liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; + if (currentManifest.timeShiftBufferDepth != -1) { + long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000; + firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, + segmentIndex.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs)); + } + // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the + // index of the last completed segment. + lastAvailableSegmentNum = segmentIndex.getSegmentNum(liveEdgeTimestampUs) - 1; + } + int segmentNum; - boolean indexUnbounded = segmentIndex.getLastSegmentNum() == DashSegmentIndex.INDEX_UNBOUNDED; if (queue.isEmpty()) { if (currentManifest.dynamic) { - seekRangeValues = seekRange.getCurrentBoundsUs(seekRangeValues); - seekPositionUs = Math.max(seekPositionUs, seekRangeValues[0]); - seekPositionUs = Math.min(seekPositionUs, seekRangeValues[1]); + seekPositionUs = getLiveSeekPosition(nowUs, indexUnbounded, segmentIndex.isExplicit()); } segmentNum = segmentIndex.getSegmentNum(seekPositionUs); - - // if the index is unbounded then the result of getSegmentNum isn't clamped to ensure that - // it doesn't exceed the last available segment. Clamp it here. - if (indexUnbounded) { - segmentNum = Math.min(segmentNum, lastAvailableSegmentNum); - } } else { MediaChunk previous = queue.get(out.queueSize - 1); segmentNum = previous.isLastChunk ? -1 @@ -490,59 +452,6 @@ public class DashChunkSource implements ChunkSource { // Do nothing. } - private void updateAvailableSegmentBounds(DashSegmentIndex segmentIndex, long nowUs) { - int indexFirstAvailableSegmentNum = segmentIndex.getFirstSegmentNum(); - int indexLastAvailableSegmentNum = segmentIndex.getLastSegmentNum(); - if (indexLastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) { - // The index is itself unbounded. We need to use the current time to calculate the range of - // available segments. - long liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; - if (currentManifest.timeShiftBufferDepth != -1) { - long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000; - indexFirstAvailableSegmentNum = Math.max(indexFirstAvailableSegmentNum, - segmentIndex.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs)); - } - // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the - // index of the last completed segment. - indexLastAvailableSegmentNum = segmentIndex.getSegmentNum(liveEdgeTimestampUs) - 1; - } - firstAvailableSegmentNum = indexFirstAvailableSegmentNum; - lastAvailableSegmentNum = indexLastAvailableSegmentNum; - } - - private void updateSeekRange(DashSegmentIndex segmentIndex, long nowUs) { - long earliestSeekPosition = segmentIndex.getTimeUs(firstAvailableSegmentNum); - long latestSeekPosition = segmentIndex.getTimeUs(lastAvailableSegmentNum) - + segmentIndex.getDurationUs(lastAvailableSegmentNum); - if (currentManifest.dynamic) { - long liveEdgeTimestampUs; - if (segmentIndex.getLastSegmentNum() == DashSegmentIndex.INDEX_UNBOUNDED) { - liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; - } else { - liveEdgeTimestampUs = segmentIndex.getTimeUs(segmentIndex.getLastSegmentNum()) - + segmentIndex.getDurationUs(segmentIndex.getLastSegmentNum()); - if (!segmentIndex.isExplicit()) { - // Some segments defined by the index may not be available yet. Bound the calculated live - // edge based on the elapsed time since the manifest became available. - liveEdgeTimestampUs = Math.min(liveEdgeTimestampUs, - nowUs - currentManifest.availabilityStartTime * 1000); - } - } - - // it's possible that the live edge latency actually puts our latest position before - // the earliest position in the case of a DVR-like stream that's just starting up, so - // in that case just return the earliest position instead - latestSeekPosition = Math.max(earliestSeekPosition, liveEdgeTimestampUs - liveEdgeLatencyUs); - } - - TimeRange newSeekRange = new TimeRange(TimeRange.TYPE_SNAPSHOT, earliestSeekPosition, - latestSeekPosition); - if (seekRange == null || !seekRange.equals(newSeekRange)) { - seekRange = newSeekRange; - notifySeekRangeChanged(seekRange); - } - } - private static boolean mimeTypeIsWebm(String mimeType) { return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); } @@ -603,12 +512,36 @@ public class DashChunkSource implements ChunkSource { } } - private long getNowUs() { - if (elapsedRealtimeOffsetUs != 0) { - return (systemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs; + /** + * For live playbacks, determines the seek position that snaps playback to be + * {@link #liveEdgeLatencyUs} behind the live edge of the current manifest + * + * @param nowUs An estimate of the current server time, in microseconds. + * @param indexUnbounded True if the segment index for this source is unbounded. False otherwise. + * @param indexExplicit True if the segment index is explicit. False otherwise. + * @return The seek position in microseconds. + */ + private long getLiveSeekPosition(long nowUs, boolean indexUnbounded, boolean indexExplicit) { + long liveEdgeTimestampUs; + if (indexUnbounded) { + liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; } else { - return System.currentTimeMillis() * 1000; + liveEdgeTimestampUs = Long.MIN_VALUE; + for (RepresentationHolder representationHolder : representationHolders.values()) { + DashSegmentIndex segmentIndex = representationHolder.segmentIndex; + int lastSegmentNum = segmentIndex.getLastSegmentNum(); + long indexLiveEdgeTimestampUs = segmentIndex.getTimeUs(lastSegmentNum) + + segmentIndex.getDurationUs(lastSegmentNum); + liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, indexLiveEdgeTimestampUs); + } + if (!indexExplicit) { + // Some segments defined by the index may not be available yet. Bound the calculated live + // edge based on the elapsed time since the manifest became available. + liveEdgeTimestampUs = Math.min(liveEdgeTimestampUs, + nowUs - currentManifest.availabilityStartTime * 1000); + } } + return liveEdgeTimestampUs - liveEdgeLatencyUs; } private static Representation[] getFilteredRepresentations(MediaPresentationDescription manifest, @@ -659,17 +592,6 @@ public class DashChunkSource implements ChunkSource { Collections.singletonList(period)); } - private void notifySeekRangeChanged(final TimeRange seekRange) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onSeekRangeChanged(seekRange); - } - }); - } - } - private static class RepresentationHolder { public final Representation representation; diff --git a/library/src/test/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java b/library/src/test/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java index 8270620985..1cdfa1efed 100644 --- a/library/src/test/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java +++ b/library/src/test/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java @@ -15,11 +15,9 @@ */ package com.google.android.exoplayer.dash; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.TimeRange; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.Format; @@ -57,19 +55,12 @@ public class DashChunkSourceTest extends InstrumentationTestCase { private static final FormatEvaluator EVALUATOR = new FixedEvaluator(); - private static final long VOD_DURATION = 30000; - - private static final long LIVE_SEGMENT_COUNT = 5; - private static final long LIVE_SEGMENT_DURATION_MS = 1000; - private static final long LIVE_TIMESHIFT_BUFFER_DEPTH_MS = - LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS; - - private static final long AVAILABILITY_START_TIME_MS = 60000; - private static final long AVAILABILITY_REALTIME_OFFSET_MS = 1000; - private static final long AVAILABILITY_CURRENT_TIME_MS = - AVAILABILITY_START_TIME_MS + LIVE_TIMESHIFT_BUFFER_DEPTH_MS - AVAILABILITY_REALTIME_OFFSET_MS; - - private static final long LIVE_SEEK_BEYOND_EDGE_MS = 60000; + private static final long AVAILABILITY_START_TIME = 0; + private static final long AVAILABILITY_LATENCY = 5000; + private static final long AVAILABILITY_REALTIME_OFFSET = 1000; + private static final long AVAILABILITY_CURRENT_TIME = + AVAILABILITY_START_TIME + AVAILABILITY_LATENCY - AVAILABILITY_REALTIME_OFFSET; + private static final FakeClock AVAILABILITY_CLOCK = new FakeClock(AVAILABILITY_CURRENT_TIME); private static final int TALL_HEIGHT = 200; private static final int WIDE_WIDTH = 400; @@ -99,21 +90,6 @@ public class DashChunkSourceTest extends InstrumentationTestCase { assertEquals(TALL_HEIGHT, out.getMaxVideoHeight()); } - public void testGetSeekRangeOnVod() { - DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(), AdaptationSet.TYPE_VIDEO, - null, null, mock(FormatEvaluator.class)); - chunkSource.enable(); - TimeRange seekRange = chunkSource.getSeekRange(); - - long[] seekRangeValuesUs = seekRange.getCurrentBoundsUs(null); - assertEquals(0, seekRangeValuesUs[0]); - assertEquals(VOD_DURATION * 1000, seekRangeValuesUs[1]); - - long[] seekRangeValuesMs = seekRange.getCurrentBoundsMs(null); - assertEquals(0, seekRangeValuesMs[0]); - assertEquals(VOD_DURATION, seekRangeValuesMs[1]); - } - public void testMaxVideoDimensionsLegacy() { SingleSegmentBase segmentBase1 = new SingleSegmentBase("https://example.com/1.mp4"); Representation representation1 = @@ -131,254 +107,147 @@ public class DashChunkSourceTest extends InstrumentationTestCase { assertEquals(TALL_HEIGHT, out.getMaxVideoHeight()); } - public void testLiveEdgeNoLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 0; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 0; - long seekRangeEndMs = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; - long chunkStartTimeMs = 4000; - long chunkEndTimeMs = 5000; + public void testLiveEdgeNoLatencyWithTimeline() { + DashChunkSource chunkSource = setupLiveEdgeTimelineTest(0L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); } - public void testLiveEdgeAlmostNoLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 1; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 0; - long seekRangeEndMs = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; - long chunkStartTimeMs = 4000; - long chunkEndTimeMs = 5000; + public void testLiveEdge500msLatencyWithTimeline() { + DashChunkSource chunkSource = setupLiveEdgeTimelineTest(500L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); } - public void testLiveEdge500msLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 500; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 0; - long seekRangeEndMs = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; - long chunkStartTimeMs = 4000; - long chunkEndTimeMs = 5000; + public void testLiveEdge1000msLatencyWithTimeline() { + DashChunkSource chunkSource = setupLiveEdgeTimelineTest(1000L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); } - public void testLiveEdge1000msLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 1000; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 0; - long seekRangeEndMs = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; - long chunkStartTimeMs = 4000; - long chunkEndTimeMs = 5000; + public void testLiveEdge1001msLatencyWithTimeline() { + DashChunkSource chunkSource = setupLiveEdgeTimelineTest(1001L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + assertEquals(3000000L, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(4000000L, ((MediaChunk) out.chunk).endTimeUs); } - public void testLiveEdge1001msLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 1001; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 0; - long seekRangeEndMs = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; - long chunkStartTimeMs = 3000; - long chunkEndTimeMs = 4000; + public void testLiveEdge2500msLatencyWithTimeline() { + DashChunkSource chunkSource = setupLiveEdgeTimelineTest(2500L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + assertEquals(2000000L, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(3000000L, ((MediaChunk) out.chunk).endTimeUs); } - public void testLiveEdge2500msLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 2500; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 0; - long seekRangeEndMs = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; - long chunkStartTimeMs = 2000; - long chunkEndTimeMs = 3000; + public void testLiveEdgeVeryHighLatencyWithTimeline() { + DashChunkSource chunkSource = setupLiveEdgeTimelineTest(10000L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + assertEquals(0L, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(1000000L, ((MediaChunk) out.chunk).endTimeUs); } - public void testLiveEdgeVeryHighLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 10000; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 0; - long seekRangeEndMs = 0; - long chunkStartTimeMs = 0; - long chunkEndTimeMs = 1000; + public void testLiveEdgeNoLatencyWithTemplate() { + DashChunkSource chunkSource = setupLiveEdgeTemplateTest(0L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + // this should actually return the "5th" segment, but it currently returns the "6th", which + // doesn't actually exist yet; this will be resolved in a subsequent cl (cl/87518875). + //assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); + //assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); } - public void testLiveEdgeNoLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 0; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 3000; - long seekRangeEndMs = 3000 + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; - long chunkStartTimeMs = 7000; - long chunkEndTimeMs = 8000; + public void testLiveEdgeAlmostNoLatencyWithTemplate() { + DashChunkSource chunkSource = setupLiveEdgeTemplateTest(1L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); } - public void testLiveEdgeAlmostNoLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 1; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 3000; - long seekRangeEndMs = 3000 + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; - long chunkStartTimeMs = 7000; - long chunkEndTimeMs = 8000; + public void testLiveEdge500msLatencyWithTemplate() { + DashChunkSource chunkSource = setupLiveEdgeTemplateTest(500L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); } - public void testLiveEdge500msLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 500; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 3000; - long seekRangeEndMs = 3000 + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; - long chunkStartTimeMs = 7000; - long chunkEndTimeMs = 8000; + public void testLiveEdge1000msLatencyWithTemplate() { + DashChunkSource chunkSource = setupLiveEdgeTemplateTest(1000L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + assertEquals(4000000L, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(5000000L, ((MediaChunk) out.chunk).endTimeUs); } - public void testLiveEdge1000msLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 1000; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 3000; - long seekRangeEndMs = 3000 + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; - long chunkStartTimeMs = 7000; - long chunkEndTimeMs = 8000; + public void testLiveEdge1001msLatencyWithTemplate() { + DashChunkSource chunkSource = setupLiveEdgeTemplateTest(1001L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + assertEquals(3000000L, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(4000000L, ((MediaChunk) out.chunk).endTimeUs); } - public void testLiveEdge1001msLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 1001; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 3000; - long seekRangeEndMs = 3000 + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; - long chunkStartTimeMs = 6000; - long chunkEndTimeMs = 7000; + public void testLiveEdge2500msLatencyWithTemplate() { + DashChunkSource chunkSource = setupLiveEdgeTemplateTest(2500L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + assertEquals(2000000L, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(3000000L, ((MediaChunk) out.chunk).endTimeUs); } - public void testLiveEdge2500msLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 2500; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 3000; - long seekRangeEndMs = 3000 + LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS - liveEdgeLatencyMs; - long chunkStartTimeMs = 5000; - long chunkEndTimeMs = 6000; + public void testLiveEdgeVeryHighLatencyWithTemplate() { + DashChunkSource chunkSource = setupLiveEdgeTemplateTest(10000L); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + chunkSource.getChunkOperation(queue, 0, 0, out); - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdgeVeryHighLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 10000; - long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; - long seekRangeStartMs = 3000; - long seekRangeEndMs = 3000; - long chunkStartTimeMs = 3000; - long chunkEndTimeMs = 4000; - - checkLiveEdgeLatencyWithTimeline(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, 0, 0, 1000); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + assertEquals(0L, ((MediaChunk) out.chunk).startTimeUs); + assertEquals(1000000L, ((MediaChunk) out.chunk).endTimeUs); } private static MediaPresentationDescription generateMpd(boolean live, - List representations, boolean limitTimeshiftBuffer) { + List representations) { Representation firstRepresentation = representations.get(0); AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); Period period = new Period(null, firstRepresentation.periodStartMs, firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet)); long duration = (live) ? TrackRenderer.UNKNOWN_TIME_US : firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs; - return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, duration, -1, live, -1, - (limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1, + return new MediaPresentationDescription(AVAILABILITY_START_TIME, duration, -1, live, -1, -1, null, Collections.singletonList(period)); } @@ -387,126 +256,72 @@ public class DashChunkSourceTest extends InstrumentationTestCase { SingleSegmentBase segmentBase1 = new SingleSegmentBase("https://example.com/1.mp4"); Representation representation1 = - Representation.newInstance(0, VOD_DURATION, null, 0, TALL_VIDEO, segmentBase1); + Representation.newInstance(0, 0, null, 0, TALL_VIDEO, segmentBase1); representations.add(representation1); SingleSegmentBase segmentBase2 = new SingleSegmentBase("https://example.com/2.mp4"); Representation representation2 = - Representation.newInstance(0, VOD_DURATION, null, 0, WIDE_VIDEO, segmentBase2); + Representation.newInstance(0, 0, null, 0, WIDE_VIDEO, segmentBase2); representations.add(representation2); - return generateMpd(false, representations, false); + return generateMpd(false, representations); } - private static MediaPresentationDescription generateLiveMpdWithTimeline(long startTime) { + private static MediaPresentationDescription generateLiveMpdWithTimeline() { List representations = new ArrayList<>(); List segmentTimeline = new ArrayList<>(); + segmentTimeline.add(new SegmentTimelineElement(0L, 1000L)); + segmentTimeline.add(new SegmentTimelineElement(1000L, 1000L)); + segmentTimeline.add(new SegmentTimelineElement(2000L, 1000L)); + segmentTimeline.add(new SegmentTimelineElement(3000L, 1000L)); + segmentTimeline.add(new SegmentTimelineElement(4000L, 1000L)); List mediaSegments = new ArrayList<>(); - long byteStart = 0; - for (int i = 0; i < LIVE_SEGMENT_COUNT; i++) { - segmentTimeline.add(new SegmentTimelineElement(startTime, LIVE_SEGMENT_DURATION_MS)); - mediaSegments.add(new RangedUri("", "", byteStart, 500L)); - startTime += LIVE_SEGMENT_DURATION_MS; - byteStart += 500; - } + mediaSegments.add(new RangedUri("", "", 0L, 500L)); + mediaSegments.add(new RangedUri("", "", 500L, 500L)); + mediaSegments.add(new RangedUri("", "", 1000L, 500L)); + mediaSegments.add(new RangedUri("", "", 1500L, 500L)); + mediaSegments.add(new RangedUri("", "", 2000L, 500L)); MultiSegmentBase segmentBase = new SegmentList(null, 1000, 0, - TrackRenderer.UNKNOWN_TIME_US, 0, TrackRenderer.UNKNOWN_TIME_US, segmentTimeline, + TrackRenderer.UNKNOWN_TIME_US, 1, TrackRenderer.UNKNOWN_TIME_US, segmentTimeline, mediaSegments); - Representation representation = Representation.newInstance(startTime, - TrackRenderer.UNKNOWN_TIME_US, null, 0, REGULAR_VIDEO, segmentBase); + Representation representation = Representation.newInstance(0, TrackRenderer.UNKNOWN_TIME_US, + null, 0, REGULAR_VIDEO, segmentBase); representations.add(representation); - return generateMpd(true, representations, false); + return generateMpd(true, representations); } - private static MediaPresentationDescription generateLiveMpdWithTemplate( - boolean limitTimeshiftBuffer) { + private static MediaPresentationDescription generateLiveMpdWithTemplate() { List representations = new ArrayList<>(); UrlTemplate initializationTemplate = null; UrlTemplate mediaTemplate = UrlTemplate.compile("$RepresentationID$/$Number$"); MultiSegmentBase segmentBase = new SegmentTemplate(null, 1000, 0, - TrackRenderer.UNKNOWN_TIME_US, 0, LIVE_SEGMENT_DURATION_MS, null, + TrackRenderer.UNKNOWN_TIME_US, 1, 1000, null, initializationTemplate, mediaTemplate, "http://www.youtube.com"); Representation representation = Representation.newInstance(0, TrackRenderer.UNKNOWN_TIME_US, null, 0, REGULAR_VIDEO, segmentBase); representations.add(representation); - return generateMpd(true, representations, limitTimeshiftBuffer); + return generateMpd(true, representations); } - private DashChunkSource setupLiveEdgeTimelineTest(long startTime, long liveEdgeLatencyMs) { - MediaPresentationDescription manifest = generateLiveMpdWithTimeline(startTime); + private DashChunkSource setupLiveEdgeTimelineTest(long liveEdgeLatencyMs) { + MediaPresentationDescription manifest = generateLiveMpdWithTimeline(); when(mockManifestFetcher.getManifest()).thenReturn(manifest); - DashChunkSource chunkSource = new DashChunkSource(mockManifestFetcher, manifest, - AdaptationSet.TYPE_VIDEO, null, mockDataSource, EVALUATOR, - new FakeClock(AVAILABILITY_CURRENT_TIME_MS + startTime), liveEdgeLatencyMs * 1000, - AVAILABILITY_REALTIME_OFFSET_MS * 1000, null, null); - chunkSource.enable(); - return chunkSource; + return new DashChunkSource(mockManifestFetcher, manifest, AdaptationSet.TYPE_VIDEO, null, + mockDataSource, EVALUATOR, AVAILABILITY_CLOCK, liveEdgeLatencyMs * 1000, + AVAILABILITY_REALTIME_OFFSET * 1000); } - private DashChunkSource setupLiveEdgeTemplateTest(long startTime, long liveEdgeLatencyMs, - boolean limitTimeshiftBuffer) { - MediaPresentationDescription manifest = generateLiveMpdWithTemplate(limitTimeshiftBuffer); + private DashChunkSource setupLiveEdgeTemplateTest(long liveEdgeLatencyMs) { + MediaPresentationDescription manifest = generateLiveMpdWithTemplate(); when(mockManifestFetcher.getManifest()).thenReturn(manifest); - DashChunkSource chunkSource = new DashChunkSource(mockManifestFetcher, manifest, - AdaptationSet.TYPE_VIDEO, null, mockDataSource, EVALUATOR, - new FakeClock(AVAILABILITY_CURRENT_TIME_MS + startTime), liveEdgeLatencyMs * 1000, - AVAILABILITY_REALTIME_OFFSET_MS * 1000, null, null); - chunkSource.enable(); - return chunkSource; - } - - private void checkLiveEdgeLatencyWithTimeline(long startTimeMs, long liveEdgeLatencyMs, - long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs, long chunkStartTimeMs, - long chunkEndTimeMs) { - DashChunkSource chunkSource = setupLiveEdgeTimelineTest(startTimeMs, liveEdgeLatencyMs); - List queue = new ArrayList<>(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, seekPositionMs * 1000, 0, out); - TimeRange seekRange = chunkSource.getSeekRange(); - - assertNotNull(out.chunk); - long[] seekRangeValuesUs = seekRange.getCurrentBoundsUs(null); - assertEquals(seekRangeStartMs * 1000, seekRangeValuesUs[0]); - assertEquals(seekRangeEndMs * 1000, seekRangeValuesUs[1]); - assertEquals(chunkStartTimeMs * 1000, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(chunkEndTimeMs * 1000, ((MediaChunk) out.chunk).endTimeUs); - } - - private void checkLiveEdgeLatencyWithTemplate(long startTimeMs, long liveEdgeLatencyMs, - long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs, long chunkStartTimeMs, - long chunkEndTimeMs, boolean limitTimeshiftBuffer) { - DashChunkSource chunkSource = setupLiveEdgeTemplateTest(startTimeMs, liveEdgeLatencyMs, - limitTimeshiftBuffer); - List queue = new ArrayList<>(); - ChunkOperationHolder out = new ChunkOperationHolder(); - chunkSource.getChunkOperation(queue, seekPositionMs * 1000, 0, out); - TimeRange seekRange = chunkSource.getSeekRange(); - - assertNotNull(out.chunk); - long[] seekRangeValuesUs = seekRange.getCurrentBoundsUs(null); - assertEquals(seekRangeStartMs * 1000, seekRangeValuesUs[0]); - assertEquals(seekRangeEndMs * 1000, seekRangeValuesUs[1]); - assertEquals(chunkStartTimeMs * 1000, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(chunkEndTimeMs * 1000, ((MediaChunk) out.chunk).endTimeUs); - } - - private void checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(long startTimeMs, - long liveEdgeLatencyMs, long seekPositionMs, long seekRangeEndMs, - long chunkStartTimeMs, long chunkEndTimeMs) { - checkLiveEdgeLatencyWithTemplate(startTimeMs, liveEdgeLatencyMs, seekPositionMs, 0, - seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs, false); - } - - private void checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(long startTimeMs, - long liveEdgeLatencyMs, long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs, - long chunkStartTimeMs, long chunkEndTimeMs) { - checkLiveEdgeLatencyWithTemplate(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs, true); + return new DashChunkSource(mockManifestFetcher, manifest, AdaptationSet.TYPE_VIDEO, null, + mockDataSource, EVALUATOR, AVAILABILITY_CLOCK, liveEdgeLatencyMs * 1000, + AVAILABILITY_REALTIME_OFFSET * 1000); } } From b806109cfd63f4aedf19f388c1091b81f61bead2 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 28 May 2015 17:14:52 +0100 Subject: [PATCH 5/7] Make HlsSampleSource use LoadControl. This will be needed for keeping WebVTT subtitle and separate audio tracks is sync whilst loading. Issue: #151 --- .../demo/player/HlsRendererBuilder.java | 12 ++- .../exoplayer/hls/HlsSampleSource.java | 96 ++++++++++++------- 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java index 43cf49924e..39b8393059 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.demo.player; +import com.google.android.exoplayer.DefaultLoadControl; +import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; @@ -32,6 +34,7 @@ import com.google.android.exoplayer.metadata.Id3Parser; import com.google.android.exoplayer.metadata.MetadataTrackRenderer; import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer; import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DefaultAllocator; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.DefaultUriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; @@ -50,8 +53,8 @@ import java.util.Map; */ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback { - private static final int REQUESTED_BUFFER_SIZE = 18 * 1024 * 1024; - private static final long REQUESTED_BUFFER_DURATION_MS = 40000; + private static final int BUFFER_SEGMENT_SIZE = 256 * 1024; + private static final int BUFFER_SEGMENTS = 64; private final Context context; private final String userAgent; @@ -89,6 +92,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback extractors; - private final Allocator allocator; private final boolean frameAccurateSeeking; private final int minLoadableRetryCount; - private final int requestedBufferSize; - private final long requestedBufferDurationUs; + private final int bufferSizeContribution; private final int eventSourceId; + private final LoadControl loadControl; private final Handler eventHandler; private final EventListener eventListener; private int remainingReleaseCount; private boolean prepared; + private boolean loadControlRegistered; private int trackCount; private int enabledTrackCount; private boolean[] trackEnabledStates; @@ -92,36 +90,35 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { private long currentLoadableExceptionTimestamp; private long currentLoadStartTimeMs; - public HlsSampleSource(HlsChunkSource chunkSource, boolean frameAccurateSeeking, - int downstreamRendererCount, int requestedBufferSize, long requestedBufferDurationMs) { - this(chunkSource, frameAccurateSeeking, downstreamRendererCount, requestedBufferSize, - requestedBufferDurationMs, null, null, 0); + public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution, boolean frameAccurateSeeking, int downstreamRendererCount) { + this(chunkSource, loadControl, bufferSizeContribution, frameAccurateSeeking, + downstreamRendererCount, null, null, 0); } - public HlsSampleSource(HlsChunkSource chunkSource, boolean frameAccurateSeeking, - int downstreamRendererCount, int requestedBufferSize, long requestedBufferDurationMs, + public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution, boolean frameAccurateSeeking, int downstreamRendererCount, Handler eventHandler, EventListener eventListener, int eventSourceId) { - this(chunkSource, frameAccurateSeeking, downstreamRendererCount, requestedBufferSize, - requestedBufferDurationMs, eventHandler, eventListener, eventSourceId, + this(chunkSource, loadControl, bufferSizeContribution, frameAccurateSeeking, + downstreamRendererCount, eventHandler, eventListener, eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT); } - public HlsSampleSource(HlsChunkSource chunkSource, boolean frameAccurateSeeking, - int downstreamRendererCount, int requestedBufferSize, long requestedBufferDurationMs, - Handler eventHandler, EventListener eventListener, - int eventSourceId, int minLoadableRetryCount) { + public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution, boolean frameAccurateSeeking, int downstreamRendererCount, + Handler eventHandler, EventListener eventListener, int eventSourceId, + int minLoadableRetryCount) { this.chunkSource = chunkSource; + this.loadControl = loadControl; + this.bufferSizeContribution = bufferSizeContribution; this.frameAccurateSeeking = frameAccurateSeeking; this.remainingReleaseCount = downstreamRendererCount; - this.requestedBufferSize = requestedBufferSize; - this.requestedBufferDurationUs = requestedBufferDurationMs * 1000; this.minLoadableRetryCount = minLoadableRetryCount; this.eventHandler = eventHandler; this.eventListener = eventListener; this.eventSourceId = eventSourceId; this.pendingResetPositionUs = NO_RESET_PENDING; extractors = new LinkedList<>(); - allocator = new DefaultAllocator(BUFFER_FRAGMENT_LENGTH); } @Override @@ -150,6 +147,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (loader == null) { loader = new Loader("Loader:HLS"); } + if (!loadControlRegistered) { + loadControl.register(this, bufferSizeContribution); + loadControlRegistered = true; + } if (!loader.isLoading()) { // We're going to have to start loading a chunk to get what we need for preparation. We should // attempt to load the chunk at positionUs, so that we'll already be loading the correct chunk @@ -182,6 +183,10 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { trackEnabledStates[track] = true; downstreamMediaFormats[track] = null; downstreamFormat = null; + if (!loadControlRegistered) { + loadControl.register(this, bufferSizeContribution); + loadControlRegistered = true; + } if (enabledTrackCount == 1) { seekToUs(positionUs); } @@ -194,12 +199,16 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { enabledTrackCount--; trackEnabledStates[track] = false; pendingDiscontinuities[track] = false; + if (loadControlRegistered) { + loadControl.unregister(this); + loadControlRegistered = false; + } if (enabledTrackCount == 0) { if (loader.isLoading()) { loader.cancelLoading(); } else { clearState(); - allocator.trim(0); + loadControl.trimAllocator(); } } } @@ -357,7 +366,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { maybeStartLoading(); } else { clearState(); - allocator.trim(0); + loadControl.trimAllocator(); } } @@ -368,7 +377,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { restartFrom(pendingResetPositionUs); } else { clearState(); - allocator.trim(0); + loadControl.trimAllocator(); } } @@ -464,13 +473,23 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } private void maybeStartLoading() { - if (currentLoadableExceptionFatal || loadingFinished || loader.isLoading()) { + if (currentLoadableExceptionFatal) { + // We've failed, but we still need to update the control with our current state. + loadControl.update(this, downstreamPositionUs, -1, false, true); return; } + long now = SystemClock.elapsedRealtime(); + long nextLoadPositionUs = getNextLoadPositionUs(); boolean isBackedOff = currentLoadableException != null; + boolean loadingOrBackedOff = loader.isLoading() || isBackedOff; + + // Update the control with our current state, and determine whether we're the next loader. + boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, + loadingOrBackedOff, false); + if (isBackedOff) { - long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp; + long elapsedMillis = now - currentLoadableExceptionTimestamp; if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { currentLoadableException = null; loader.startLoading(currentLoadable, this); @@ -478,20 +497,17 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { return; } - if (previousTsLoadable != null - && (previousTsLoadable.endTimeUs - downstreamPositionUs >= requestedBufferDurationUs - || allocator.getTotalBytesAllocated() >= requestedBufferSize)) { - // We already have the target amount of data or time buffered. + if (loader.isLoading() || !nextLoader) { return; } - Chunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable, - pendingResetPositionUs, downstreamPositionUs); + Chunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable, pendingResetPositionUs, + downstreamPositionUs); if (nextLoadable == null) { return; } - currentLoadStartTimeMs = SystemClock.elapsedRealtime(); + currentLoadStartTimeMs = now; currentLoadable = nextLoadable; if (isTsChunk(currentLoadable)) { TsChunk tsChunk = (TsChunk) currentLoadable; @@ -500,7 +516,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { } HlsExtractorWrapper extractorWrapper = tsChunk.extractorWrapper; if (extractors.isEmpty() || extractors.getLast() != extractorWrapper) { - extractorWrapper.init(allocator); + extractorWrapper.init(loadControl.getAllocator()); extractors.addLast(extractorWrapper); } notifyLoadStarted(tsChunk.dataSpec.length, tsChunk.type, tsChunk.trigger, tsChunk.format, @@ -513,6 +529,18 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { loader.startLoading(currentLoadable, this); } + /** + * Gets the next load time, assuming that the next load starts where the previous chunk ended (or + * from the pending reset time, if there is one). + */ + private long getNextLoadPositionUs() { + if (isPendingReset()) { + return pendingResetPositionUs; + } else { + return previousTsLoadable.isLastChunk ? -1 : previousTsLoadable.endTimeUs; + } + } + private boolean isTsChunk(Chunk chunk) { return chunk instanceof TsChunk; } From a85a169604ef09a12c72a82b6f85dd79dadde7fd Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 28 May 2015 17:18:26 +0100 Subject: [PATCH 6/7] Simplify the demo app. --- .../android/exoplayer/demo/DemoUtil.java | 102 ------------- .../exoplayer/demo/PlayerActivity.java | 79 ++++++---- .../android/exoplayer/demo/Samples.java | 46 +++--- .../SmoothStreamingTestMediaDrmCallback.java | 5 +- .../demo/WidevineTestMediaDrmCallback.java | 5 +- .../demo/player/DashRendererBuilder.java | 57 ++------ .../demo/player/DebugTrackRenderer.java | 138 ------------------ .../exoplayer/demo/player/DemoPlayer.java | 81 +++++----- .../demo/player/ExtractorRendererBuilder.java | 13 +- .../demo/player/HlsRendererBuilder.java | 17 +-- .../SmoothStreamingRendererBuilder.java | 39 +---- .../drm/StreamingDrmSessionManager.java | 30 ++-- .../drm}/UnsupportedDrmException.java | 14 +- .../exoplayer/util/DebugTextViewHelper.java | 125 ++++++++++++++++ .../google/android/exoplayer/util/Util.java | 54 +++++++ 15 files changed, 357 insertions(+), 448 deletions(-) delete mode 100644 demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java delete mode 100644 demo/src/main/java/com/google/android/exoplayer/demo/player/DebugTrackRenderer.java rename {demo/src/main/java/com/google/android/exoplayer/demo/player => library/src/main/java/com/google/android/exoplayer/drm}/UnsupportedDrmException.java (70%) create mode 100644 library/src/main/java/com/google/android/exoplayer/util/DebugTextViewHelper.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java b/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java deleted file mode 100644 index 558b294f09..0000000000 --- a/demo/src/main/java/com/google/android/exoplayer/demo/DemoUtil.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer.demo; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.CookieHandler; -import java.net.CookieManager; -import java.net.CookiePolicy; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Map; - -/** - * Utility methods for the demo application. - */ -public class DemoUtil { - - public static final int TYPE_DASH = 0; - public static final int TYPE_SS = 1; - public static final int TYPE_HLS = 2; - public static final int TYPE_MP4 = 3; - public static final int TYPE_MP3 = 4; - public static final int TYPE_M4A = 5; - public static final int TYPE_WEBM = 6; - public static final int TYPE_TS = 7; - public static final int TYPE_AAC = 8; - - private static final CookieManager defaultCookieManager; - - static { - defaultCookieManager = new CookieManager(); - defaultCookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); - } - - public static byte[] executePost(String url, byte[] data, Map requestProperties) - throws IOException { - HttpURLConnection urlConnection = null; - try { - urlConnection = (HttpURLConnection) new URL(url).openConnection(); - urlConnection.setRequestMethod("POST"); - urlConnection.setDoOutput(data != null); - urlConnection.setDoInput(true); - if (requestProperties != null) { - for (Map.Entry requestProperty : requestProperties.entrySet()) { - urlConnection.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); - } - } - if (data != null) { - OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream()); - out.write(data); - out.close(); - } - InputStream in = new BufferedInputStream(urlConnection.getInputStream()); - return convertInputStreamToByteArray(in); - } finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } - } - } - - private static byte[] convertInputStreamToByteArray(InputStream inputStream) throws IOException { - byte[] bytes = null; - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - byte data[] = new byte[1024]; - int count; - while ((count = inputStream.read(data)) != -1) { - bos.write(data, 0, count); - } - bos.flush(); - bos.close(); - inputStream.close(); - bytes = bos.toByteArray(); - return bytes; - } - - public static void setDefaultCookieManager() { - CookieHandler currentHandler = CookieHandler.getDefault(); - if (currentHandler != defaultCookieManager) { - CookieHandler.setDefault(defaultCookieManager); - } - } - -} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index 822c85ec4e..a47d082e0a 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -25,7 +25,7 @@ import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.player.ExtractorRendererBuilder; import com.google.android.exoplayer.demo.player.HlsRendererBuilder; import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder; -import com.google.android.exoplayer.demo.player.UnsupportedDrmException; +import com.google.android.exoplayer.drm.UnsupportedDrmException; import com.google.android.exoplayer.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer.extractor.mp4.Mp4Extractor; import com.google.android.exoplayer.extractor.ts.AdtsExtractor; @@ -37,6 +37,7 @@ import com.google.android.exoplayer.metadata.TxxxMetadata; import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.SubtitleLayout; +import com.google.android.exoplayer.util.DebugTextViewHelper; import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.VerboseLogUtil; @@ -65,6 +66,9 @@ import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.TextView; import android.widget.Toast; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; import java.util.List; import java.util.Map; @@ -75,14 +79,30 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, DemoPlayer.Listener, DemoPlayer.CaptionListener, DemoPlayer.Id3MetadataListener, AudioCapabilitiesReceiver.Listener { + public static final int TYPE_DASH = 0; + public static final int TYPE_SS = 1; + public static final int TYPE_HLS = 2; + public static final int TYPE_MP4 = 3; + public static final int TYPE_MP3 = 4; + public static final int TYPE_FMP4 = 5; + public static final int TYPE_WEBM = 6; + public static final int TYPE_TS = 7; + public static final int TYPE_AAC = 8; + public static final int TYPE_M4A = 9; + public static final String CONTENT_TYPE_EXTRA = "content_type"; public static final String CONTENT_ID_EXTRA = "content_id"; private static final String TAG = "PlayerActivity"; - private static final int MENU_GROUP_TRACKS = 1; private static final int ID_OFFSET = 2; + private static final CookieManager defaultCookieManager; + static { + defaultCookieManager = new CookieManager(); + defaultCookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); + } + private EventLogger eventLogger; private MediaController mediaController; private View debugRootView; @@ -97,6 +117,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private Button retryButton; private DemoPlayer player; + private DebugTextViewHelper debugViewHelper; private boolean playerNeedsPrepare; private long playerPosition; @@ -162,7 +183,10 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, audioButton = (Button) findViewById(R.id.audio_controls); textButton = (Button) findViewById(R.id.text_controls); - DemoUtil.setDefaultCookieManager(); + CookieHandler currentHandler = CookieHandler.getDefault(); + if (currentHandler != defaultCookieManager) { + CookieHandler.setDefault(defaultCookieManager); + } } @Override @@ -220,31 +244,26 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private RendererBuilder getRendererBuilder() { String userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); switch (contentType) { - case DemoUtil.TYPE_SS: + case TYPE_SS: return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(), - new SmoothStreamingTestMediaDrmCallback(), debugTextView); - case DemoUtil.TYPE_DASH: + new SmoothStreamingTestMediaDrmCallback()); + case TYPE_DASH: return new DashRendererBuilder(this, userAgent, contentUri.toString(), - new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities); - case DemoUtil.TYPE_HLS: - return new HlsRendererBuilder(this, userAgent, contentUri.toString(), debugTextView, - audioCapabilities); - case DemoUtil.TYPE_M4A: // There are no file format differences between M4A and MP4. - case DemoUtil.TYPE_MP4: - return new ExtractorRendererBuilder(this, userAgent, contentUri, debugTextView, - new Mp4Extractor()); - case DemoUtil.TYPE_MP3: - return new ExtractorRendererBuilder(this, userAgent, contentUri, debugTextView, - new Mp3Extractor()); - case DemoUtil.TYPE_TS: - return new ExtractorRendererBuilder(this, userAgent, contentUri, debugTextView, + new WidevineTestMediaDrmCallback(contentId), audioCapabilities); + case TYPE_HLS: + return new HlsRendererBuilder(this, userAgent, contentUri.toString(), audioCapabilities); + case TYPE_M4A: // There are no file format differences between M4A and MP4. + case TYPE_MP4: + return new ExtractorRendererBuilder(this, userAgent, contentUri, new Mp4Extractor()); + case TYPE_MP3: + return new ExtractorRendererBuilder(this, userAgent, contentUri, new Mp3Extractor()); + case TYPE_TS: + return new ExtractorRendererBuilder(this, userAgent, contentUri, new TsExtractor(0, audioCapabilities)); - case DemoUtil.TYPE_AAC: - return new ExtractorRendererBuilder(this, userAgent, contentUri, debugTextView, - new AdtsExtractor()); - case DemoUtil.TYPE_WEBM: - return new ExtractorRendererBuilder(this, userAgent, contentUri, debugTextView, - new WebmExtractor()); + case TYPE_AAC: + return new ExtractorRendererBuilder(this, userAgent, contentUri, new AdtsExtractor()); + case TYPE_WEBM: + return new ExtractorRendererBuilder(this, userAgent, contentUri, new WebmExtractor()); default: throw new IllegalStateException("Unsupported type: " + contentType); } @@ -265,6 +284,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, player.addListener(eventLogger); player.setInfoListener(eventLogger); player.setInternalErrorListener(eventLogger); + debugViewHelper = new DebugTextViewHelper(player, debugTextView); + debugViewHelper.start(); } if (playerNeedsPrepare) { player.prepare(); @@ -277,6 +298,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private void releasePlayer() { if (player != null) { + debugViewHelper.stop(); + debugViewHelper = null; playerPosition = player.getCurrentPosition(); player.release(); player = null; @@ -322,11 +345,9 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, if (e instanceof UnsupportedDrmException) { // Special case DRM failures. UnsupportedDrmException unsupportedDrmException = (UnsupportedDrmException) e; - int stringId = unsupportedDrmException.reason == UnsupportedDrmException.REASON_NO_DRM - ? R.string.drm_error_not_supported + int stringId = Util.SDK_INT < 18 ? R.string.drm_error_not_supported : unsupportedDrmException.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.drm_error_unsupported_scheme - : R.string.drm_error_unknown; + ? R.string.drm_error_unsupported_scheme : R.string.drm_error_unknown; Toast.makeText(getApplicationContext(), stringId, Toast.LENGTH_LONG).show(); } playerNeedsPrepare = true; diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index f68bb58892..cda6b9cc2c 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -47,12 +47,12 @@ import java.util.Locale; "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&" + "ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7." - + "8506521BFC350652163895D4C26DEE124209AA9E&key=ik0", DemoUtil.TYPE_DASH), + + "8506521BFC350652163895D4C26DEE124209AA9E&key=ik0", PlayerActivity.TYPE_DASH), new Sample("Google Play", "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" + "as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&" + "ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29." - + "84308FF04844498CE6FBCE4731507882B8307798&key=ik0", DemoUtil.TYPE_DASH), + + "84308FF04844498CE6FBCE4731507882B8307798&key=ik0", PlayerActivity.TYPE_DASH), }; public static final Sample[] YOUTUBE_DASH_WEBM = new Sample[] { @@ -60,21 +60,21 @@ import java.util.Locale; "http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?" + "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&" + "ipbits=0&expire=19000000000&signature=249B04F79E984D7F86B4D8DB48AE6FAF41C17AB3." - + "7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0", DemoUtil.TYPE_DASH), + + "7B9F0EC0505E1566E59B8E488E9419F253DDF413&key=ik0", PlayerActivity.TYPE_DASH), new Sample("Google Play", "http://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?" + "as=fmp4_audio_clear,webm2_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&" + "ipbits=0&expire=19000000000&signature=B1C2A74783AC1CC4865EB312D7DD2D48230CC9FD." - + "BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0", DemoUtil.TYPE_DASH), + + "BD153B9882175F1F94BFE5141A5482313EA38E8D&key=ik0", PlayerActivity.TYPE_DASH), }; public static final Sample[] SMOOTHSTREAMING = new Sample[] { new Sample("Super speed", "http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism", - DemoUtil.TYPE_SS), + PlayerActivity.TYPE_SS), new Sample("Super speed (PlayReady)", "http://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism", - DemoUtil.TYPE_SS), + PlayerActivity.TYPE_SS), }; public static final Sample[] WIDEVINE_GTS = new Sample[] { @@ -82,72 +82,72 @@ import java.util.Locale; "http://www.youtube.com/api/manifest/dash/id/d286538032258a1c/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0" + "&ipbits=0&expire=19000000000&signature=477CF7D478BE26C205045D507E9358F85F84C065." - + "8971631EB657BC33EC2F48A2FF4211956760C3E9&key=ik0", DemoUtil.TYPE_DASH), + + "8971631EB657BC33EC2F48A2FF4211956760C3E9&key=ik0", PlayerActivity.TYPE_DASH), new Sample("WV: HDCP not required", "48fcc369939ac96c", "http://www.youtube.com/api/manifest/dash/id/48fcc369939ac96c/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0" + "&ipbits=0&expire=19000000000&signature=171DAE48D00B5BE7434BC1A9F84DAE0463C7EA7A." - + "0925B4DBB5605BEE9F5D088C48F25F5108E96191&key=ik0", DemoUtil.TYPE_DASH), + + "0925B4DBB5605BEE9F5D088C48F25F5108E96191&key=ik0", PlayerActivity.TYPE_DASH), new Sample("WV: HDCP required", "e06c39f1151da3df", "http://www.youtube.com/api/manifest/dash/id/e06c39f1151da3df/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0" + "&ipbits=0&expire=19000000000&signature=8D3B8AF4E3F72B7F127C8D0D39B7AFCF37B30519." - + "A118BADEBF3582AD2CC257B0EE6E579C6955D8AA&key=ik0", DemoUtil.TYPE_DASH), + + "A118BADEBF3582AD2CC257B0EE6E579C6955D8AA&key=ik0", PlayerActivity.TYPE_DASH), new Sample("WV: Secure video path required", "0894c7c8719b28a0", "http://www.youtube.com/api/manifest/dash/id/0894c7c8719b28a0/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0" + "&ipbits=0&expire=19000000000&signature=A41D835C7387885A4A820628F57E481E00095931." - + "9D50DBEEB5E37344647EE11BDA129A7FCDE8B7B9&key=ik0", DemoUtil.TYPE_DASH), + + "9D50DBEEB5E37344647EE11BDA129A7FCDE8B7B9&key=ik0", PlayerActivity.TYPE_DASH), new Sample("WV: HDCP + secure video path required", "efd045b1eb61888a", "http://www.youtube.com/api/manifest/dash/id/efd045b1eb61888a/source/youtube" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0" + "&ipbits=0&expire=19000000000&signature=A97C9032C9D0C74F1643DB17C178873887C229E4." - + "0A657BF6F23C8BC1538F276137383478330B76DE&key=ik0", DemoUtil.TYPE_DASH), + + "0A657BF6F23C8BC1538F276137383478330B76DE&key=ik0", PlayerActivity.TYPE_DASH), new Sample("WV: 30s license duration (fails at ~30s)", "f9a34cab7b05881a", "http://www.youtube.com/api/manifest/dash/id/f9a34cab7b05881a/source/youtube?" + "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0" + "&ipbits=0&expire=19000000000&signature=80648A12A7D5FC1FA02B52B4250E4EB74CF0C5FD." - + "66A261130CA137AA5C541EA9CED2DBF240829EE6&key=ik0", DemoUtil.TYPE_DASH), + + "66A261130CA137AA5C541EA9CED2DBF240829EE6&key=ik0", PlayerActivity.TYPE_DASH), }; public static final Sample[] HLS = new Sample[] { new Sample("Apple master playlist", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/" - + "bipbop_4x3_variant.m3u8", DemoUtil.TYPE_HLS), + + "bipbop_4x3_variant.m3u8", PlayerActivity.TYPE_HLS), new Sample("Apple master playlist advanced", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/" - + "bipbop_16x9_variant.m3u8", DemoUtil.TYPE_HLS), + + "bipbop_16x9_variant.m3u8", PlayerActivity.TYPE_HLS), new Sample("Apple TS media playlist", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/" - + "prog_index.m3u8", DemoUtil.TYPE_HLS), + + "prog_index.m3u8", PlayerActivity.TYPE_HLS), new Sample("Apple AAC media playlist", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/" - + "prog_index.m3u8", DemoUtil.TYPE_HLS), + + "prog_index.m3u8", PlayerActivity.TYPE_HLS), new Sample("Apple ID3 metadata", "http://devimages.apple.com/samplecode/adDemo/ad.m3u8", - DemoUtil.TYPE_HLS), + PlayerActivity.TYPE_HLS), }; public static final Sample[] MISC = new Sample[] { new Sample("Dizzy", "http://html5demos.com/assets/dizzy.mp4", - DemoUtil.TYPE_MP4), + PlayerActivity.TYPE_MP4), new Sample("Apple AAC 10s", "https://devimages.apple.com.edgekey.net/" + "streaming/examples/bipbop_4x3/gear0/fileSequence0.aac", - DemoUtil.TYPE_AAC), + PlayerActivity.TYPE_AAC), new Sample("Apple TS 10s", "https://devimages.apple.com.edgekey.net/streaming/examples/" + "bipbop_4x3/gear1/fileSequence0.ts", - DemoUtil.TYPE_TS), + PlayerActivity.TYPE_TS), new Sample("Big Buck Bunny (MP4 Video)", "http://redirector.c.youtube.com/videoplayback?id=604ed5ce52eda7ee&itag=22&source=youtube&" + "sparams=ip,ipbits,expire,source,id&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=" + "513F28C7FDCBEC60A66C86C9A393556C99DC47FB.04C88036EEE12565A1ED864A875A58F15D8B5300" + "&key=ik0", - DemoUtil.TYPE_MP4), + PlayerActivity.TYPE_MP4), new Sample("Google Play (MP3 Audio)", "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3", - DemoUtil.TYPE_MP3), + PlayerActivity.TYPE_MP3), new Sample("Google Glass (WebM Video with Vorbis Audio)", "http://demos.webmproject.org/exoplayer/glass_vp9_vorbis.webm", - DemoUtil.TYPE_WEBM), + PlayerActivity.TYPE_WEBM), }; private Samples() {} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/SmoothStreamingTestMediaDrmCallback.java b/demo/src/main/java/com/google/android/exoplayer/demo/SmoothStreamingTestMediaDrmCallback.java index ec4b0c231d..257b0cac2e 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/SmoothStreamingTestMediaDrmCallback.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/SmoothStreamingTestMediaDrmCallback.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer.demo; import com.google.android.exoplayer.drm.MediaDrmCallback; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; import android.media.MediaDrm.KeyRequest; @@ -48,7 +49,7 @@ public class SmoothStreamingTestMediaDrmCallback implements MediaDrmCallback { @Override public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); - return DemoUtil.executePost(url, null, null); + return Util.executePost(url, null, null); } @Override @@ -57,7 +58,7 @@ public class SmoothStreamingTestMediaDrmCallback implements MediaDrmCallback { if (TextUtils.isEmpty(url)) { url = PLAYREADY_TEST_DEFAULT_URI; } - return DemoUtil.executePost(url, request.getData(), KEY_REQUEST_PROPERTIES); + return Util.executePost(url, request.getData(), KEY_REQUEST_PROPERTIES); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/WidevineTestMediaDrmCallback.java b/demo/src/main/java/com/google/android/exoplayer/demo/WidevineTestMediaDrmCallback.java index 1f29f6b7a1..bffd5fcb1f 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/WidevineTestMediaDrmCallback.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/WidevineTestMediaDrmCallback.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer.demo; import com.google.android.exoplayer.drm.MediaDrmCallback; +import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; import android.media.MediaDrm.KeyRequest; @@ -43,7 +44,7 @@ public class WidevineTestMediaDrmCallback implements MediaDrmCallback { @Override public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); - return DemoUtil.executePost(url, null, null); + return Util.executePost(url, null, null); } @Override @@ -52,7 +53,7 @@ public class WidevineTestMediaDrmCallback implements MediaDrmCallback { if (TextUtils.isEmpty(url)) { url = defaultUri; } - return DemoUtil.executePost(url, request.getData(), null); + return Util.executePost(url, request.getData(), null); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java index b875ac9ab7..149ed96e56 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java @@ -42,9 +42,9 @@ import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver; import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback; import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback; -import com.google.android.exoplayer.drm.DrmSessionManager; import com.google.android.exoplayer.drm.MediaDrmCallback; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.drm.UnsupportedDrmException; import com.google.android.exoplayer.text.TextTrackRenderer; import com.google.android.exoplayer.text.ttml.TtmlParser; import com.google.android.exoplayer.text.webvtt.WebvttParser; @@ -57,14 +57,10 @@ import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.Util; -import android.annotation.TargetApi; import android.content.Context; import android.media.MediaCodec; -import android.media.UnsupportedSchemeException; import android.os.Handler; import android.util.Log; -import android.util.Pair; -import android.widget.TextView; import java.io.IOException; import java.util.ArrayList; @@ -104,7 +100,6 @@ public class DashRendererBuilder implements RendererBuilder, private final String userAgent; private final String url; private final MediaDrmCallback drmCallback; - private final TextView debugTextView; private final AudioCapabilities audioCapabilities; private DemoPlayer player; @@ -116,12 +111,11 @@ public class DashRendererBuilder implements RendererBuilder, private long elapsedRealtimeOffset; public DashRendererBuilder(Context context, String userAgent, String url, - MediaDrmCallback drmCallback, TextView debugTextView, AudioCapabilities audioCapabilities) { + MediaDrmCallback drmCallback, AudioCapabilities audioCapabilities) { this.context = context; this.userAgent = userAgent; this.url = url; this.drmCallback = drmCallback; - this.debugTextView = debugTextView; this.audioCapabilities = audioCapabilities; } @@ -192,20 +186,18 @@ public class DashRendererBuilder implements RendererBuilder, // Check drm support if necessary. boolean filterHdContent = false; - DrmSessionManager drmSessionManager = null; + StreamingDrmSessionManager drmSessionManager = null; if (hasContentProtection) { if (Util.SDK_INT < 18) { callback.onRenderersError( - new UnsupportedDrmException(UnsupportedDrmException.REASON_NO_DRM)); + new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)); return; } try { - Pair drmSessionManagerData = - V18Compat.getDrmSessionManagerData(player, drmCallback); - drmSessionManager = drmSessionManagerData.first; - // HD streams require L1 security. + drmSessionManager = StreamingDrmSessionManager.newWidevineInstance( + player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); filterHdContent = videoAdaptationSet != null && videoAdaptationSet.hasContentProtection() - && !drmSessionManagerData.second; + && getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1; } catch (UnsupportedDrmException e) { callback.onRenderersError(e); return; @@ -226,10 +218,8 @@ public class DashRendererBuilder implements RendererBuilder, // Build the video renderer. final MediaCodecVideoTrackRenderer videoRenderer; - final TrackRenderer debugRenderer; if (videoRepresentationIndices == null || videoRepresentationIndices.length == 0) { videoRenderer = null; - debugRenderer = null; } else { DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, @@ -240,8 +230,6 @@ public class DashRendererBuilder implements RendererBuilder, DemoPlayer.TYPE_VIDEO); videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50); - debugRenderer = debugTextView != null - ? new DebugTrackRenderer(debugTextView, player, videoRenderer, bandwidthMeter) : null; } // Build the audio chunk sources. @@ -353,34 +341,13 @@ public class DashRendererBuilder implements RendererBuilder, renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; renderers[DemoPlayer.TYPE_TEXT] = textRenderer; - renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; - callback.onRenderers(trackNames, multiTrackChunkSources, renderers); + callback.onRenderers(trackNames, multiTrackChunkSources, renderers, bandwidthMeter); } - @TargetApi(18) - private static class V18Compat { - - public static Pair getDrmSessionManagerData(DemoPlayer player, - MediaDrmCallback drmCallback) throws UnsupportedDrmException { - try { - StreamingDrmSessionManager streamingDrmSessionManager = - StreamingDrmSessionManager.newWidevineInstance(player.getPlaybackLooper(), drmCallback, - null, player.getMainHandler(), player); - return Pair.create((DrmSessionManager) streamingDrmSessionManager, - getWidevineSecurityLevel(streamingDrmSessionManager) == SECURITY_LEVEL_1); - } catch (UnsupportedSchemeException e) { - throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME); - } catch (Exception e) { - throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNKNOWN, e); - } - } - - private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) { - String securityLevelProperty = sessionManager.getPropertyString("securityLevel"); - return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty - .equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN; - } - + private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) { + String securityLevelProperty = sessionManager.getPropertyString("securityLevel"); + return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty + .equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN; } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DebugTrackRenderer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DebugTrackRenderer.java deleted file mode 100644 index 7f7f4aefd3..0000000000 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DebugTrackRenderer.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer.demo.player; - -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaCodecTrackRenderer; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.chunk.Format; -import com.google.android.exoplayer.upstream.BandwidthMeter; - -import android.widget.TextView; - -/** - * A {@link TrackRenderer} that periodically updates debugging information displayed by a - * {@link TextView}. - */ -/* package */ class DebugTrackRenderer extends TrackRenderer implements Runnable { - - private final TextView textView; - private final DemoPlayer player; - private final MediaCodecTrackRenderer renderer; - private final BandwidthMeter bandwidthMeter; - - private volatile boolean pendingFailure; - private volatile long currentPositionUs; - - public DebugTrackRenderer(TextView textView, DemoPlayer player, - MediaCodecTrackRenderer renderer) { - this(textView, player, renderer, null); - } - - public DebugTrackRenderer(TextView textView, DemoPlayer player, MediaCodecTrackRenderer renderer, - BandwidthMeter bandwidthMeter) { - this.textView = textView; - this.player = player; - this.renderer = renderer; - this.bandwidthMeter = bandwidthMeter; - } - - public void injectFailure() { - pendingFailure = true; - } - - @Override - protected boolean isEnded() { - return true; - } - - @Override - protected boolean isReady() { - return true; - } - - @Override - protected int doPrepare(long positionUs) throws ExoPlaybackException { - maybeFail(); - return STATE_PREPARED; - } - - @Override - protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - maybeFail(); - if (positionUs < currentPositionUs || positionUs > currentPositionUs + 1000000) { - currentPositionUs = positionUs; - textView.post(this); - } - } - - @Override - public void run() { - textView.setText(getRenderString()); - } - - private String getRenderString() { - return getTimeString() + " " + getQualityString() + " " + getBandwidthString() + " " - + renderer.codecCounters.getDebugString(); - } - - private String getTimeString() { - return "ms(" + (currentPositionUs / 1000) + ")"; - } - - private String getQualityString() { - Format format = player.getVideoFormat(); - return format == null ? "id:? br:? h:?" - : "id:" + format.id + " br:" + format.bitrate + " h:" + format.height; - } - - private String getBandwidthString() { - if (bandwidthMeter == null - || bandwidthMeter.getBitrateEstimate() == BandwidthMeter.NO_ESTIMATE) { - return "bw:?"; - } else { - return "bw:" + (bandwidthMeter.getBitrateEstimate() / 1000); - } - } - - @Override - protected long getCurrentPositionUs() { - return currentPositionUs; - } - - @Override - protected long getDurationUs() { - return TrackRenderer.MATCH_LONGEST_US; - } - - @Override - protected long getBufferedPositionUs() { - return TrackRenderer.END_OF_TRACK_US; - } - - @Override - protected void seekTo(long timeUs) { - currentPositionUs = timeUs; - } - - private void maybeFail() throws ExoPlaybackException { - if (pendingFailure) { - pendingFailure = false; - throw new ExoPlaybackException("fail() was called on DebugTrackRenderer"); - } - } - -} diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index b475915d6a..c7e639699b 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -15,10 +15,12 @@ */ package com.google.android.exoplayer.demo.player; +import com.google.android.exoplayer.CodecCounters; import com.google.android.exoplayer.DummyTrackRenderer; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecTrackRenderer; import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.TimeRange; @@ -29,10 +31,12 @@ import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.MultiTrackChunkSource; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.hls.HlsSampleSource; -import com.google.android.exoplayer.metadata.MetadataTrackRenderer; +import com.google.android.exoplayer.metadata.MetadataTrackRenderer.MetadataRenderer; import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.TextRenderer; +import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.util.DebugTextViewHelper; import com.google.android.exoplayer.util.PlayerControl; import android.media.MediaCodec.CryptoException; @@ -54,7 +58,8 @@ import java.util.concurrent.CopyOnWriteArrayList; public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, - StreamingDrmSessionManager.EventListener, TextRenderer { + StreamingDrmSessionManager.EventListener, TextRenderer, + MetadataRenderer>, DebugTextViewHelper.Provider { /** * Builds renderers for the player. @@ -84,9 +89,10 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi * multiple tracks. An individual element may be null if it does not have multiple tracks. * @param renderers Renderers indexed by {@link DemoPlayer} TYPE_* constants. An individual * element may be null if there do not exist tracks of the corresponding type. + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. May be null. */ void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources, - TrackRenderer[] renderers); + TrackRenderer[] renderers, BandwidthMeter bandwidthMeter); /** * Invoked if a {@link RendererBuilder} encounters an error. * @@ -163,12 +169,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi public static final int DISABLED_TRACK = -1; public static final int PRIMARY_TRACK = 0; - public static final int RENDERER_COUNT = 5; + public static final int RENDERER_COUNT = 4; public static final int TYPE_VIDEO = 0; public static final int TYPE_AUDIO = 1; public static final int TYPE_TEXT = 2; - public static final int TYPE_TIMED_METADATA = 3; - public static final int TYPE_DEBUG = 4; + public static final int TYPE_METADATA = 3; private static final int RENDERER_BUILDING_STATE_IDLE = 1; private static final int RENDERER_BUILDING_STATE_BUILDING = 2; @@ -187,9 +192,11 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi private Surface surface; private InternalRendererBuilderCallback builderCallback; private TrackRenderer videoRenderer; + private CodecCounters codecCounters; private Format videoFormat; private int videoTrackToRestore; + private BandwidthMeter bandwidthMeter; private MultiTrackChunkSource[] multiTrackSources; private String[][] trackNames; private int[] selectedTracks; @@ -275,10 +282,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } } - public Format getVideoFormat() { - return videoFormat; - } - public void setBackgrounded(boolean backgrounded) { if (this.backgrounded == backgrounded) { return; @@ -310,7 +313,8 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } /* package */ void onRenderers(String[][] trackNames, - MultiTrackChunkSource[] multiTrackSources, TrackRenderer[] renderers) { + MultiTrackChunkSource[] multiTrackSources, TrackRenderer[] renderers, + BandwidthMeter bandwidthMeter) { builderCallback = null; // Normalize the results. if (trackNames == null) { @@ -333,7 +337,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi // Complete preparation. this.trackNames = trackNames; this.videoRenderer = renderers[TYPE_VIDEO]; + this.codecCounters = videoRenderer instanceof MediaCodecTrackRenderer + ? ((MediaCodecTrackRenderer) videoRenderer).codecCounters + : renderers[TYPE_AUDIO] instanceof MediaCodecTrackRenderer + ? ((MediaCodecTrackRenderer) renderers[TYPE_AUDIO]).codecCounters : null; this.multiTrackSources = multiTrackSources; + this.bandwidthMeter = bandwidthMeter; pushSurface(false); pushTrackSelection(TYPE_VIDEO, true); pushTrackSelection(TYPE_AUDIO, true); @@ -387,6 +396,22 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi return playerState; } + @Override + public Format getFormat() { + return videoFormat; + } + + @Override + public BandwidthMeter getBandwidthMeter() { + return bandwidthMeter; + } + + @Override + public CodecCounters getCodecCounters() { + return codecCounters; + } + + @Override public long getCurrentPosition() { return player.getCurrentPosition(); } @@ -494,9 +519,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } @Override - public void onDecoderInitialized( - String decoderName, - long elapsedRealtimeMs, + public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, long initializationDurationMs) { if (infoListener != null) { infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs); @@ -512,19 +535,16 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi @Override public void onCues(List cues) { - processCues(cues); + if (captionListener != null && selectedTracks[TYPE_TEXT] != DISABLED_TRACK) { + captionListener.onCues(cues); + } } - /* package */ MetadataTrackRenderer.MetadataRenderer> - getId3MetadataRenderer() { - return new MetadataTrackRenderer.MetadataRenderer>() { - @Override - public void onMetadata(Map metadata) { - if (id3MetadataListener != null) { - id3MetadataListener.onId3Metadata(metadata); - } - } - }; + @Override + public void onMetadata(Map metadata) { + if (id3MetadataListener != null && selectedTracks[TYPE_METADATA] != DISABLED_TRACK) { + id3MetadataListener.onId3Metadata(metadata); + } } @Override @@ -612,13 +632,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } } - /* package */ void processCues(List cues) { - if (captionListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) { - return; - } - captionListener.onCues(cues); - } - private class InternalRendererBuilderCallback implements RendererBuilderCallback { private boolean canceled; @@ -629,9 +642,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi @Override public void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources, - TrackRenderer[] renderers) { + TrackRenderer[] renderers, BandwidthMeter bandwidthMeter) { if (!canceled) { - DemoPlayer.this.onRenderers(trackNames, multiTrackSources, renderers); + DemoPlayer.this.onRenderers(trackNames, multiTrackSources, renderers, bandwidthMeter); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java index d2c0f8bee9..6cab32227d 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java @@ -29,7 +29,6 @@ import com.google.android.exoplayer.upstream.DefaultUriDataSource; import android.content.Context; import android.media.MediaCodec; import android.net.Uri; -import android.widget.TextView; /** * A {@link RendererBuilder} for streams that can be read using an {@link Extractor}. @@ -41,15 +40,12 @@ public class ExtractorRendererBuilder implements RendererBuilder { private final Context context; private final String userAgent; private final Uri uri; - private final TextView debugTextView; private final Extractor extractor; - public ExtractorRendererBuilder(Context context, String userAgent, Uri uri, - TextView debugTextView, Extractor extractor) { + public ExtractorRendererBuilder(Context context, String userAgent, Uri uri, Extractor extractor) { this.context = context; this.userAgent = userAgent; this.uri = uri; - this.debugTextView = debugTextView; this.extractor = extractor; } @@ -67,16 +63,11 @@ public class ExtractorRendererBuilder implements RendererBuilder { MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource, null, true, player.getMainHandler(), player); - // Build the debug renderer. - TrackRenderer debugRenderer = debugTextView != null - ? new DebugTrackRenderer(debugTextView, player, videoRenderer, bandwidthMeter) : null; - // Invoke the callback. TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; - renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; - callback.onRenderers(null, null, renderers); + callback.onRenderers(null, null, renderers, bandwidthMeter); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java index 39b8393059..115f42c1da 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java @@ -43,7 +43,6 @@ import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import android.content.Context; import android.media.MediaCodec; import android.os.Handler; -import android.widget.TextView; import java.io.IOException; import java.util.Map; @@ -59,18 +58,16 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback> id3Renderer = - new MetadataTrackRenderer<>(sampleSource, new Id3Parser(), - player.getId3MetadataRenderer(), mainHandler.getLooper()); + new MetadataTrackRenderer<>(sampleSource, new Id3Parser(), player, mainHandler.getLooper()); Eia608TrackRenderer closedCaptionRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper()); - // Build the debug renderer. - TrackRenderer debugRenderer = debugTextView != null - ? new DebugTrackRenderer(debugTextView, player, videoRenderer, bandwidthMeter) : null; - TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; - renderers[DemoPlayer.TYPE_TIMED_METADATA] = id3Renderer; + renderers[DemoPlayer.TYPE_METADATA] = id3Renderer; renderers[DemoPlayer.TYPE_TEXT] = closedCaptionRenderer; - renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; - callback.onRenderers(null, null, renderers); + callback.onRenderers(null, null, renderers, bandwidthMeter); } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java index 9aa80915e4..faea15473a 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallba import com.google.android.exoplayer.drm.DrmSessionManager; import com.google.android.exoplayer.drm.MediaDrmCallback; import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.drm.UnsupportedDrmException; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; @@ -46,16 +47,12 @@ import com.google.android.exoplayer.upstream.DefaultUriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.Util; -import android.annotation.TargetApi; import android.content.Context; import android.media.MediaCodec; -import android.media.UnsupportedSchemeException; import android.os.Handler; -import android.widget.TextView; import java.io.IOException; import java.util.Arrays; -import java.util.UUID; /** * A {@link RendererBuilder} for SmoothStreaming. @@ -73,19 +70,17 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, private final String userAgent; private final String url; private final MediaDrmCallback drmCallback; - private final TextView debugTextView; private DemoPlayer player; private RendererBuilderCallback callback; private ManifestFetcher manifestFetcher; public SmoothStreamingRendererBuilder(Context context, String userAgent, String url, - MediaDrmCallback drmCallback, TextView debugTextView) { + MediaDrmCallback drmCallback) { this.context = context; this.userAgent = userAgent; this.url = url; this.drmCallback = drmCallback; - this.debugTextView = debugTextView; } @Override @@ -118,12 +113,12 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, if (manifest.protectionElement != null) { if (Util.SDK_INT < 18) { callback.onRenderersError( - new UnsupportedDrmException(UnsupportedDrmException.REASON_NO_DRM)); + new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME)); return; } try { - drmSessionManager = V18Compat.getDrmSessionManager(manifest.protectionElement.uuid, player, - drmCallback); + drmSessionManager = new StreamingDrmSessionManager(manifest.protectionElement.uuid, + player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player); } catch (UnsupportedDrmException e) { callback.onRenderersError(e); return; @@ -159,10 +154,8 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, // Build the video renderer. final MediaCodecVideoTrackRenderer videoRenderer; - final TrackRenderer debugRenderer; if (videoTrackIndices == null || videoTrackIndices.length == 0) { videoRenderer = null; - debugRenderer = null; } else { DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher, @@ -173,8 +166,6 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, DemoPlayer.TYPE_VIDEO); videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50); - debugRenderer = debugTextView != null - ? new DebugTrackRenderer(debugTextView, player, videoRenderer, bandwidthMeter) : null; } // Build the audio renderer. @@ -252,25 +243,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer; renderers[DemoPlayer.TYPE_TEXT] = textRenderer; - renderers[DemoPlayer.TYPE_DEBUG] = debugRenderer; - callback.onRenderers(trackNames, multiTrackChunkSources, renderers); - } - - @TargetApi(18) - private static class V18Compat { - - public static DrmSessionManager getDrmSessionManager(UUID uuid, DemoPlayer player, - MediaDrmCallback drmCallback) throws UnsupportedDrmException { - try { - return new StreamingDrmSessionManager(uuid, player.getPlaybackLooper(), drmCallback, null, - player.getMainHandler(), player); - } catch (UnsupportedSchemeException e) { - throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME); - } catch (Exception e) { - throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNKNOWN, e); - } - } - + callback.onRenderers(trackNames, multiTrackChunkSources, renderers, bandwidthMeter); } } diff --git a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java index 3705e5bb25..2ee73b05e6 100644 --- a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java @@ -110,11 +110,11 @@ public class StreamingDrmSessionManager implements DrmSessionManager { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @throws UnsupportedSchemeException If the specified DRM scheme is not supported. + * @throws UnsupportedDrmException If the specified DRM scheme is not supported. */ public static StreamingDrmSessionManager newWidevineInstance(Looper playbackLooper, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, - Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException { + Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { return new StreamingDrmSessionManager(WIDEVINE_UUID, playbackLooper, callback, optionalKeyRequestParameters, eventHandler, eventListener); } @@ -132,11 +132,11 @@ public class StreamingDrmSessionManager implements DrmSessionManager { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @throws UnsupportedSchemeException If the specified DRM scheme is not supported. + * @throws UnsupportedDrmException If the specified DRM scheme is not supported. */ public static StreamingDrmSessionManager newPlayReadyInstance(Looper playbackLooper, MediaDrmCallback callback, String customData, Handler eventHandler, - EventListener eventListener) throws UnsupportedSchemeException { + EventListener eventListener) throws UnsupportedDrmException { HashMap optionalKeyRequestParameters; if (!TextUtils.isEmpty(customData)) { optionalKeyRequestParameters = new HashMap<>(); @@ -158,17 +158,23 @@ public class StreamingDrmSessionManager implements DrmSessionManager { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @throws UnsupportedSchemeException If the specified DRM scheme is not supported. + * @throws UnsupportedDrmException If the specified DRM scheme is not supported. */ public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, - EventListener eventListener) throws UnsupportedSchemeException { + EventListener eventListener) throws UnsupportedDrmException { this.uuid = uuid; this.callback = callback; this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.eventHandler = eventHandler; this.eventListener = eventListener; - mediaDrm = new MediaDrm(uuid); + try { + mediaDrm = new MediaDrm(uuid); + } catch (UnsupportedSchemeException e) { + throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e); + } catch (Exception e) { + throw new UnsupportedDrmException(UnsupportedDrmException.REASON_INSTANTIATION_ERROR, e); + } mediaDrm.setOnEventListener(new MediaDrmEventListener()); mediaDrmHandler = new MediaDrmHandler(playbackLooper); postResponseHandler = new PostResponseHandler(playbackLooper); @@ -176,12 +182,12 @@ public class StreamingDrmSessionManager implements DrmSessionManager { } @Override - public int getState() { + public final int getState() { return state; } @Override - public MediaCrypto getMediaCrypto() { + public final MediaCrypto getMediaCrypto() { if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { throw new IllegalStateException(); } @@ -197,7 +203,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { } @Override - public Exception getError() { + public final Exception getError() { return state == STATE_ERROR ? lastException : null; } @@ -250,7 +256,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { } @Override - public void open(DrmInitData drmInitData) { + public final void open(DrmInitData drmInitData) { if (++openCount != 1) { return; } @@ -272,7 +278,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { } @Override - public void close() { + public final void close() { if (--openCount != 0) { return; } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/UnsupportedDrmException.java b/library/src/main/java/com/google/android/exoplayer/drm/UnsupportedDrmException.java similarity index 70% rename from demo/src/main/java/com/google/android/exoplayer/demo/player/UnsupportedDrmException.java rename to library/src/main/java/com/google/android/exoplayer/drm/UnsupportedDrmException.java index e71e1b04d3..b5f2d12211 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/UnsupportedDrmException.java +++ b/library/src/main/java/com/google/android/exoplayer/drm/UnsupportedDrmException.java @@ -13,16 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.demo.player; +package com.google.android.exoplayer.drm; /** - * Exception thrown when the required level of DRM is not supported. + * Thrown when the requested DRM scheme is not supported. */ public final class UnsupportedDrmException extends Exception { - public static final int REASON_NO_DRM = 0; + /** + * The requested DRM scheme is unsupported by the device. + */ public static final int REASON_UNSUPPORTED_SCHEME = 1; - public static final int REASON_UNKNOWN = 2; + /** + * There device advertises support for the requested DRM scheme, but there was an error + * instantiating it. The cause can be retrieved using {@link #getCause()}. + */ + public static final int REASON_INSTANTIATION_ERROR = 2; public final int reason; diff --git a/library/src/main/java/com/google/android/exoplayer/util/DebugTextViewHelper.java b/library/src/main/java/com/google/android/exoplayer/util/DebugTextViewHelper.java new file mode 100644 index 0000000000..134b40e744 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/DebugTextViewHelper.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.util; + +import com.google.android.exoplayer.CodecCounters; +import com.google.android.exoplayer.chunk.Format; +import com.google.android.exoplayer.upstream.BandwidthMeter; + +import android.widget.TextView; + +/** + * A helper class for periodically updating debug information displayed by a {@link TextView}. + */ +public final class DebugTextViewHelper implements Runnable { + + /** + * Provides debug information about an ongoing playback. + */ + public interface Provider { + + /** + * Returns the current playback position, in milliseconds. + */ + long getCurrentPosition(); + + /** + * Returns a format whose information should be displayed, or null. + */ + Format getFormat(); + + /** + * Returns a {@link BandwidthMeter} whose estimate should be displayed, or null. + */ + BandwidthMeter getBandwidthMeter(); + + /** + * Returns a {@link CodecCounters} whose information should be displayed, or null. + */ + CodecCounters getCodecCounters(); + + } + + private static final int REFRESH_INTERVAL_MS = 1000; + + private final TextView textView; + private final Provider debuggable; + + /** + * @param debuggable The {@link Provider} from which debug information should be obtained. + * @param textView The {@link TextView} that should be updated to display the information. + */ + public DebugTextViewHelper(Provider debuggable, TextView textView) { + this.debuggable = debuggable; + this.textView = textView; + } + + /** + * Starts periodic updates of the {@link TextView}. + *

+ * Should be called from the application's main thread. + */ + public void start() { + stop(); + run(); + } + + /** + * Stops periodic updates of the {@link TextView}. + *

+ * Should be called from the application's main thread. + */ + public void stop() { + textView.removeCallbacks(this); + } + + @Override + public void run() { + textView.setText(getRenderString()); + textView.postDelayed(this, REFRESH_INTERVAL_MS); + } + + private String getRenderString() { + return getTimeString() + " " + getQualityString() + " " + getBandwidthString() + " " + + getVideoCodecCountersString(); + } + + private String getTimeString() { + return "ms(" + debuggable.getCurrentPosition() + ")"; + } + + private String getQualityString() { + Format format = debuggable.getFormat(); + return format == null ? "id:? br:? h:?" + : "id:" + format.id + " br:" + format.bitrate + " h:" + format.height; + } + + private String getBandwidthString() { + BandwidthMeter bandwidthMeter = debuggable.getBandwidthMeter(); + if (bandwidthMeter == null + || bandwidthMeter.getBitrateEstimate() == BandwidthMeter.NO_ESTIMATE) { + return "bw:?"; + } else { + return "bw:" + (bandwidthMeter.getBitrateEstimate() / 1000); + } + } + + private String getVideoCodecCountersString() { + CodecCounters codecCounters = debuggable.getCodecCounters(); + return codecCounters == null ? "" : codecCounters.getDebugString(); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 5dc2993ef6..4bfab023e5 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -26,6 +26,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.text.TextUtils; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -40,6 +41,7 @@ import java.util.Collections; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.TimeZone; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -583,4 +585,56 @@ public final class Util { + ") " + "ExoPlayerLib/" + ExoPlayerLibraryInfo.VERSION; } + /** + * Executes a post request using {@link HttpURLConnection}. + * + * @param url The request URL. + * @param data The request body, or null. + * @param requestProperties Request properties, or null. + * @return The response body. + * @throws IOException If an error occurred making the request. + */ + // TODO: Remove this and use HttpDataSource once DataSpec supports inclusion of a POST body. + public static byte[] executePost(String url, byte[] data, Map requestProperties) + throws IOException { + HttpURLConnection urlConnection = null; + try { + urlConnection = (HttpURLConnection) new URL(url).openConnection(); + urlConnection.setRequestMethod("POST"); + urlConnection.setDoOutput(data != null); + urlConnection.setDoInput(true); + if (requestProperties != null) { + for (Map.Entry requestProperty : requestProperties.entrySet()) { + urlConnection.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); + } + } + // Write the request body, if there is one. + if (data != null) { + OutputStream out = urlConnection.getOutputStream(); + try { + out.write(data); + } finally { + out.close(); + } + } + // Read and return the response body. + InputStream inputStream = urlConnection.getInputStream(); + try { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + byte scratch[] = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(scratch)) != -1) { + byteArrayOutputStream.write(scratch, 0, bytesRead); + } + return byteArrayOutputStream.toByteArray(); + } finally { + inputStream.close(); + } + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } + } From cb1e6ad41822deb0ba38b33f35ded760b8b7dd3d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 28 May 2015 17:19:30 +0100 Subject: [PATCH 7/7] Tweak project configuration --- library/build.gradle | 2 +- library/src/test/.project | 2 +- library/src/test/project.properties | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/library/build.gradle b/library/build.gradle index a6c2fa6414..dd741703ba 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -72,7 +72,7 @@ android.libraryVariants.all { variant -> classpath = files(variant.javaCompile.classpath.files, project.android.getBootClasspath()) options { links "http://docs.oracle.com/javase/7/docs/api/" - linksOffline "https://d.android.com/reference","${android.sdkDirectory}/docs/reference" + linksOffline "https://developer.android.com/reference","${android.sdkDirectory}/docs/reference" } exclude '**/BuildConfig.java' exclude '**/R.java' diff --git a/library/src/test/.project b/library/src/test/.project index d63886b065..888dce8656 100644 --- a/library/src/test/.project +++ b/library/src/test/.project @@ -1,6 +1,6 @@ - ExoPlayerTests + ExoPlayerLibTests ExoPlayerLib diff --git a/library/src/test/project.properties b/library/src/test/project.properties index ef6eb035b6..0031332ec9 100644 --- a/library/src/test/project.properties +++ b/library/src/test/project.properties @@ -12,4 +12,5 @@ # Project target. target=android-22 +android.library=false android.library.reference.1=../main