diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ce376bfc07..234c91daba 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,9 +1,69 @@ # Release notes # -### r2.1.1 ### +### r2.2.0 ### -Bugfix release only. Users of r2.1.0 and r2.0.x should proactively update to -this version. +* Demo app: Automatic recovery from BehindLiveWindowException, plus improved + handling of pausing and resuming live streams + ([#2344](https://github.com/google/ExoPlayer/issues/2344)). +* AndroidTV: Added Support for tunneled video playback + ([#1688](https://github.com/google/ExoPlayer/issues/1688)). +* DRM: Renamed StreamingDrmSessionManager to DefaultDrmSessionManager and + added support for using offline licenses + ([#876](https://github.com/google/ExoPlayer/issues/876)). +* DRM: Introduce OfflineLicenseHelper to help with offline license acquisition, + renewal and release. +* UI: Updated player control assets. Added vector drawables for use on API level + 21 and above. +* UI: Made player control seek bar work correctly with key events if focusable + ([#2278](https://github.com/google/ExoPlayer/issues/2278)). +* HLS: Improved support for streams that use EXT-X-DISCONTINUITY without + EXT-X-DISCONTINUITY-SEQUENCE + ([#1789](https://github.com/google/ExoPlayer/issues/1789)). +* HLS: Support for EXT-X-START tag + ([#1544](https://github.com/google/ExoPlayer/issues/1544)). +* HLS: Check #EXTM3U header is present when parsing the playlist. Fail + gracefully if not ([#2301](https://github.com/google/ExoPlayer/issues/2301)). +* HLS: Fix memory leak + ([#2319](https://github.com/google/ExoPlayer/issues/2319)). +* HLS: Fix non-seamless first adaptation where master playlist omits resolution + tags ([#2096](https://github.com/google/ExoPlayer/issues/2096)). +* HLS: Fix handling of WebVTT subtitle renditions with non-standard segment file + extensions ([#2025](https://github.com/google/ExoPlayer/issues/2025) and + [#2355](https://github.com/google/ExoPlayer/issues/2355)). +* HLS: Better handle inconsistent HLS playlist update + ([#2249](https://github.com/google/ExoPlayer/issues/2249)). +* DASH: Don't overflow when dealing with large segment numbers + ([#2311](https://github.com/google/ExoPlayer/issues/2311)). +* DASH: Fix propagation of language from the manifest + ([#2335](https://github.com/google/ExoPlayer/issues/2335)). +* SmoothStreaming: Work around "Offset to sample data was negative" failures + ([#2292](https://github.com/google/ExoPlayer/issues/2292), + [#2101](https://github.com/google/ExoPlayer/issues/2101) and + [#1152](https://github.com/google/ExoPlayer/issues/1152)). +* MP3/ID3: Added support for parsing Chapter and URL link frames + ([#2316](https://github.com/google/ExoPlayer/issues/2316)). +* MP3/ID3: Handle ID3 frames that end with empty text field + ([#2309](https://github.com/google/ExoPlayer/issues/2309)). +* Added ClippingMediaSource for playing clipped portions of media + ([#1988](https://github.com/google/ExoPlayer/issues/1988)). +* Added convenience methods to query whether the current window is dynamic and + seekable ([#2320](https://github.com/google/ExoPlayer/issues/2320)). +* Support setting of default headers on HttpDataSource.Factory implementations + ([#2166](https://github.com/google/ExoPlayer/issues/2166)). +* Fixed cache failures when using an encrypted cache content index. +* Fix visual artifacts when switching output surface + ([#2093](https://github.com/google/ExoPlayer/issues/2093)). +* Fix gradle + proguard configurations. +* Fix player position when replacing the MediaSource + ([#2369](https://github.com/google/ExoPlayer/issues/2369)). +* Misc bug fixes, including + [#2330](https://github.com/google/ExoPlayer/issues/2330), + [#2269](https://github.com/google/ExoPlayer/issues/2269), + [#2252](https://github.com/google/ExoPlayer/issues/2252), + [#2264](https://github.com/google/ExoPlayer/issues/2264) and + [#2290](https://github.com/google/ExoPlayer/issues/2290). + +### r2.1.1 ### * Fix some subtitle types (e.g. WebVTT) being displayed out of sync ([#2208](https://github.com/google/ExoPlayer/issues/2208)). @@ -52,9 +112,9 @@ this version. * Improved flexibility of SimpleExoPlayer ([#2102](https://github.com/google/ExoPlayer/issues/2102)). * Fix issue where only the audio of a video would play due to capability - detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007)) - ([#2034](https://github.com/google/ExoPlayer/issues/2034)) - ([#2157](https://github.com/google/ExoPlayer/issues/2157)). + detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007), + [#2034](https://github.com/google/ExoPlayer/issues/2034) and + [#2157](https://github.com/google/ExoPlayer/issues/2157)). * Fix issues that could cause ExtractorMediaSource based playbacks to get stuck buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)). * Correctly set SimpleExoPlayerView surface aspect ratio when an active player @@ -186,6 +246,14 @@ in all V2 releases. This cannot be assumed for changes in r1.5.12 and later, however it can be assumed that all such changes are included in the most recent V2 release. +### r1.5.14 ### + +* Fixed cache failures when using an encrypted cache content index. +* SmoothStreaming: Work around "Offset to sample data was negative" failures + ([#2292](https://github.com/google/ExoPlayer/issues/2292), + [#2101](https://github.com/google/ExoPlayer/issues/2101) and + [#1152](https://github.com/google/ExoPlayer/issues/1152)). + ### r1.5.13 ### * Improvements to the upstream cache package. diff --git a/build.gradle b/build.gradle index 5586944d70..3f4bab597a 100644 --- a/build.gradle +++ b/build.gradle @@ -29,13 +29,13 @@ allprojects { jcenter() } project.ext { - compileSdkVersion=24 - targetSdkVersion=24 - buildToolsVersion='23.0.3' + compileSdkVersion=25 + targetSdkVersion=25 + buildToolsVersion='25' releaseRepoName = 'exoplayer' releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.1.1' + releaseVersion = 'r2.2.0' releaseWebsite = 'https://github.com/google/ExoPlayer' } } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 4c6d832211..2f3dc0d1bf 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2200" + android:versionName="2.2.0"> @@ -27,7 +27,7 @@ - - ExoPlayer2 Demo + ExoPlayer Video diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index d7c5e21fcc..0d669f826d 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -63,6 +63,7 @@ git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ --enable-decoder=vorbis \ --enable-decoder=opus \ --enable-decoder=flac \ + --enable-decoder=alac \ && \ make -j4 && \ make install-libs diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 0aac601045..6c3ece68a2 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -19,8 +19,8 @@ import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -43,21 +43,12 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { * @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 FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); - } - - /** - * @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. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, bufferProcessors); } @Override diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 12f4bcf672..2af2101ee7 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; import java.nio.ByteBuffer; import java.util.List; @@ -88,6 +89,13 @@ import java.util.List; if (!hasOutputFormat) { channelCount = ffmpegGetChannelCount(nativeContext); sampleRate = ffmpegGetSampleRate(nativeContext); + if (sampleRate == 0 && "alac".equals(codecName)) { + // ALAC decoder did not set the sample rate in earlier versions of FFMPEG. + // See https://trac.ffmpeg.org/ticket/6096 + ParsableByteArray parsableExtraData = new ParsableByteArray(extraData); + parsableExtraData.setPosition(extraData.length - 4); + sampleRate = parsableExtraData.readUnsignedIntToInt(); + } hasOutputFormat = true; } outputBuffer.data.position(0); @@ -123,6 +131,7 @@ import java.util.List; private static byte[] getExtraData(String mimeType, List initializationData) { switch (mimeType) { case MimeTypes.AUDIO_AAC: + case MimeTypes.AUDIO_ALAC: case MimeTypes.AUDIO_OPUS: return initializationData.get(0); case MimeTypes.AUDIO_VORBIS: diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index 90b42c01bb..4992bcbb3e 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -92,6 +92,8 @@ public final class FfmpegLibrary { return "amrwb"; case MimeTypes.AUDIO_FLAC: return "flac"; + case MimeTypes.AUDIO_ALAC: + return "alac"; default: return null; } diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt index 8e7f5e17d5..ee0a9fa5b5 100644 --- a/extensions/flac/proguard-rules.txt +++ b/extensions/flac/proguard-rules.txt @@ -5,7 +5,10 @@ native ; } -# Some members of this class are being accessed from native methods. Keep them unobfuscated. +# Some members of these classes are being accessed from native methods. Keep them unobfuscated. -keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni { *; } +-keep class com.google.android.exoplayer2.util.FlacStreamInfo { + *; +} diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 42c5908619..d13194793e 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -67,7 +67,7 @@ public final class FlacExtractor implements Extractor { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = extractorOutput.track(0); + trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); extractorOutput.endTracks(); try { decoderJni = new FlacDecoderJni(); diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index eb7206c9cf..5efaf98512 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.flac; import android.os.Handler; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -38,21 +38,12 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { * @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 LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); - } - - /** - * @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. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, bufferProcessors); } @Override diff --git a/extensions/flac/src/main/jni/Android.mk b/extensions/flac/src/main/jni/Android.mk index e009333633..ff54c1b3c0 100644 --- a/extensions/flac/src/main/jni/Android.mk +++ b/extensions/flac/src/main/jni/Android.mk @@ -31,7 +31,7 @@ LOCAL_C_INCLUDES := \ LOCAL_SRC_FILES := $(FLAC_SOURCES) LOCAL_CFLAGS += '-DVERSION="1.3.1"' -DFLAC__NO_MD5 -DFLAC__INTEGER_ONLY_LIBRARY -DFLAC__NO_ASM -LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC +LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC -DHAVE_SYS_PARAM_H LOCAL_CFLAGS += -O3 -funroll-loops -finline-functions LOCAL_LDLIBS := -llog -lz -lm diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 7d22c7fe79..e4925cb462 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -453,7 +453,8 @@ int64_t FLACParser::getSeekPosition(int64_t timeUs) { } FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points; - for (unsigned i = mSeekTable->num_points - 1; i >= 0; i--) { + for (unsigned i = mSeekTable->num_points; i > 0; ) { + i--; if (points[i].sample_number <= sample) { return firstFrameOffset + points[i].stream_offset; } diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 1850e68229..f31f80f518 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.opus; import android.os.Handler; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -40,35 +40,26 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @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. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ - public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); + public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, bufferProcessors); } /** * @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. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. */ public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); - } - - /** - * @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. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. - */ - public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { - super(eventHandler, eventListener, audioCapabilities, drmSessionManager, - playClearSamplesWithoutKeys); + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys, + bufferProcessors); } @Override diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index 6d0deb44ae..83e461d279 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -213,7 +213,7 @@ import java.util.List; SimpleOutputBuffer outputBuffer, int sampleRate); private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate, - ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv, + ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); private native void opusClose(long decoder); private native void opusReset(long decoder); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 0d7547d125..73ec7c2f96 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -141,7 +141,7 @@ import java.nio.ByteBuffer; private native long vpxClose(long context); private native long vpxDecode(long context, ByteBuffer encoded, int length); private native long vpxSecureDecode(long context, ByteBuffer encoded, int length, - ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv, + ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer); private native int vpxGetErrorCode(long context); diff --git a/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 b/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 new file mode 100644 index 0000000000..16907fdd98 Binary files /dev/null and b/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 differ diff --git a/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump new file mode 100644 index 0000000000..9d3755b23b --- /dev/null +++ b/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -0,0 +1,382 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = 0 +numberOfTracks = 3 +track 0: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = video/avc + maxInputSize = -1 + width = 1080 + height = 720 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = -1 + encoderPadding = -1 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample count = 30 + sample 0: + time = 66000 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 199000 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 132000 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100000 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166000 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 332000 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266000 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233000 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 299000 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 466000 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 399000 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367000 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433000 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 599000 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533000 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500000 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 566000 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 733000 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 666000 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633000 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700000 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 866000 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800000 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767000 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 833000 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1000000 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 933000 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900000 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967000 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1033000 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + format: + bitrate = -1 + id = 2 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = -1 + pixelWidthHeightRatio = -1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = -1 + encoderPadding = -1 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = und + drmInitData = - + initializationData: + data = length 5, hash 2B7623A + sample count = 46 + sample 0: + time = 0 + flags = 1 + data = length 18, hash 96519432 + sample 1: + time = 23000 + flags = 1 + data = length 4, hash EE9DF + sample 2: + time = 46000 + flags = 1 + data = length 4, hash EEDBF + sample 3: + time = 69000 + flags = 1 + data = length 157, hash E2F078F4 + sample 4: + time = 92000 + flags = 1 + data = length 371, hash B9471F94 + sample 5: + time = 116000 + flags = 1 + data = length 373, hash 2AB265CB + sample 6: + time = 139000 + flags = 1 + data = length 402, hash 1295477C + sample 7: + time = 162000 + flags = 1 + data = length 455, hash 2D8146C8 + sample 8: + time = 185000 + flags = 1 + data = length 434, hash F2C5D287 + sample 9: + time = 208000 + flags = 1 + data = length 450, hash 84143FCD + sample 10: + time = 232000 + flags = 1 + data = length 429, hash EF769D50 + sample 11: + time = 255000 + flags = 1 + data = length 450, hash EC3DE692 + sample 12: + time = 278000 + flags = 1 + data = length 447, hash 3E519E13 + sample 13: + time = 301000 + flags = 1 + data = length 457, hash 1E4F23A0 + sample 14: + time = 325000 + flags = 1 + data = length 447, hash A439EA97 + sample 15: + time = 348000 + flags = 1 + data = length 456, hash 1E9034C6 + sample 16: + time = 371000 + flags = 1 + data = length 398, hash 99DB7345 + sample 17: + time = 394000 + flags = 1 + data = length 474, hash 3F05F10A + sample 18: + time = 417000 + flags = 1 + data = length 416, hash C105EE09 + sample 19: + time = 441000 + flags = 1 + data = length 454, hash 5FDBE458 + sample 20: + time = 464000 + flags = 1 + data = length 438, hash 41A93AC3 + sample 21: + time = 487000 + flags = 1 + data = length 443, hash 10FDA652 + sample 22: + time = 510000 + flags = 1 + data = length 412, hash 1F791E25 + sample 23: + time = 534000 + flags = 1 + data = length 482, hash A6D983D + sample 24: + time = 557000 + flags = 1 + data = length 386, hash BED7392F + sample 25: + time = 580000 + flags = 1 + data = length 463, hash 5309F8C9 + sample 26: + time = 603000 + flags = 1 + data = length 394, hash 21C7321F + sample 27: + time = 626000 + flags = 1 + data = length 489, hash 71B4730D + sample 28: + time = 650000 + flags = 1 + data = length 403, hash D9C6DE89 + sample 29: + time = 673000 + flags = 1 + data = length 447, hash 9B14B73B + sample 30: + time = 696000 + flags = 1 + data = length 439, hash 4760D35B + sample 31: + time = 719000 + flags = 1 + data = length 463, hash 1601F88D + sample 32: + time = 743000 + flags = 1 + data = length 423, hash D4AE6773 + sample 33: + time = 766000 + flags = 1 + data = length 497, hash A3C674D3 + sample 34: + time = 789000 + flags = 1 + data = length 419, hash D3734A1F + sample 35: + time = 812000 + flags = 1 + data = length 474, hash DFB41F9 + sample 36: + time = 835000 + flags = 1 + data = length 413, hash 53E7CB9F + sample 37: + time = 859000 + flags = 1 + data = length 445, hash D15B0E39 + sample 38: + time = 882000 + flags = 1 + data = length 453, hash 77ED81E4 + sample 39: + time = 905000 + flags = 1 + data = length 545, hash 3321AEB9 + sample 40: + time = 928000 + flags = 1 + data = length 317, hash F557D0E + sample 41: + time = 952000 + flags = 1 + data = length 537, hash ED58CF7B + sample 42: + time = 975000 + flags = 1 + data = length 458, hash 51CDAA10 + sample 43: + time = 998000 + flags = 1 + data = length 465, hash CBA1EFD7 + sample 44: + time = 1021000 + flags = 1 + data = length 446, hash D6735B8A + sample 45: + time = 1044000 + flags = 1 + data = length 10, hash A453EEBE +track 3: + format: + bitrate = -1 + id = null + containerMimeType = null + sampleMimeType = application/cea-608 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = -1 + pixelWidthHeightRatio = -1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = -1 + encoderPadding = -1 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + initializationData: + sample count = 0 +tracksEnded = true diff --git a/library/src/androidTest/assets/ts/sample.ac3.0.dump b/library/src/androidTest/assets/ts/sample.ac3.0.dump index c5f241950b..1b6c77efb6 100644 --- a/library/src/androidTest/assets/ts/sample.ac3.0.dump +++ b/library/src/androidTest/assets/ts/sample.ac3.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 1 track 0: format: bitrate = -1 - id = null + id = 0 containerMimeType = null sampleMimeType = audio/ac3 maxInputSize = -1 diff --git a/library/src/androidTest/assets/ts/sample.adts.0.dump b/library/src/androidTest/assets/ts/sample.adts.0.dump index 3325abcfeb..0a7427d3f1 100644 --- a/library/src/androidTest/assets/ts/sample.adts.0.dump +++ b/library/src/androidTest/assets/ts/sample.adts.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 2 track 0: format: bitrate = -1 - id = null + id = 0 containerMimeType = null sampleMimeType = audio/mp4a-latm maxInputSize = -1 @@ -606,7 +606,7 @@ track 0: track 1: format: bitrate = -1 - id = null + id = 1 containerMimeType = null sampleMimeType = application/id3 maxInputSize = -1 diff --git a/library/src/androidTest/assets/ts/sample.ps.0.dump b/library/src/androidTest/assets/ts/sample.ps.0.dump index 48127ce1c6..3b44fb6fb9 100644 --- a/library/src/androidTest/assets/ts/sample.ps.0.dump +++ b/library/src/androidTest/assets/ts/sample.ps.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 2 track 192: format: bitrate = -1 - id = null + id = 192 containerMimeType = null sampleMimeType = audio/mpeg-L2 maxInputSize = 4096 @@ -45,7 +45,7 @@ track 192: track 224: format: bitrate = -1 - id = null + id = 224 containerMimeType = null sampleMimeType = video/mpeg2 maxInputSize = -1 diff --git a/library/src/androidTest/assets/ts/sample.ts.0.dump b/library/src/androidTest/assets/ts/sample.ts.0.dump index 8b0da7bd02..26c6665aaa 100644 --- a/library/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/src/androidTest/assets/ts/sample.ts.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 2 track 256: format: bitrate = -1 - id = null + id = 1/256 containerMimeType = null sampleMimeType = video/mpeg2 maxInputSize = -1 @@ -38,7 +38,7 @@ track 256: track 257: format: bitrate = -1 - id = null + id = 1/257 containerMimeType = null sampleMimeType = audio/mpeg-L2 maxInputSize = 4096 diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index c7ebb22d9a..985e93404a 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.MimeTypes; import java.util.Arrays; import java.util.HashMap; import org.mockito.Mock; @@ -213,11 +214,15 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { } private static AdaptationSet newAdaptationSets(Representation... representations) { - return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations)); + return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null); } private static Representation newRepresentations(DrmInitData drmInitData) { - Format format = Format.createVideoSampleFormat("", "", "", 0, 0, 0, 0, 0, null, drmInitData); + Format format = Format.createVideoContainerFormat("id", MimeTypes.VIDEO_MP4, + MimeTypes.VIDEO_H264, "", Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, 0); + if (drmInitData != null) { + format = format.copyWithDrmInitData(drmInitData); + } return Representation.newInstance("", 0, format, "", new SingleSegmentBase()); } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index 9a8a1f7f27..95ad8b446e 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -25,21 +25,32 @@ import com.google.android.exoplayer2.testutil.TestUtil; */ public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { - private static final TestUtil.ExtractorFactory EXTRACTOR_FACTORY = - new TestUtil.ExtractorFactory() { - @Override - public Extractor create() { - return new FragmentedMp4Extractor(); - } - }; - public void testSample() throws Exception { - TestUtil.assertOutput(EXTRACTOR_FACTORY, "mp4/sample_fragmented.mp4", getInstrumentation()); + TestUtil.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation()); + } + + public void testSampleWithSeiPayloadParsing() throws Exception { + // Enabling the CEA-608 track enables SEI payload parsing. + TestUtil.assertOutput(getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), + "mp4/sample_fragmented_sei.mp4", getInstrumentation()); } public void testAtomWithZeroSize() throws Exception { - TestUtil.assertThrows(EXTRACTOR_FACTORY, "mp4/sample_fragmented_zero_size_atom.mp4", + TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4", getInstrumentation(), ParserException.class); } + private static TestUtil.ExtractorFactory getExtractorFactory() { + return getExtractorFactory(0); + } + + private static TestUtil.ExtractorFactory getExtractorFactory(final int flags) { + return new TestUtil.ExtractorFactory() { + @Override + public Extractor create() { + return new FragmentedMp4Extractor(flags, null); + } + }; + } + } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java index ebb547810b..bcfa90a565 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java @@ -69,8 +69,8 @@ public class AdtsReaderTest extends TestCase { @Override protected void setUp() throws Exception { FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); - adtsOutput = fakeExtractorOutput.track(0); - id3Output = fakeExtractorOutput.track(1); + adtsOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_AUDIO); + id3Output = fakeExtractorOutput.track(1, C.TRACK_TYPE_METADATA); adtsReader = new AdtsReader(true); TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1); adtsReader.createTracks(fakeExtractorOutput, idGenerator); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java index 453a33a521..c4d9de3100 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java @@ -16,9 +16,9 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.extractor.ExtractorOutput; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index c9d6535164..9bcb1c2377 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -17,11 +17,11 @@ package com.google.android.exoplayer2.extractor.ts; import android.test.InstrumentationTestCase; import android.util.SparseArray; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.ByteArrayOutputStream; import java.util.Random; @@ -92,7 +93,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { TrackOutput trackOutput = reader.getTrackOutput(); assertTrue(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */)); assertEquals( - Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, "und", null, 0), + Format.createTextSampleFormat("1/257", "mime", null, 0, 0, "und", null, 0), ((FakeTrackOutput) trackOutput).format); } @@ -178,8 +179,9 @@ public final class TsExtractorTest extends InstrumentationTestCase { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_UNKNOWN); + output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), "mime", null, 0, 0, language, null, 0)); } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java new file mode 100644 index 0000000000..c50ff06699 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2016 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.exoplayer2.metadata.scte35; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataDecoderException; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.util.TimestampAdjuster; +import java.nio.ByteBuffer; +import java.util.List; +import junit.framework.TestCase; + +/** + * Test for {@link SpliceInfoDecoder}. + */ +public final class SpliceInfoDecoderTest extends TestCase { + + private SpliceInfoDecoder decoder; + private MetadataInputBuffer inputBuffer; + + @Override + public void setUp() { + decoder = new SpliceInfoDecoder(); + inputBuffer = new MetadataInputBuffer(); + } + + public void testWrappedAroundTimeSignalCommand() throws MetadataDecoderException { + byte[] rawTimeSignalSection = new byte[] { + 0, // table_id. + (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4). + 0x14, // section_length(8). + 0x00, // protocol_version. + 0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1). + 0x00, 0x00, 0x00, 0x00, // pts_adjustment(32). + 0x00, // cw_index. + 0x00, // tier(8). + 0x00, // tier(4), splice_command_length(4). + 0x05, // splice_command_length(8). + 0x06, // splice_command_type = time_signal. + // Start of splice_time(). + (byte) 0x80, // time_specified_flag, reserved, pts_time(1). + 0x52, 0x03, 0x02, (byte) 0x8f, // pts_time(32). PTS for a second after playback position. + 0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction). + + // The playback position is 57:15:58.43 approximately. + // With this offset, the playback position pts before wrapping is 0x451ebf851. + Metadata metadata = feedInputBuffer(rawTimeSignalSection, 0x3000000000L, -0x50000L); + assertEquals(1, metadata.length()); + assertEquals(removePtsConversionPrecisionError(0x3001000000L, inputBuffer.subsampleOffsetUs), + ((TimeSignalCommand) metadata.get(0)).playbackPositionUs); + } + + public void test2SpliceInsertCommands() throws MetadataDecoderException { + byte[] rawSpliceInsertCommand1 = new byte[] { + 0, // table_id. + (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4). + 0x19, // section_length(8). + 0x00, // protocol_version. + 0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1). + 0x00, 0x00, 0x00, 0x00, // pts_adjustment(32). + 0x00, // cw_index. + 0x00, // tier(8). + 0x00, // tier(4), splice_command_length(4). + 0x0e, // splice_command_length(8). + 0x05, // splice_command_type = splice_insert. + // Start of splice_insert(). + 0x00, 0x00, 0x00, 0x42, // splice_event_id. + 0x00, // splice_event_cancel_indicator, reserved. + 0x40, // out_of_network_indicator, program_splice_flag, duration_flag, + // splice_immediate_flag, reserved. + // start of splice_time(). + (byte) 0x80, // time_specified_flag, reserved, pts_time(1). + 0x00, 0x00, 0x00, 0x00, // PTS for playback position 3s. + 0x00, 0x10, // unique_program_id. + 0x01, // avail_num. + 0x02, // avails_expected. + 0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction). + + Metadata metadata = feedInputBuffer(rawSpliceInsertCommand1, 2000000, 3000000); + assertEquals(1, metadata.length()); + SpliceInsertCommand command = (SpliceInsertCommand) metadata.get(0); + assertEquals(66, command.spliceEventId); + assertFalse(command.spliceEventCancelIndicator); + assertFalse(command.outOfNetworkIndicator); + assertTrue(command.programSpliceFlag); + assertFalse(command.spliceImmediateFlag); + assertEquals(3000000, command.programSplicePlaybackPositionUs); + assertEquals(C.TIME_UNSET, command.breakDuration); + assertEquals(16, command.uniqueProgramId); + assertEquals(1, command.availNum); + assertEquals(2, command.availsExpected); + + byte[] rawSpliceInsertCommand2 = new byte[] { + 0, // table_id. + (byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4). + 0x22, // section_length(8). + 0x00, // protocol_version. + 0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1). + 0x00, 0x00, 0x00, 0x00, // pts_adjustment(32). + 0x00, // cw_index. + 0x00, // tier(8). + 0x00, // tier(4), splice_command_length(4). + 0x13, // splice_command_length(8). + 0x05, // splice_command_type = splice_insert. + // Start of splice_insert(). + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // splice_event_id. + 0x00, // splice_event_cancel_indicator, reserved. + 0x00, // out_of_network_indicator, program_splice_flag, duration_flag, + // splice_immediate_flag, reserved. + 0x02, // component_count. + 0x10, // component_tag. + // start of splice_time(). + (byte) 0x81, // time_specified_flag, reserved, pts_time(1). + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // PTS for playback position 10s. + // start of splice_time(). + 0x11, // component_tag. + 0x00, // time_specified_flag, reserved. + 0x00, 0x20, // unique_program_id. + 0x01, // avail_num. + 0x02, // avails_expected. + 0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction). + + // By changing the subsample offset we force adjuster reconstruction. + long subsampleOffset = 1000011; + metadata = feedInputBuffer(rawSpliceInsertCommand2, 1000000, subsampleOffset); + assertEquals(1, metadata.length()); + command = (SpliceInsertCommand) metadata.get(0); + assertEquals(0xffffffffL, command.spliceEventId); + assertFalse(command.spliceEventCancelIndicator); + assertFalse(command.outOfNetworkIndicator); + assertFalse(command.programSpliceFlag); + assertFalse(command.spliceImmediateFlag); + assertEquals(C.TIME_UNSET, command.programSplicePlaybackPositionUs); + assertEquals(C.TIME_UNSET, command.breakDuration); + List componentSplices = command.componentSpliceList; + assertEquals(2, componentSplices.size()); + assertEquals(16, componentSplices.get(0).componentTag); + assertEquals(1000000, componentSplices.get(0).componentSplicePlaybackPositionUs); + assertEquals(17, componentSplices.get(1).componentTag); + assertEquals(C.TIME_UNSET, componentSplices.get(1).componentSplicePts); + assertEquals(32, command.uniqueProgramId); + assertEquals(1, command.availNum); + assertEquals(2, command.availsExpected); + } + + private Metadata feedInputBuffer(byte[] data, long timeUs, long subsampleOffset) + throws MetadataDecoderException{ + inputBuffer.clear(); + inputBuffer.data = ByteBuffer.allocate(data.length).put(data); + inputBuffer.timeUs = timeUs; + inputBuffer.subsampleOffsetUs = subsampleOffset; + return decoder.decode(inputBuffer); + } + + private static long removePtsConversionPrecisionError(long timeUs, long offsetUs) { + return TimestampAdjuster.ptsToUs(TimestampAdjuster.usToPts(timeUs - offsetUs)) + offsetUs; + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 944781b890..4de0ae4081 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -20,6 +20,8 @@ import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; +import java.util.Collections; +import java.util.List; /** * Unit tests for {@link DashManifestParser}. @@ -70,34 +72,57 @@ public class DashManifestParserTest extends InstrumentationTestCase { } public void testParseCea608AccessibilityChannel() { - assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel("CC1=eng")); - assertEquals(2, DashManifestParser.parseCea608AccessibilityChannel("CC2=eng")); - assertEquals(3, DashManifestParser.parseCea608AccessibilityChannel("CC3=eng")); - assertEquals(4, DashManifestParser.parseCea608AccessibilityChannel("CC4=eng")); + assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel( + buildCea608AccessibilityDescriptors("CC1=eng"))); + assertEquals(2, DashManifestParser.parseCea608AccessibilityChannel( + buildCea608AccessibilityDescriptors("CC2=eng"))); + assertEquals(3, DashManifestParser.parseCea608AccessibilityChannel( + buildCea608AccessibilityDescriptors("CC3=eng"))); + assertEquals(4, DashManifestParser.parseCea608AccessibilityChannel( + buildCea608AccessibilityDescriptors("CC4=eng"))); - assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(null)); - assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("")); - assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC0=eng")); - assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC5=eng")); - assertEquals(Format.NO_VALUE, - DashManifestParser.parseCea608AccessibilityChannel("Wrong format")); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel( + buildCea608AccessibilityDescriptors(null))); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel( + buildCea608AccessibilityDescriptors(""))); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel( + buildCea608AccessibilityDescriptors("CC0=eng"))); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel( + buildCea608AccessibilityDescriptors("CC5=eng"))); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel( + buildCea608AccessibilityDescriptors("Wrong format"))); } public void testParseCea708AccessibilityChannel() { - assertEquals(1, DashManifestParser.parseCea708AccessibilityChannel("1=lang:eng")); - assertEquals(2, DashManifestParser.parseCea708AccessibilityChannel("2=lang:eng")); - assertEquals(3, DashManifestParser.parseCea708AccessibilityChannel("3=lang:eng")); - assertEquals(62, DashManifestParser.parseCea708AccessibilityChannel("62=lang:eng")); - assertEquals(63, DashManifestParser.parseCea708AccessibilityChannel("63=lang:eng")); + assertEquals(1, DashManifestParser.parseCea708AccessibilityChannel( + buildCea708AccessibilityDescriptors("1=lang:eng"))); + assertEquals(2, DashManifestParser.parseCea708AccessibilityChannel( + buildCea708AccessibilityDescriptors("2=lang:eng"))); + assertEquals(3, DashManifestParser.parseCea708AccessibilityChannel( + buildCea708AccessibilityDescriptors("3=lang:eng"))); + assertEquals(62, DashManifestParser.parseCea708AccessibilityChannel( + buildCea708AccessibilityDescriptors("62=lang:eng"))); + assertEquals(63, DashManifestParser.parseCea708AccessibilityChannel( + buildCea708AccessibilityDescriptors("63=lang:eng"))); - assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(null)); - assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel("")); - assertEquals(Format.NO_VALUE, - DashManifestParser.parseCea708AccessibilityChannel("0=lang:eng")); - assertEquals(Format.NO_VALUE, - DashManifestParser.parseCea708AccessibilityChannel("64=lang:eng")); - assertEquals(Format.NO_VALUE, - DashManifestParser.parseCea708AccessibilityChannel("Wrong format")); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel( + buildCea708AccessibilityDescriptors(null))); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel( + buildCea708AccessibilityDescriptors(""))); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel( + buildCea708AccessibilityDescriptors("0=lang:eng"))); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel( + buildCea708AccessibilityDescriptors("64=lang:eng"))); + assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel( + buildCea708AccessibilityDescriptors("Wrong format"))); + } + + private static List buildCea608AccessibilityDescriptors(String value) { + return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-608:2015", value)); + } + + private static List buildCea708AccessibilityDescriptors(String value) { + return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-708:2015", value)); } } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 8eacecf9d3..4286a283c0 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -35,6 +35,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" + "#EXT-X-VERSION:3\n" + + "#EXT-X-PLAYLIST-TYPE:VOD\n" + "#EXT-X-TARGETDURATION:8\n" + "#EXT-X-MEDIA-SEQUENCE:2679\n" + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n" @@ -71,6 +72,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { assertEquals(HlsPlaylist.TYPE_MEDIA, playlist.type); HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; + assertEquals(HlsMediaPlaylist.PLAYLIST_TYPE_VOD, mediaPlaylist.playlistType); assertEquals(2679, mediaPlaylist.mediaSequence); assertEquals(3, mediaPlaylist.version); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index c9eaa33204..a5b272cebd 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -20,9 +20,9 @@ import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSource; -import com.google.android.exoplayer2.testutil.FakeDataSource.Builder; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.FileDataSource; import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -119,9 +119,22 @@ public class CacheDataSourceTest extends InstrumentationTestCase { C.LENGTH_UNSET, KEY_2))); } + public void testIgnoreCacheForUnsetLengthRequests() throws Exception { + CacheDataSource cacheDataSource = createCacheDataSource(false, true, + CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS); + assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET); + MoreAsserts.assertEmpty(simpleCache.getKeys()); + } + + public void testReadOnlyCache() throws Exception { + CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null); + assertReadDataContentLength(cacheDataSource, false, false); + assertEquals(0, cacheDir.list().length); + } + private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength) throws IOException { - // Read all data from upstream and cache + // Read all data from upstream and write to cache CacheDataSource cacheDataSource = createCacheDataSource(false, simulateUnknownLength); assertReadDataContentLength(cacheDataSource, unboundedRequest, simulateUnknownLength); @@ -171,15 +184,27 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private CacheDataSource createCacheDataSource(boolean setReadException, boolean simulateUnknownLength) { - Builder builder = new Builder(); + return createCacheDataSource(setReadException, simulateUnknownLength, + CacheDataSource.FLAG_BLOCK_ON_CACHE); + } + + private CacheDataSource createCacheDataSource(boolean setReadException, + boolean simulateUnknownLength, @CacheDataSource.Flags int flags) { + return createCacheDataSource(setReadException, simulateUnknownLength, flags, + new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE)); + } + + private CacheDataSource createCacheDataSource(boolean setReadException, + boolean simulateUnknownLength, @CacheDataSource.Flags int flags, + CacheDataSink cacheWriteDataSink) { + FakeDataSource.Builder builder = new FakeDataSource.Builder(); if (setReadException) { builder.appendReadError(new IOException("Shouldn't read from upstream")); } - builder.setSimulateUnknownLength(simulateUnknownLength); - builder.appendReadData(TEST_DATA); - FakeDataSource upstream = builder.build(); - return new CacheDataSource(simpleCache, upstream, CacheDataSource.FLAG_BLOCK_ON_CACHE, - MAX_CACHE_FILE_SIZE); + FakeDataSource upstream = + builder.setSimulateUnknownLength(simulateUnknownLength).appendReadData(TEST_DATA).build(); + return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink, + flags, null); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/C.java b/library/src/main/java/com/google/android/exoplayer2/C.java index 0b1c33bfc9..7e9fe46c10 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -515,7 +515,13 @@ public final class C { * The stereo mode for 360/3D/VR videos. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({Format.NO_VALUE, STEREO_MODE_MONO, STEREO_MODE_TOP_BOTTOM, STEREO_MODE_LEFT_RIGHT}) + @IntDef({ + Format.NO_VALUE, + STEREO_MODE_MONO, + STEREO_MODE_TOP_BOTTOM, + STEREO_MODE_LEFT_RIGHT, + STEREO_MODE_STEREO_MESH + }) public @interface StereoMode {} /** * Indicates Monoscopic stereo layout, used with 360/3D/VR videos. @@ -529,6 +535,11 @@ public final class C { * Indicates Left-Right stereo layout, used with 360/3D/VR videos. */ public static final int STEREO_MODE_LEFT_RIGHT = 2; + /** + * Indicates a stereo layout where the left and right eyes have separate meshes, + * used with 360/3D/VR videos. + */ + public static final int STEREO_MODE_STEREO_MESH = 3; /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 266a1e0da2..faf86087c9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1215,7 +1215,7 @@ import java.io.IOException; long newLoadingPeriodStartPositionUs; if (loadingPeriodHolder == null) { - newLoadingPeriodStartPositionUs = playbackInfo.startPositionUs; + newLoadingPeriodStartPositionUs = playbackInfo.positionUs; } else { int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex; if (newLoadingPeriodIndex diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index ea522ac4c8..5100acbbd8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - String VERSION = "2.1.1"; + String VERSION = "2.2.0"; /** * The version of the library, expressed as an integer. @@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo { * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * integer version 123045006 (123-045-006). */ - int VERSION_INT = 2001001; + int VERSION_INT = 2002000; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index 14b52466cb..aa6c435653 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -120,7 +120,7 @@ public final class Format implements Parcelable { /** * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo * modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link - * C#STEREO_MODE_LEFT_RIGHT}. + * C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}. */ @C.StereoMode public final int stereoMode; @@ -447,16 +447,19 @@ public final class Format implements Parcelable { drmInitData, metadata); } - public Format copyWithManifestFormatInfo(Format manifestFormat, - boolean preferManifestDrmInitData) { + public Format copyWithManifestFormatInfo(Format manifestFormat) { + if (this == manifestFormat) { + // No need to copy from ourselves. + return this; + } String id = manifestFormat.id; String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs; int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate; float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate; @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; String language = this.language == null ? manifestFormat.language : this.language; - DrmInitData drmInitData = (preferManifestDrmInitData && manifestFormat.drmInitData != null) - || this.drmInitData == null ? manifestFormat.drmInitData : this.drmInitData; + DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData + : this.drmInitData; return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, @@ -681,9 +684,6 @@ public final class Format implements Parcelable { dest.writeParcelable(metadata, 0); } - /** - * {@link Creator} implementation. - */ public static final Creator CREATOR = new Creator() { @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index da9417374e..4547ec7e08 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -29,6 +29,7 @@ import android.view.SurfaceView; import android.view.TextureView; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -479,8 +480,8 @@ public class SimpleExoPlayer implements ExoPlayer { } @Override - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline) { - player.prepare(mediaSource, resetPosition, resetTimeline); + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + player.prepare(mediaSource, resetPosition, resetState); } @Override @@ -624,7 +625,7 @@ public class SimpleExoPlayer implements ExoPlayer { buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, componentListener, allowedVideoJoiningTimeMs, out); buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, - componentListener, out); + componentListener, buildBufferProcessors(), out); buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out); @@ -636,7 +637,7 @@ public class SimpleExoPlayer implements ExoPlayer { * @param context The {@link Context} associated with the player. * @param mainHandler A handler associated with the main thread's looper. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. + * not be used for DRM protected playbacks. * @param extensionRendererMode The extension renderer mode. * @param eventListener An event listener. * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers @@ -681,17 +682,19 @@ public class SimpleExoPlayer implements ExoPlayer { * @param context The {@link Context} associated with the player. * @param mainHandler A handler associated with the main thread's looper. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. + * not be used for DRM protected playbacks. * @param extensionRendererMode The extension renderer mode. * @param eventListener An event listener. + * @param bufferProcessors An array of {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. May be empty. * @param out An array to which the built renderers should be appended. */ protected void buildAudioRenderers(Context context, Handler mainHandler, DrmSessionManager drmSessionManager, @ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener, - ArrayList out) { + BufferProcessor[] bufferProcessors, ArrayList out) { out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, - mainHandler, eventListener, AudioCapabilities.getCapabilities(context))); + mainHandler, eventListener, AudioCapabilities.getCapabilities(context), bufferProcessors)); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; @@ -705,8 +708,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, BufferProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + bufferProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibopusAudioRenderer."); } catch (ClassNotFoundException e) { @@ -719,8 +723,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, BufferProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + bufferProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibflacAudioRenderer."); } catch (ClassNotFoundException e) { @@ -733,8 +738,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, BufferProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + bufferProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded FfmpegAudioRenderer."); } catch (ClassNotFoundException e) { @@ -787,6 +793,14 @@ public class SimpleExoPlayer implements ExoPlayer { // Do nothing. } + /** + * Builds an array of {@link BufferProcessor}s which will process PCM audio buffers before they + * are output. + */ + protected BufferProcessor[] buildBufferProcessors() { + return new BufferProcessor[0]; + } + // Internal methods. private void removeSurfaceCallbacks() { diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index b5873904fc..cc3f91bc0a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -25,7 +25,6 @@ import android.os.ConditionVariable; import android.os.SystemClock; import android.util.Log; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -54,7 +53,9 @@ import java.nio.ByteOrder; * safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling * {@link #configure(String, int, int, int, int)}. *

- * Call {@link #release()} when the instance is no longer required. + * Call {@link #handleEndOfStream()} to play out all data when no more input buffers will be + * provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call + * {@link #release()} when the instance is no longer required. */ public final class AudioTrack { @@ -89,6 +90,21 @@ public final class AudioTrack { } + /** + * Thrown when a failure occurs configuring the track. + */ + public static final class ConfigurationException extends Exception { + + public ConfigurationException(Throwable cause) { + super(cause); + } + + public ConfigurationException(String message) { + super(message); + } + + } + /** * Thrown when a failure occurs initializing an {@link android.media.AudioTrack}. */ @@ -120,13 +136,15 @@ public final class AudioTrack { public static final class WriteException extends Exception { /** - * An error value returned from {@link android.media.AudioTrack#write(byte[], int, int)}. + * The error value returned from {@link android.media.AudioTrack#write(byte[], int, int)} or + * {@link android.media.AudioTrack#write(ByteBuffer, int, int)}. */ public final int errorCode; /** - * @param errorCode An error value returned from - * {@link android.media.AudioTrack#write(byte[], int, int)}. + * @param errorCode The error value returned from + * {@link android.media.AudioTrack#write(byte[], int, int)} or + * {@link android.media.AudioTrack#write(ByteBuffer, int, int)}. */ public WriteException(int errorCode) { super("AudioTrack write failed: " + errorCode); @@ -212,15 +230,15 @@ public final class AudioTrack { /** * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more * than this amount. - * - *

This is a fail safe that should not be required on correctly functioning devices. + *

+ * This is a fail safe that should not be required on correctly functioning devices. */ private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND; /** * AudioTrack latencies are deemed impossibly large if they are greater than this amount. - * - *

This is a fail safe that should not be required on correctly functioning devices. + *

+ * This is a fail safe that should not be required on correctly functioning devices. */ private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND; @@ -251,6 +269,7 @@ public final class AudioTrack { public static boolean failOnSpuriousAudioTimestamp = false; private final AudioCapabilities audioCapabilities; + private final BufferProcessor[] bufferProcessors; private final Listener listener; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; @@ -264,12 +283,12 @@ public final class AudioTrack { private android.media.AudioTrack audioTrack; private int sampleRate; private int channelConfig; + @C.Encoding + private int encoding; + @C.Encoding + private int outputEncoding; @C.StreamType private int streamType; - @C.Encoding - private int sourceEncoding; - @C.Encoding - private int targetEncoding; private boolean passthrough; private int pcmFrameSize; private int bufferSize; @@ -295,12 +314,10 @@ public final class AudioTrack { private long latencyUs; private float volume; - private byte[] temporaryBuffer; - private int temporaryBufferOffset; - private ByteBuffer currentSourceBuffer; - - private ByteBuffer resampledBuffer; - private boolean useResampledBuffer; + private ByteBuffer inputBuffer; + private ByteBuffer outputBuffer; + private byte[] preV21OutputBuffer; + private int preV21OutputBufferOffset; private boolean playing; private int audioSessionId; @@ -309,11 +326,18 @@ public final class AudioTrack { private long lastFeedElapsedRealtimeMs; /** - * @param audioCapabilities The current audio capabilities. + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors An array of {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. May be empty. * @param listener Listener for audio track events. */ - public AudioTrack(AudioCapabilities audioCapabilities, Listener listener) { + public AudioTrack(AudioCapabilities audioCapabilities, BufferProcessor[] bufferProcessors, + Listener listener) { this.audioCapabilities = audioCapabilities; + this.bufferProcessors = new BufferProcessor[bufferProcessors.length + 1]; + this.bufferProcessors[0] = new ResamplingBufferProcessor(); + System.arraycopy(bufferProcessors, 0, this.bufferProcessors, 1, bufferProcessors.length); this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { @@ -386,7 +410,7 @@ public final class AudioTrack { // The AudioTrack has started, but we don't have any samples to compute a smoothed position. currentPositionUs = audioTrackUtil.getPlaybackHeadPositionUs() + startMediaTimeUs; } else { - // getPlayheadPositionUs() only has a granularity of ~20ms, so we base the position off the + // getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the // system clock (and a smoothed offset between it and the playhead position) so as to // prevent jitter in the reported positions. currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + startMediaTimeUs; @@ -410,9 +434,23 @@ public final class AudioTrack { * {@link C#ENCODING_PCM_32BIT}. * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a * suitable buffer size automatically. + * @throws ConfigurationException If an error occurs configuring the track. */ public void configure(String mimeType, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) { + @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException { + boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); + @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding; + if (!passthrough) { + for (BufferProcessor bufferProcessor : bufferProcessors) { + try { + bufferProcessor.configure(sampleRate, channelCount, encoding); + } catch (BufferProcessor.UnhandledFormatException e) { + throw new ConfigurationException(e); + } + encoding = bufferProcessor.getOutputEncoding(); + } + } + int channelConfig; switch (channelCount) { case 1: @@ -440,7 +478,7 @@ public final class AudioTrack { channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; break; default: - throw new IllegalArgumentException("Unsupported channel count: " + channelCount); + throw new ConfigurationException("Unsupported channel count: " + channelCount); } // Workaround for overly strict channel configuration checks on nVidia Shield. @@ -458,25 +496,13 @@ public final class AudioTrack { } } - boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); - // Workaround for Nexus Player not reporting support for mono passthrough. // (See [Internal: b/34268671].) if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) { channelConfig = AudioFormat.CHANNEL_OUT_STEREO; } - @C.Encoding int sourceEncoding; - if (passthrough) { - sourceEncoding = getEncodingForMimeType(mimeType); - } else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT - || pcmEncoding == C.ENCODING_PCM_24BIT || pcmEncoding == C.ENCODING_PCM_32BIT) { - sourceEncoding = pcmEncoding; - } else { - throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding); - } - - if (isInitialized() && this.sourceEncoding == sourceEncoding && this.sampleRate == sampleRate + if (isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate && this.channelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; @@ -484,28 +510,28 @@ public final class AudioTrack { reset(); - this.sourceEncoding = sourceEncoding; + this.encoding = encoding; this.passthrough = passthrough; this.sampleRate = sampleRate; this.channelConfig = channelConfig; - targetEncoding = passthrough ? sourceEncoding : C.ENCODING_PCM_16BIT; pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels. + outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT; if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; } else if (passthrough) { // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into // account. [Internal: b/25181305] - if (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3) { + if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { // AC-3 allows bitrates up to 640 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND); - } else /* (targetEncoding == C.ENCODING_DTS || targetEncoding == C.ENCODING_DTS_HD */ { + } else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ { // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); } } else { int minBufferSize = - android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, targetEncoding); + android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * pcmFrameSize; @@ -527,15 +553,15 @@ public final class AudioTrack { releasingConditionVariable.block(); if (tunneling) { - audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding, + audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding, bufferSize, audioSessionId); } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - targetEncoding, bufferSize, MODE_STREAM); + outputEncoding, bufferSize, MODE_STREAM); } else { // Re-attach to the same audio session. audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - targetEncoding, bufferSize, MODE_STREAM, audioSessionId); + outputEncoding, bufferSize, MODE_STREAM, audioSessionId); } checkAudioTrackInitialized(); @@ -607,8 +633,10 @@ public final class AudioTrack { * @throws InitializationException If an error occurs initializing the track. * @throws WriteException If an error occurs writing the audio data. */ + @SuppressWarnings("ReferenceEquality") public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws InitializationException, WriteException { + Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer); if (!isInitialized()) { initialize(); if (playing) { @@ -616,26 +644,12 @@ public final class AudioTrack { } } - boolean hadData = hasData; - hasData = hasPendingData(); - if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { - long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; - listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); - } - boolean result = writeBuffer(buffer, presentationTimeUs); - lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); - return result; - } - - private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { - boolean isNewSourceBuffer = currentSourceBuffer == null; - Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer); - currentSourceBuffer = buffer; - if (needsPassthroughWorkarounds()) { // An AC-3 audio track continues to play data written while it is paused. Stop writing so its // buffer empties. See [Internal: b/18899620]. if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) { + // We force an underrun to pause the track, so don't notify the listener in this case. + hasData = false; return false; } @@ -648,27 +662,25 @@ public final class AudioTrack { } } - if (isNewSourceBuffer) { - // We're seeing this buffer for the first time. + boolean hadData = hasData; + hasData = hasPendingData(); + if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { + long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; + listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); + } - if (!currentSourceBuffer.hasRemaining()) { + if (inputBuffer == null) { + // We are seeing this buffer for the first time. + if (!buffer.hasRemaining()) { // The buffer is empty. - currentSourceBuffer = null; return true; } - useResampledBuffer = targetEncoding != sourceEncoding; - if (useResampledBuffer) { - Assertions.checkState(targetEncoding == C.ENCODING_PCM_16BIT); - // Resample the buffer to get the data in the target encoding. - resampledBuffer = resampleTo16BitPcm(currentSourceBuffer, sourceEncoding, resampledBuffer); - buffer = resampledBuffer; - } - if (passthrough && framesPerEncodedSample == 0) { // If this is the first encoded sample, calculate the sample size in frames. - framesPerEncodedSample = getFramesPerEncodedSample(targetEncoding, buffer); + framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); } + if (startMediaTimeState == START_NOT_SET) { startMediaTimeUs = Math.max(0, presentationTimeUs); startMediaTimeState = START_IN_SYNC; @@ -690,21 +702,35 @@ public final class AudioTrack { listener.onPositionDiscontinuity(); } } - if (Util.SDK_INT < 21) { - // Copy {@code buffer} into {@code temporaryBuffer}. - int bytesRemaining = buffer.remaining(); - if (temporaryBuffer == null || temporaryBuffer.length < bytesRemaining) { - temporaryBuffer = new byte[bytesRemaining]; + + inputBuffer = buffer; + if (!passthrough) { + for (BufferProcessor bufferProcessor : bufferProcessors) { + buffer = bufferProcessor.handleBuffer(buffer); } - int originalPosition = buffer.position(); - buffer.get(temporaryBuffer, 0, bytesRemaining); - buffer.position(originalPosition); - temporaryBufferOffset = 0; + } + outputBuffer = buffer; + if (Util.SDK_INT < 21) { + int bytesRemaining = outputBuffer.remaining(); + if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { + preV21OutputBuffer = new byte[bytesRemaining]; + } + int originalPosition = outputBuffer.position(); + outputBuffer.get(preV21OutputBuffer, 0, bytesRemaining); + outputBuffer.position(originalPosition); + preV21OutputBufferOffset = 0; } } - buffer = useResampledBuffer ? resampledBuffer : buffer; - int bytesRemaining = buffer.remaining(); + if (writeOutputBuffer(presentationTimeUs)) { + inputBuffer = null; + return true; + } + return false; + } + + private boolean writeOutputBuffer(long presentationTimeUs) throws WriteException { + int bytesRemaining = outputBuffer.remaining(); int bytesWritten = 0; if (Util.SDK_INT < 21) { // passthrough == false // Work out how many bytes we can write without the risk of blocking. @@ -713,18 +739,21 @@ public final class AudioTrack { int bytesToWrite = bufferSize - bytesPending; if (bytesToWrite > 0) { bytesToWrite = Math.min(bytesRemaining, bytesToWrite); - bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite); - if (bytesWritten >= 0) { - temporaryBufferOffset += bytesWritten; + bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); + if (bytesWritten > 0) { + preV21OutputBufferOffset += bytesWritten; + outputBuffer.position(outputBuffer.position() + bytesWritten); } - buffer.position(buffer.position() + bytesWritten); } + } else if (tunneling) { + bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, outputBuffer, bytesRemaining, + presentationTimeUs); } else { - bytesWritten = tunneling - ? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs) - : writeNonBlockingV21(audioTrack, buffer, bytesRemaining); + bytesWritten = writeNonBlockingV21(audioTrack, outputBuffer, bytesRemaining); } + lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); + if (bytesWritten < 0) { throw new WriteException(bytesWritten); } @@ -736,7 +765,6 @@ public final class AudioTrack { if (passthrough) { submittedEncodedFrames += framesPerEncodedSample; } - currentSourceBuffer = null; return true; } return false; @@ -812,7 +840,7 @@ public final class AudioTrack { * audio session id has changed. Enabling tunneling requires platform API version 21 onwards. * * @param tunnelingAudioSessionId The audio session id to use. - * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. + * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. */ public void enableTunnelingV21(int tunnelingAudioSessionId) { Assertions.checkState(Util.SDK_INT >= 21); @@ -880,8 +908,11 @@ public final class AudioTrack { submittedPcmBytes = 0; submittedEncodedFrames = 0; framesPerEncodedSample = 0; - currentSourceBuffer = null; + inputBuffer = null; avSyncHeader = null; + for (BufferProcessor bufferProcessor : bufferProcessors) { + bufferProcessor.flush(); + } bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; latencyUs = 0; @@ -915,6 +946,9 @@ public final class AudioTrack { public void release() { reset(); releaseKeepSessionIdAudioTrack(); + for (BufferProcessor bufferProcessor : bufferProcessors) { + bufferProcessor.release(); + } audioSessionId = C.AUDIO_SESSION_ID_UNSET; playing = false; } @@ -1089,7 +1123,7 @@ public final class AudioTrack { */ private boolean needsPassthroughWorkarounds() { return Util.SDK_INT < 23 - && (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3); + && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3); } /** @@ -1124,82 +1158,6 @@ public final class AudioTrack { sessionId); } - /** - * Converts the provided buffer into 16-bit PCM. - * - * @param buffer The buffer containing the data to convert. - * @param sourceEncoding The data encoding. - * @param out A buffer into which the output should be written, if its capacity is sufficient. - * @return The 16-bit PCM output. Different to the out parameter if null was passed, or if the - * capacity was insufficient for the output. - */ - private static ByteBuffer resampleTo16BitPcm(ByteBuffer buffer, @C.PcmEncoding int sourceEncoding, - ByteBuffer out) { - int offset = buffer.position(); - int limit = buffer.limit(); - int size = limit - offset; - - int resampledSize; - switch (sourceEncoding) { - case C.ENCODING_PCM_8BIT: - resampledSize = size * 2; - break; - case C.ENCODING_PCM_24BIT: - resampledSize = (size / 3) * 2; - break; - case C.ENCODING_PCM_32BIT: - resampledSize = size / 2; - break; - case C.ENCODING_PCM_16BIT: - case C.ENCODING_INVALID: - case Format.NO_VALUE: - default: - // Never happens. - throw new IllegalStateException(); - } - - ByteBuffer resampledBuffer = out; - if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) { - resampledBuffer = ByteBuffer.allocateDirect(resampledSize); - } - resampledBuffer.position(0); - resampledBuffer.limit(resampledSize); - - // Samples are little endian. - switch (sourceEncoding) { - case C.ENCODING_PCM_8BIT: - // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. - for (int i = offset; i < limit; i++) { - resampledBuffer.put((byte) 0); - resampledBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128)); - } - break; - case C.ENCODING_PCM_24BIT: - // 24->16 bit resampling. Drop the least significant byte. - for (int i = offset; i < limit; i += 3) { - resampledBuffer.put(buffer.get(i + 1)); - resampledBuffer.put(buffer.get(i + 2)); - } - break; - case C.ENCODING_PCM_32BIT: - // 32->16 bit resampling. Drop the two least significant bytes. - for (int i = offset; i < limit; i += 4) { - resampledBuffer.put(buffer.get(i + 2)); - resampledBuffer.put(buffer.get(i + 3)); - } - break; - case C.ENCODING_PCM_16BIT: - case C.ENCODING_INVALID: - case Format.NO_VALUE: - default: - // Never happens. - throw new IllegalStateException(); - } - - resampledBuffer.position(0); - return resampledBuffer; - } - @C.Encoding private static int getEncodingForMimeType(String mimeType) { switch (mimeType) { diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java new file mode 100644 index 0000000000..4f604f1a5d --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2017 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.exoplayer2.audio; + +import com.google.android.exoplayer2.C; +import java.nio.ByteBuffer; + +/** + * Interface for processors of audio buffers. + */ +public interface BufferProcessor { + + /** + * Exception thrown when a processor can't be configured for a given input format. + */ + final class UnhandledFormatException extends Exception { + + public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) { + super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding " + + encoding); + } + + } + + /** + * Configures this processor to take input buffers with the specified format. + * + * @param sampleRateHz The sample rate of input audio in Hz. + * @param channelCount The number of interleaved channels in input audio. + * @param encoding The encoding of input audio. + * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. + */ + void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException; + + /** + * Returns the encoding used in buffers output by this processor. + */ + @C.Encoding + int getOutputEncoding(); + + /** + * Processes the data in the specified input buffer in its entirety. + * + * @param input A buffer containing the input data to process. + * @return A buffer containing the processed output. This may be the same as the input buffer if + * no processing was required. + */ + ByteBuffer handleBuffer(ByteBuffer input); + + /** + * Clears any state in preparation for receiving a new stream of buffers. + */ + void flush(); + + /** + * Releases any resources associated with this instance. + */ + void release(); + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index b4813d90a2..dc7cdf42c8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -121,13 +121,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { + AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, + BufferProcessor... bufferProcessors) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener()); + audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener()); eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -183,7 +186,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void configureCodec(MediaCodec codec, Format format, MediaCrypto crypto) { + protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, + MediaCrypto crypto) { if (passthroughEnabled) { // Override the MIME type used to configure the codec if we are using a passthrough decoder. passthroughMediaFormat = format.getFrameworkMediaFormatV16(); @@ -218,14 +222,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + throws ExoPlaybackException { boolean passthrough = passthroughMediaFormat != null; String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME) : MimeTypes.AUDIO_RAW; MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); - audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + try { + audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + } catch (AudioTrack.ConfigurationException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java new file mode 100644 index 0000000000..507cdbcdd1 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2017 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.exoplayer2.audio; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import java.nio.ByteBuffer; + +/** + * A {@link BufferProcessor} that outputs buffers in {@link C#ENCODING_PCM_16BIT}. + */ +/* package */ final class ResamplingBufferProcessor implements BufferProcessor { + + @C.PcmEncoding + private int encoding; + private ByteBuffer outputBuffer; + + public ResamplingBufferProcessor() { + encoding = C.ENCODING_INVALID; + } + + @Override + public void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException { + if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT + && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (encoding == C.ENCODING_PCM_16BIT) { + outputBuffer = null; + } + this.encoding = encoding; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public ByteBuffer handleBuffer(ByteBuffer buffer) { + int position = buffer.position(); + int limit = buffer.limit(); + int size = limit - position; + + int resampledSize; + switch (encoding) { + case C.ENCODING_PCM_16BIT: + // No processing required. + return buffer; + case C.ENCODING_PCM_8BIT: + resampledSize = size * 2; + break; + case C.ENCODING_PCM_24BIT: + resampledSize = (size / 3) * 2; + break; + case C.ENCODING_PCM_32BIT: + resampledSize = size / 2; + break; + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + if (outputBuffer == null || outputBuffer.capacity() < resampledSize) { + outputBuffer = ByteBuffer.allocateDirect(resampledSize).order(buffer.order()); + } else { + outputBuffer.clear(); + } + + // Samples are little endian. + switch (encoding) { + case C.ENCODING_PCM_8BIT: + // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. + for (int i = position; i < limit; i++) { + outputBuffer.put((byte) 0); + outputBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128)); + } + break; + case C.ENCODING_PCM_24BIT: + // 24->16 bit resampling. Drop the least significant byte. + for (int i = position; i < limit; i += 3) { + outputBuffer.put(buffer.get(i + 1)); + outputBuffer.put(buffer.get(i + 2)); + } + break; + case C.ENCODING_PCM_32BIT: + // 32->16 bit resampling. Drop the two least significant bytes. + for (int i = position; i < limit; i += 4) { + outputBuffer.put(buffer.get(i + 2)); + outputBuffer.put(buffer.get(i + 3)); + } + break; + case C.ENCODING_PCM_16BIT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + outputBuffer.flip(); + return outputBuffer; + } + + @Override + public void flush() { + // Do nothing. + } + + @Override + public void release() { + outputBuffer = null; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index d23ee769dd..9e75145626 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -102,10 +102,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @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. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener) { - this(eventHandler, eventListener, null); + AudioRendererEventListener eventListener, BufferProcessor... bufferProcessors) { + this(eventHandler, eventListener, null, null, false, bufferProcessors); } /** @@ -133,13 +135,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * begin in parallel with key acquisition. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + BufferProcessor... bufferProcessors) { super(C.TRACK_TYPE_AUDIO); eventDispatcher = new EventDispatcher(eventHandler, eventListener); - audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener()); + audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener()); this.drmSessionManager = drmSessionManager; formatHolder = new FormatHolder(); this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; @@ -193,8 +198,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements while (drainOutputBuffer()) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } catch (AudioTrack.InitializationException | AudioTrack.WriteException - | AudioDecoderException e) { + } catch (AudioDecoderException | AudioTrack.ConfigurationException + | AudioTrack.InitializationException | AudioTrack.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } decoderCounters.ensureUpdated(); @@ -255,7 +260,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException, - AudioTrack.InitializationException, AudioTrack.WriteException { + AudioTrack.ConfigurationException, AudioTrack.InitializationException, + AudioTrack.WriteException { if (outputBuffer == null) { outputBuffer = decoder.dequeueOutputBuffer(); if (outputBuffer == null) { diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 9c959a38c5..3af0f8a5c0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -280,6 +280,7 @@ public class DefaultDrmSessionManager implements DrmSe * required. * *

{@code mode} must be one of these: + *

    *
  • {@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is * requested otherwise the offline license is restored. *
  • {@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license @@ -288,6 +289,7 @@ public class DefaultDrmSessionManager implements DrmSe * requested otherwise the offline license is renewed. *
  • {@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license * is released. + *
* * @param mode The mode to be set. * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. @@ -530,9 +532,8 @@ public class DefaultDrmSessionManager implements DrmSe } private void postKeyRequest(byte[] scope, int keyType) { - KeyRequest keyRequest; try { - keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType, + KeyRequest keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType, optionalKeyRequestParameters); postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget(); } catch (Exception e) { @@ -564,7 +565,8 @@ public class DefaultDrmSessionManager implements DrmSe } } else { byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response); - if (keySetId != null && keySetId.length != 0) { + if ((mode == MODE_DOWNLOAD || (mode == MODE_PLAYBACK && offlineLicenseKeySetId != null)) + && keySetId != null && keySetId.length != 0) { offlineLicenseKeySetId = keySetId; } state = STATE_OPENED_WITH_KEYS; diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 4d64187a8b..df9b1fffa0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -31,7 +31,7 @@ public interface DrmSession { /** Wraps the exception which is the cause of the error state. */ class DrmSessionException extends Exception { - DrmSessionException(Exception e) { + public DrmSessionException(Exception e) { super(e); } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index e0c9ca5296..f9d5efffb1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -24,6 +24,8 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.HashMap; @@ -57,21 +59,62 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { } /** + * @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request + * properties can be set by calling {@link #setKeyRequestProperty(String, String)}. * @param defaultUrl The default license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @param keyRequestProperties Request properties to set when making key requests, or null. */ + @Deprecated public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory, Map keyRequestProperties) { this.dataSourceFactory = dataSourceFactory; this.defaultUrl = defaultUrl; - this.keyRequestProperties = keyRequestProperties; + this.keyRequestProperties = new HashMap<>(); + if (keyRequestProperties != null) { + this.keyRequestProperties.putAll(keyRequestProperties); + } + } + + /** + * Sets a header for key requests made by the callback. + * + * @param name The name of the header field. + * @param value The value of the field. + */ + public void setKeyRequestProperty(String name, String value) { + Assertions.checkNotNull(name); + Assertions.checkNotNull(value); + synchronized (keyRequestProperties) { + keyRequestProperties.put(name, value); + } + } + + /** + * Clears a header for key requests made by the callback. + * + * @param name The name of the header field. + */ + public void clearKeyRequestProperty(String name) { + Assertions.checkNotNull(name); + synchronized (keyRequestProperties) { + keyRequestProperties.remove(name); + } + } + + /** + * Clears all headers for key requests made by the callback. + */ + public void clearAllKeyRequestProperties() { + synchronized (keyRequestProperties) { + keyRequestProperties.clear(); + } } @Override public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); - return executePost(url, new byte[0], null); + return executePost(dataSourceFactory, url, new byte[0], null); } @Override @@ -85,14 +128,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { if (C.PLAYREADY_UUID.equals(uuid)) { requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES); } - if (keyRequestProperties != null) { + synchronized (keyRequestProperties) { requestProperties.putAll(keyRequestProperties); } - return executePost(url, request.getData(), requestProperties); + return executePost(dataSourceFactory, url, request.getData(), requestProperties); } - private byte[] executePost(String url, byte[] data, Map requestProperties) - throws IOException { + private static byte[] executePost(HttpDataSource.Factory dataSourceFactory, String url, + byte[] data, Map requestProperties) throws IOException { HttpDataSource dataSource = dataSourceFactory.createDataSource(); if (requestProperties != null) { for (Map.Entry requestProperty : requestProperties.entrySet()) { diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index a11d65d4d3..b3729c2377 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -93,7 +93,7 @@ public final class OfflineLicenseHelper { public static OfflineLicenseHelper newWidevineInstance( String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { return newWidevineInstance( - new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null); + new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null); } /** @@ -210,11 +210,14 @@ public final class OfflineLicenseHelper { Representation representation = adaptationSet.representations.get(0); DrmInitData drmInitData = representation.format.drmInitData; if (drmInitData == null) { - InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation); + ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format, + adaptationSet.type); + InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation, + extractorWrapper); if (initializationChunk == null) { return null; } - Format sampleFormat = initializationChunk.getSampleFormat(); + Format sampleFormat = extractorWrapper.getSampleFormat(); if (sampleFormat != null) { drmInitData = sampleFormat.drmInitData; } @@ -288,8 +291,9 @@ public final class OfflineLicenseHelper { return session; } - private static InitializationChunk loadInitializationChunk(final DataSource dataSource, - final Representation representation) throws IOException, InterruptedException { + private static InitializationChunk loadInitializationChunk(DataSource dataSource, + Representation representation, ChunkExtractorWrapper extractorWrapper) + throws IOException, InterruptedException { RangedUri rangedUri = representation.getInitializationUri(); if (rangedUri == null) { return null; @@ -298,18 +302,17 @@ public final class OfflineLicenseHelper { rangedUri.length, representation.getCacheKey()); InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec, representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */, - newWrappedExtractor(representation.format)); + extractorWrapper); initializationChunk.load(); return initializationChunk; } - private static ChunkExtractorWrapper newWrappedExtractor(final Format format) { + private static ChunkExtractorWrapper newWrappedExtractor(Format format, int trackType) { final String mimeType = format.containerMimeType; final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); - return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */, - false /* resendFormatOnInit */); + return new ChunkExtractorWrapper(extractor, format, trackType); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java index b3bcd97048..460e8d33a8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java @@ -70,6 +70,8 @@ public final class DefaultTrackOutput implements TrackOutput { private Format downstreamFormat; // Accessed only by the loading thread (or the consuming thread when there is no loading thread). + private boolean pendingFormatAdjustment; + private Format lastUnadjustedFormat; private long sampleOffsetUs; private long totalBytesWritten; private Allocation lastAllocation; @@ -445,23 +447,24 @@ public final class DefaultTrackOutput implements TrackOutput { } /** - * Like {@link #format(Format)}, but with an offset that will be added to the timestamps of - * samples subsequently queued to the buffer. The offset is also used to adjust - * {@link Format#subsampleOffsetUs} for both the {@link Format} passed and those subsequently - * passed to {@link #format(Format)}. + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples + * subsequently queued to the buffer. * - * @param format The format. * @param sampleOffsetUs The timestamp offset in microseconds. */ - public void formatWithOffset(Format format, long sampleOffsetUs) { - this.sampleOffsetUs = sampleOffsetUs; - format(format); + public void setSampleOffsetUs(long sampleOffsetUs) { + if (this.sampleOffsetUs != sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + pendingFormatAdjustment = true; + } } @Override public void format(Format format) { Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); boolean formatChanged = infoQueue.format(adjustedFormat); + lastUnadjustedFormat = format; + pendingFormatAdjustment = false; if (upstreamFormatChangeListener != null && formatChanged) { upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); } @@ -518,6 +521,9 @@ public final class DefaultTrackOutput implements TrackOutput { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, byte[] encryptionKey) { + if (pendingFormatAdjustment) { + format(lastUnadjustedFormat); + } if (!startWriteOperation()) { infoQueue.commitSampleTimestamp(timeUs); return; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java index 38b0325cba..de3dfd5266 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java @@ -102,4 +102,5 @@ public interface Extractor { * Releases all kept resources. */ void release(); + } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java index a547f745ca..a59cb1d1f2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java @@ -23,17 +23,18 @@ public interface ExtractorOutput { /** * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track. *

- * The same {@link TrackOutput} is returned if multiple calls are made with the same - * {@code trackId}. + * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. * - * @param trackId A track identifier. + * @param id A track identifier. + * @param type The type of the track. Typically one of the {@link com.google.android.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. * @return The {@link TrackOutput} for the given track identifier. */ - TrackOutput track(int trackId); + TrackOutput track(int id, int type); /** * Called when all tracks have been identified, meaning no new {@code trackId} values will be - * passed to {@link #track(int)}. + * passed to {@link #track(int, int)}. */ void endTracks(); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 5b396749ac..218e6ffd82 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.flv; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -183,10 +184,12 @@ public final class FlvExtractor implements Extractor, SeekMap { boolean hasAudio = (flags & 0x04) != 0; boolean hasVideo = (flags & 0x01) != 0; if (hasAudio && audioReader == null) { - audioReader = new AudioTagPayloadReader(extractorOutput.track(TAG_TYPE_AUDIO)); + audioReader = new AudioTagPayloadReader( + extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO)); } if (hasVideo && videoReader == null) { - videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO)); + videoReader = new VideoTagPayloadReader( + extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO)); } if (metadataReader == null) { metadataReader = new ScriptTagPayloadReader(null); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index ccd74bc654..40110c724e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -546,11 +546,9 @@ public final class MatroskaExtractor implements Extractor { } break; case ID_TRACK_ENTRY: - if (tracks.get(currentTrack.number) == null && isCodecSupported(currentTrack.codecId)) { + if (isCodecSupported(currentTrack.codecId)) { currentTrack.initializeOutput(extractorOutput, currentTrack.number); tracks.put(currentTrack.number, currentTrack); - } else { - // We've seen this track entry before, or the codec is unsupported. Do nothing. } currentTrack = null; break; @@ -692,6 +690,9 @@ public final class MatroskaExtractor implements Extractor { case 3: currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM; break; + case 15: + currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH; + break; default: break; } @@ -1525,6 +1526,7 @@ public final class MatroskaExtractor implements Extractor { throw new ParserException("Unrecognized codec identifier."); } + int type; Format format; @C.SelectionFlags int selectionFlags = 0; selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0; @@ -1532,10 +1534,12 @@ public final class MatroskaExtractor implements Extractor { // TODO: Consider reading the name elements of the tracks and, if present, incorporating them // into the trackId passed when creating the formats. if (MimeTypes.isAudio(mimeType)) { + type = C.TRACK_TYPE_AUDIO; format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding, initializationData, drmInitData, selectionFlags, language); } else if (MimeTypes.isVideo(mimeType)) { + type = C.TRACK_TYPE_VIDEO; if (displayUnit == Track.DISPLAY_UNIT_PIXELS) { displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth; displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight; @@ -1548,10 +1552,12 @@ public final class MatroskaExtractor implements Extractor { Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData, Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData); } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, selectionFlags, language, drmInitData); } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) || MimeTypes.APPLICATION_PGS.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, initializationData, language, drmInitData); } else if (MimeTypes.TEXT_SSA.equals(mimeType)) { @@ -1561,7 +1567,7 @@ public final class MatroskaExtractor implements Extractor { throw new ParserException("Unexpected MIME type."); } - this.output = output.track(number); + this.output = output.track(number, type); this.output.format(format); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 9bdefeceaf..00394f7912 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -33,6 +34,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Extracts data from an MP3 file. @@ -51,6 +54,18 @@ public final class Mp3Extractor implements Extractor { }; + /** + * Flags controlling the behavior of the extractor. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) + public @interface Flags {} + /** + * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would + * otherwise not be possible. + */ + public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; + /** * The maximum number of bytes to search when synchronizing, before giving up. */ @@ -72,6 +87,7 @@ public final class Mp3Extractor implements Extractor { private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); + @Flags private final int flags; private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; @@ -93,16 +109,27 @@ public final class Mp3Extractor implements Extractor { * Constructs a new {@link Mp3Extractor}. */ public Mp3Extractor() { - this(C.TIME_UNSET); + this(0); } /** * Constructs a new {@link Mp3Extractor}. * + * @param flags Flags that control the extractor's behavior. + */ + public Mp3Extractor(@Flags int flags) { + this(flags, C.TIME_UNSET); + } + + /** + * Constructs a new {@link Mp3Extractor}. + * + * @param flags Flags that control the extractor's behavior. * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or * {@link C#TIME_UNSET} if forcing is not required. */ - public Mp3Extractor(long forcedFirstSampleTimestampUs) { + public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) { + this.flags = flags; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(SCRATCH_LENGTH); synchronizedHeader = new MpegAudioHeader(); @@ -118,7 +145,7 @@ public final class Mp3Extractor implements Extractor { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = extractorOutput.track(0); + trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); extractorOutput.endTracks(); } @@ -350,7 +377,8 @@ public final class Mp3Extractor implements Extractor { } } - if (seeker == null) { + if (seeker == null || (!seeker.isSeekable() + && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { // Repopulate the synchronized header in case we had to skip an invalid seeking header, which // would give an invalid CBR bitrate. input.resetPeekPosition(); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index c8ee8ff8c3..cc7e662336 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -135,6 +135,7 @@ import java.util.List; public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); public static final int TYPE_camm = Util.getIntegerCodeForString("camm"); + public static final int TYPE_alac = Util.getIntegerCodeForString("alac"); public final int type; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 9dc0578263..54141f2545 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -332,6 +332,9 @@ import java.util.List; return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } + // Omit any sample at the end point of an edit for audio tracks. + boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO; + // Count the number of samples after applying edits. int editedSampleCount = 0; int nextSampleIndex = 0; @@ -342,7 +345,8 @@ import java.util.List; long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale, track.movieTimescale); int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, true, false); + int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample, + false); editedSampleCount += endIndex - startIndex; copyMetadata |= nextSampleIndex != startIndex; nextSampleIndex = endIndex; @@ -365,7 +369,7 @@ import java.util.List; long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale, track.movieTimescale); int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, true, false); + int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false); if (copyMetadata) { int count = endIndex - startIndex; System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count); @@ -604,7 +608,7 @@ import java.util.List; || childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl || childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt - || childAtomType == Atom.TYPE__mp3) { + || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac) { parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, isQuickTime, drmInitData, out, i); } else if (childAtomType == Atom.TYPE_TTML) { @@ -716,6 +720,9 @@ import java.util.List; case 2: stereoMode = C.STEREO_MODE_LEFT_RIGHT; break; + case 3: + stereoMode = C.STEREO_MODE_STEREO_MESH; + break; default: break; } @@ -839,6 +846,8 @@ import java.util.List; mimeType = MimeTypes.AUDIO_RAW; } else if (atomType == Atom.TYPE__mp3) { mimeType = MimeTypes.AUDIO_MPEG; + } else if (atomType == Atom.TYPE_alac) { + mimeType = MimeTypes.AUDIO_ALAC; } byte[] initializationData = null; @@ -876,6 +885,10 @@ import java.util.List; out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); + } else if (childAtomType == Atom.TYPE_alac) { + initializationData = new byte[childAtomSize]; + parent.setPosition(childPosition); + parent.readBytes(initializationData, 0, childAtomSize); } childPosition += childAtomSize; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 603aec4b22..0beb644ff6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -31,14 +31,15 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom; +import com.google.android.exoplayer2.text.cea.CeaUtil; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; @@ -67,15 +68,13 @@ public final class FragmentedMp4Extractor implements Extractor { }; - private static final String TAG = "FragmentedMp4Extractor"; - private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); - /** * Flags controlling the behavior of the extractor. */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, - FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_SIDELOADED}) + FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK, + FLAG_SIDELOADED}) public @interface Flags {} /** * Flag to work around an issue in some video streams where every frame is marked as a sync frame. @@ -94,12 +93,19 @@ public final class FragmentedMp4Extractor implements Extractor { * messages in the stream will be delivered as samples to this track. */ public static final int FLAG_ENABLE_EMSG_TRACK = 4; + /** + * Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages + * contained within SEI NAL units in the stream will be delivered as samples to this track. + */ + public static final int FLAG_ENABLE_CEA608_TRACK = 8; /** * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 * container. */ - private static final int FLAG_SIDELOADED = 8; + private static final int FLAG_SIDELOADED = 16; + private static final String TAG = "FragmentedMp4Extractor"; + private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; @@ -120,7 +126,8 @@ public final class FragmentedMp4Extractor implements Extractor { // Temporary arrays. private final ParsableByteArray nalStartCode; - private final ParsableByteArray nalLength; + private final ParsableByteArray nalPrefix; + private final ParsableByteArray nalBuffer; private final ParsableByteArray encryptionSignalByte; // Adjusts sample timestamps. @@ -146,16 +153,25 @@ public final class FragmentedMp4Extractor implements Extractor { private int sampleSize; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; + private boolean processSeiNalUnitPayload; // Extractor output. private ExtractorOutput extractorOutput; private TrackOutput eventMessageTrackOutput; + private TrackOutput cea608TrackOutput; // Whether extractorOutput.seekMap has been called. private boolean haveOutputSeekMap; public FragmentedMp4Extractor() { - this(0, null); + this(0); + } + + /** + * @param flags Flags that control the extractor's behavior. + */ + public FragmentedMp4Extractor(@Flags int flags) { + this(flags, null); } /** @@ -163,23 +179,24 @@ public final class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) { - this(flags, null, timestampAdjuster); + this(flags, timestampAdjuster, null); } /** * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor * will not receive a moov box in the input data. - * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ - public FragmentedMp4Extractor(@Flags int flags, Track sideloadedTrack, - TimestampAdjuster timestampAdjuster) { - this.sideloadedTrack = sideloadedTrack; + public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, + Track sideloadedTrack) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; + this.sideloadedTrack = sideloadedTrack; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); - nalLength = new ParsableByteArray(4); + nalPrefix = new ParsableByteArray(5); + nalBuffer = new ParsableByteArray(); encryptionSignalByte = new ParsableByteArray(1); extendedTypeScratch = new byte[16]; containerAtoms = new Stack<>(); @@ -199,10 +216,10 @@ public final class FragmentedMp4Extractor implements Extractor { public void init(ExtractorOutput output) { extractorOutput = output; if (sideloadedTrack != null) { - TrackBundle bundle = new TrackBundle(output.track(0)); + TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type)); bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); trackBundles.put(0, bundle); - maybeInitEventMessageTrack(); + maybeInitExtraTracks(); extractorOutput.endTracks(); } } @@ -410,19 +427,19 @@ public final class FragmentedMp4Extractor implements Extractor { // We need to create the track bundles. for (int i = 0; i < trackCount; i++) { Track track = tracks.valueAt(i); - trackBundles.put(track.id, new TrackBundle(extractorOutput.track(i))); + TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); + trackBundle.init(track, defaultSampleValuesArray.get(track.id)); + trackBundles.put(track.id, trackBundle); durationUs = Math.max(durationUs, track.durationUs); } - maybeInitEventMessageTrack(); + maybeInitExtraTracks(); extractorOutput.endTracks(); } else { Assertions.checkState(trackBundles.size() == trackCount); - } - - // Initialization of tracks and default sample values. - for (int i = 0; i < trackCount; i++) { - Track track = tracks.valueAt(i); - trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id)); + for (int i = 0; i < trackCount; i++) { + Track track = tracks.valueAt(i); + trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id)); + } } } @@ -437,13 +454,17 @@ public final class FragmentedMp4Extractor implements Extractor { } } - private void maybeInitEventMessageTrack() { - if ((flags & FLAG_ENABLE_EMSG_TRACK) == 0) { - return; + private void maybeInitExtraTracks() { + if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) { + eventMessageTrackOutput = extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); + eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, + Format.OFFSET_SAMPLE_RELATIVE)); + } + if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutput == null) { + cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, C.TRACK_TYPE_TEXT); + cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, + null, Format.NO_VALUE, 0, null, null)); } - eventMessageTrackOutput = extractorOutput.track(trackBundles.size()); - eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, - Format.OFFSET_SAMPLE_RELATIVE)); } /** @@ -1045,29 +1066,50 @@ public final class FragmentedMp4Extractor implements Extractor { if (track.nalUnitLengthFieldLength != 0) { // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. - byte[] nalLengthData = nalLength.data; - nalLengthData[0] = 0; - nalLengthData[1] = 0; - nalLengthData[2] = 0; - int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength; + byte[] nalPrefixData = nalPrefix.data; + nalPrefixData[0] = 0; + nalPrefixData[1] = 0; + nalPrefixData[2] = 0; + int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1; int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength; // NAL units are length delimited, but the decoder requires start code delimited units. // Loop until we've written the sample to the track output, replacing length delimiters with // start codes as we encounter them. while (sampleBytesWritten < sampleSize) { if (sampleCurrentNalBytesRemaining == 0) { - // Read the NAL length so that we know where we find the next one. - input.readFully(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); - nalLength.setPosition(0); - sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); + // Read the NAL length so that we know where we find the next one, and its type. + input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength); + nalPrefix.setPosition(0); + sampleCurrentNalBytesRemaining = nalPrefix.readUnsignedIntToInt() - 1; // Write a start code for the current NAL unit. nalStartCode.setPosition(0); output.sampleData(nalStartCode, 4); - sampleBytesWritten += 4; + // Write the NAL unit type byte. + output.sampleData(nalPrefix, 1); + // TODO: Don't try and process the SEI NAL unit if the payload is encrypted. + processSeiNalUnitPayload = cea608TrackOutput != null + && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); + sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; } else { - // Write the payload of the NAL unit. - int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); + int writtenBytes; + if (processSeiNalUnitPayload) { + // Read and write the payload of the SEI NAL unit. + nalBuffer.reset(sampleCurrentNalBytesRemaining); + input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining); + output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining); + writtenBytes = sampleCurrentNalBytesRemaining; + // Unescape and process the SEI NAL unit. + int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit()); + // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte. + nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0); + nalBuffer.setLimit(unescapedLength); + CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer, + cea608TrackOutput); + } else { + // Write the payload of the NAL unit. + writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); + } sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 3759a80fd4..0c990f5747 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -344,7 +344,8 @@ public final class Mp4Extractor implements Extractor, SeekMap { continue; } - Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i)); + Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, + extractorOutput.track(i, track.type)); // Each sample has up to three bytes of overhead for the start code that replaces its length. // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java index 5f41126737..cc3c5de311 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -75,7 +76,7 @@ public class OggExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - TrackOutput trackOutput = output.track(0); + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); output.endTracks(); // TODO: fix the case if sniff() isn't called streamReader.init(output, trackOutput); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index f9957aebe5..7840eafce6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -65,7 +65,7 @@ public final class RawCcExtractor implements Extractor { @Override public void init(ExtractorOutput output) { output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); - trackOutput = output.track(0); + trackOutput = output.track(0, C.TRACK_TYPE_TEXT); output.endTracks(); trackOutput.format(format); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index 52faa8c673..790c036f1d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final ParsableByteArray headerScratchBytes; private final String language; + private String trackFormatId; private TrackOutput output; private int state; @@ -84,7 +85,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { - output = extractorOutput.track(generator.getNextId()); + generator.generateNewId(); + trackFormatId = generator.getFormatId(); + output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override @@ -180,8 +183,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; headerScratchBits.skipBits(40); isEac3 = headerScratchBits.readBits(5) == 16; headerScratchBits.setPosition(headerScratchBits.getPosition() - 45); - format = isEac3 ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, null, language , null) - : Ac3Util.parseAc3SyncframeFormat(headerScratchBits, null, language, null); + format = isEac3 + ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, trackFormatId, language , null) + : Ac3Util.parseAc3SyncframeFormat(headerScratchBits, trackFormatId, language, null); output.format(format); } sampleSize = isEac3 ? Ac3Util.parseEAc3SyncframeSize(headerScratchBits.data) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 47cb217fc7..58318ea78d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -61,6 +61,7 @@ import java.util.Collections; private final ParsableByteArray id3HeaderBuffer; private final String language; + private String formatId; private TrackOutput output; private TrackOutput id3Output; @@ -108,11 +109,14 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); if (exposeId3) { - id3Output = extractorOutput.track(idGenerator.getNextId()); - id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, - Format.NO_VALUE, null)); + idGenerator.generateNewId(); + id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(), + MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); } else { id3Output = new DummyTrackOutput(); } @@ -300,7 +304,7 @@ import java.util.Collections; Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( audioSpecificConfig); - Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, + Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, Collections.singletonList(audioSpecificConfig), null, 0, language); // In this class a sample is an access unit, but the MediaFormat sample rate specifies the diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 31aa88d11a..c798494e42 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -74,10 +74,11 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_H262: return new PesReader(new H262Reader()); case TsExtractor.TS_STREAM_TYPE_H264: - return isSet(FLAG_IGNORE_H264_STREAM) ? null : new PesReader( - new H264Reader(isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS))); + return isSet(FLAG_IGNORE_H264_STREAM) ? null + : new PesReader(new H264Reader(new SeiReader(), isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), + isSet(FLAG_DETECT_ACCESS_UNITS))); case TsExtractor.TS_STREAM_TYPE_H265: - return new PesReader(new H265Reader()); + return new PesReader(new H265Reader(new SeiReader())); case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO: return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM) ? null : new SectionReader(new SpliceInfoSectionReader()); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 9707685295..874de83b68 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final ParsableByteArray headerScratchBytes; private final String language; + private String formatId; private TrackOutput output; private int state; @@ -79,7 +80,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override @@ -165,7 +168,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private void parseHeader() { byte[] frameData = headerScratchBytes.data; if (format == null) { - format = DtsUtil.parseDtsFormat(frameData, null, language, null); + format = DtsUtil.parseDtsFormat(frameData, formatId, language, null); output.format(format); } sampleSize = DtsUtil.getDtsFrameSize(frameData); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index 02ea6d7c4e..ba515d31ed 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -37,6 +37,7 @@ import java.util.Collections; private static final int START_EXTENSION = 0xB5; private static final int START_GROUP = 0xB8; + private String formatId; private TrackOutput output; // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. @@ -78,7 +79,9 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); } @Override @@ -126,7 +129,7 @@ import java.util.Collections; int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { // The csd data is complete, so we can decode and output the media format. - Pair result = parseCsdBuffer(csdBuffer); + Pair result = parseCsdBuffer(csdBuffer, formatId); output.format(result.first); frameDurationUs = result.second; hasOutputFormat = true; @@ -166,10 +169,11 @@ import java.util.Collections; * Parses the {@link Format} and frame duration from a csd buffer. * * @param csdBuffer The csd buffer. + * @param formatId The id for the generated format. May be null. * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or * 0 if the duration could not be determined. */ - private static Pair parseCsdBuffer(CsdBuffer csdBuffer) { + private static Pair parseCsdBuffer(CsdBuffer csdBuffer, String formatId) { byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); int firstByte = csdData[4] & 0xFF; @@ -195,7 +199,7 @@ import java.util.Collections; break; } - Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_MPEG2, null, + Format format = Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_MPEG2, null, Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, Collections.singletonList(csdData), Format.NO_VALUE, pixelWidthHeightRatio, null); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index ed4682d9b9..c1d24b7a33 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -39,6 +39,7 @@ import java.util.List; private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set + private final SeiReader seiReader; private final boolean allowNonIdrKeyframes; private final boolean detectAccessUnits; private final NalUnitTargetBuffer sps; @@ -47,8 +48,8 @@ import java.util.List; private long totalBytesWritten; private final boolean[] prefixFlags; + private String formatId; private TrackOutput output; - private SeiReader seiReader; private SampleReader sampleReader; // State that should not be reset on seek. @@ -61,15 +62,17 @@ import java.util.List; private final ParsableByteArray seiWrapper; /** + * @param seiReader An SEI reader for consuming closed caption channels. * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as * synchronization samples (key-frames). * @param detectAccessUnits Whether to split the input stream into access units (samples) based on * slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs). */ - public H264Reader(boolean allowNonIdrKeyframes, boolean detectAccessUnits) { - prefixFlags = new boolean[3]; + public H264Reader(SeiReader seiReader, boolean allowNonIdrKeyframes, boolean detectAccessUnits) { + this.seiReader = seiReader; this.allowNonIdrKeyframes = allowNonIdrKeyframes; this.detectAccessUnits = detectAccessUnits; + prefixFlags = new boolean[3]; sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); @@ -88,9 +91,11 @@ import java.util.List; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); + seiReader.createTracks(extractorOutput, idGenerator); } @Override @@ -175,7 +180,7 @@ import java.util.List; initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength)); NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength); NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength); - output.format(Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, + output.format(Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, spsData.width, spsData.height, Format.NO_VALUE, initializationData, Format.NO_VALUE, spsData.pixelWidthAspectRatio, null)); hasOutputFormat = true; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index a78169a054..30a5bdc1fd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -44,9 +44,11 @@ import java.util.Collections; private static final int PREFIX_SEI_NUT = 39; private static final int SUFFIX_SEI_NUT = 40; + private final SeiReader seiReader; + + private String formatId; private TrackOutput output; private SampleReader sampleReader; - private SeiReader seiReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -66,7 +68,11 @@ import java.util.Collections; // Scratch variables to avoid allocations. private final ParsableByteArray seiWrapper; - public H265Reader() { + /** + * @param seiReader An SEI reader for consuming closed caption channels. + */ + public H265Reader(SeiReader seiReader) { + this.seiReader = seiReader; prefixFlags = new boolean[3]; vps = new NalUnitTargetBuffer(VPS_NUT, 128); sps = new NalUnitTargetBuffer(SPS_NUT, 128); @@ -90,9 +96,11 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); sampleReader = new SampleReader(output); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); + seiReader.createTracks(extractorOutput, idGenerator); } @Override @@ -183,7 +191,7 @@ import java.util.Collections; sps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding); if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) { - output.format(parseMediaFormat(vps, sps, pps)); + output.format(parseMediaFormat(formatId, vps, sps, pps)); hasOutputFormat = true; } } @@ -205,8 +213,8 @@ import java.util.Collections; } } - private static Format parseMediaFormat(NalUnitTargetBuffer vps, NalUnitTargetBuffer sps, - NalUnitTargetBuffer pps) { + private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps, + NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { // Build codec-specific data. byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength); @@ -311,7 +319,7 @@ import java.util.Collections; } } - return Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H265, null, Format.NO_VALUE, + return Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H265, null, Format.NO_VALUE, Format.NO_VALUE, picWidthInLumaSamples, picHeightInLumaSamples, Format.NO_VALUE, Collections.singletonList(csd), Format.NO_VALUE, pixelWidthHeightRatio, null); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index c19bc9d14e..7d2ecc4e74 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -56,9 +56,10 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, - null)); + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3, + null, Format.NO_VALUE, null)); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index c67e7ad0ab..6301716286 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final MpegAudioHeader header; private final String language; + private String formatId; private TrackOutput output; private int state; @@ -76,7 +77,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override @@ -176,9 +179,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; frameSize = header.frameSize; if (!hasOutputFormat) { frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate; - Format format = Format.createAudioSampleFormat(null, header.mimeType, null, Format.NO_VALUE, - MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, null, null, 0, - language); + Format format = Format.createAudioSampleFormat(formatId, header.mimeType, null, + Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, + null, null, 0, language); output.format(format); hasOutputFormat = true; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index 598394a870..59696b9dea 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -16,12 +16,11 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Log; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorOutput; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; /** * Parses PES packet data and extracts samples. diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index 5c50ca7bf3..883fb8f880 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -23,10 +23,10 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.IOException; /** diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java index 347c401337..d6e6eadf3f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java @@ -16,10 +16,10 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.extractor.ExtractorOutput; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; /** * Reads section data. diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java index 822f5653c4..d217cfcb7a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorOutput; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; /** diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index a2791bcaae..a3f4deffcb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -17,8 +17,10 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.text.cea.Cea608Decoder; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.text.cea.CeaUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -27,49 +29,17 @@ import com.google.android.exoplayer2.util.ParsableByteArray; */ /* package */ final class SeiReader { - private final TrackOutput output; + private TrackOutput output; - public SeiReader(TrackOutput output) { - this.output = output; - output.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null, - Format.NO_VALUE, 0, null, null)); + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); + output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), + MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); } public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { - int b; - while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { - // Parse payload type. - int payloadType = 0; - do { - b = seiBuffer.readUnsignedByte(); - payloadType += b; - } while (b == 0xFF); - // Parse payload size. - int payloadSize = 0; - do { - b = seiBuffer.readUnsignedByte(); - payloadSize += b; - } while (b == 0xFF); - // Process the payload. - if (Cea608Decoder.isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) { - // Ignore country_code (1) + provider_code (2) + user_identifier (4) - // + user_data_type_code (1). - seiBuffer.skipBytes(8); - // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1). - int ccCount = seiBuffer.readUnsignedByte() & 0x1F; - // Ignore em_data (1) - seiBuffer.skipBytes(1); - // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2) - // + cc_data_1 (8) + cc_data_2 (8). - int sampleLength = ccCount * 3; - output.sampleData(seiBuffer, sampleLength); - output.sampleMetadata(pesTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null); - // Ignore trailing information in SEI, if any. - seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3)); - } else { - seiBuffer.skipBytes(payloadSize); - } - } + CeaUtil.consume(pesTimeUs, seiBuffer, output); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java index 121a622362..27838d4c25 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java @@ -18,10 +18,10 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; /** * Parses splice info sections as defined by SCTE35. @@ -36,9 +36,10 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) { this.timestampAdjuster = timestampAdjuster; - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, null, - Format.NO_VALUE, null)); + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); + output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35, + null, Format.NO_VALUE, null)); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index bf5adac500..99f5d0832e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -25,16 +25,19 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * Facilitates the extraction of data from the MPEG-2 TS container format. @@ -79,7 +82,7 @@ public final class TsExtractor implements Extractor { private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT; private final boolean hlsMode; - private final TimestampAdjuster timestampAdjuster; + private final List timestampAdjusters; private final ParsableByteArray tsPacketBuffer; private final ParsableBitArray tsScratch; private final SparseIntArray continuityCounters; @@ -89,18 +92,12 @@ public final class TsExtractor implements Extractor { // Accessed only by the loading thread. private ExtractorOutput output; + private int remainingPmts; private boolean tracksEnded; private TsPayloadReader id3Reader; public TsExtractor() { - this(new TimestampAdjuster(0)); - } - - /** - * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. - */ - public TsExtractor(TimestampAdjuster timestampAdjuster) { - this(timestampAdjuster, new DefaultTsPayloadReaderFactory(), false); + this(new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory(), false); } /** @@ -111,7 +108,12 @@ public final class TsExtractor implements Extractor { */ public TsExtractor(TimestampAdjuster timestampAdjuster, TsPayloadReader.Factory payloadReaderFactory, boolean hlsMode) { - this.timestampAdjuster = timestampAdjuster; + if (hlsMode) { + timestampAdjusters = Collections.singletonList(timestampAdjuster); + } else { + timestampAdjusters = new ArrayList<>(); + timestampAdjusters.add(timestampAdjuster); + } this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); this.hlsMode = hlsMode; tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); @@ -150,7 +152,10 @@ public final class TsExtractor implements Extractor { @Override public void seek(long position, long timeUs) { - timestampAdjuster.reset(); + int timestampAdjustersCount = timestampAdjusters.size(); + for (int i = 0; i < timestampAdjustersCount; i++) { + timestampAdjusters.get(i).reset(); + } tsPacketBuffer.reset(); continuityCounters.clear(); // Elementary stream readers' state should be cleared to get consistent behaviours when seeking. @@ -307,8 +312,12 @@ public final class TsExtractor implements Extractor { } else { int pid = patScratch.readBits(13); tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid))); + remainingPmts++; } } + if (!hlsMode) { + tsPayloadReaders.remove(TS_PAT_PID); + } } } @@ -345,10 +354,21 @@ public final class TsExtractor implements Extractor { // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment. return; } - // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), program_number (16), - // reserved (2), version_number (5), current_next_indicator (1), // section_number (8), + // TimestampAdjuster assignment. + TimestampAdjuster timestampAdjuster; + if (hlsMode || remainingPmts == 1) { + timestampAdjuster = timestampAdjusters.get(0); + } else { + timestampAdjuster = new TimestampAdjuster(timestampAdjusters.get(0).firstSampleTimestampUs); + timestampAdjusters.add(timestampAdjuster); + } + + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12) + sectionData.skipBytes(2); + int programNumber = sectionData.readUnsignedShort(); + // reserved (2), version_number (5), current_next_indicator (1), section_number (8), // last_section_number (8), reserved (3), PCR_PID (13) - sectionData.skipBytes(9); + sectionData.skipBytes(5); // Read program_info_length. sectionData.readBytes(pmtScratch, 2); @@ -364,7 +384,7 @@ public final class TsExtractor implements Extractor { EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]); id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo); id3Reader.init(timestampAdjuster, output, - new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); + new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); } int remainingEntriesLength = sectionData.bytesLeft(); @@ -393,7 +413,8 @@ public final class TsExtractor implements Extractor { } else { reader = payloadReaderFactory.createPayloadReader(streamType, esInfo); if (reader != null) { - reader.init(timestampAdjuster, output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE)); + reader.init(timestampAdjuster, output, + new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE)); } } @@ -404,13 +425,17 @@ public final class TsExtractor implements Extractor { if (hlsMode) { if (!tracksEnded) { output.endTracks(); + remainingPmts = 0; + tracksEnded = true; } } else { - tsPayloadReaders.remove(TS_PAT_PID); tsPayloadReaders.remove(pid); - output.endTracks(); + remainingPmts--; + if (remainingPmts == 0) { + output.endTracks(); + tracksEnded = true; + } } - tracksEnded = true; } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index 304c8c1282..4169e0f3a0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -17,9 +17,9 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import com.google.android.exoplayer2.extractor.ExtractorOutput; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; /** * Parses TS packet payload data. @@ -81,17 +81,63 @@ public interface TsPayloadReader { */ final class TrackIdGenerator { - private final int firstId; - private final int idIncrement; - private int generatedIdCount; + private static final int ID_UNSET = Integer.MIN_VALUE; - public TrackIdGenerator(int firstId, int idIncrement) { - this.firstId = firstId; - this.idIncrement = idIncrement; + private final String formatIdPrefix; + private final int firstTrackId; + private final int trackIdIncrement; + private int trackId; + private String formatId; + + public TrackIdGenerator(int firstTrackId, int trackIdIncrement) { + this(ID_UNSET, firstTrackId, trackIdIncrement); } - public int getNextId() { - return firstId + idIncrement * generatedIdCount++; + public TrackIdGenerator(int programNumber, int firstTrackId, int trackIdIncrement) { + this.formatIdPrefix = programNumber != ID_UNSET ? programNumber + "/" : ""; + this.firstTrackId = firstTrackId; + this.trackIdIncrement = trackIdIncrement; + trackId = ID_UNSET; + } + + /** + * Generates a new set of track and track format ids. Must be called before {@code get*} + * methods. + */ + public void generateNewId() { + trackId = trackId == ID_UNSET ? firstTrackId : trackId + trackIdIncrement; + formatId = formatIdPrefix + trackId; + } + + /** + * Returns the last generated track id. Must be called after the first {@link #generateNewId()} + * call. + * + * @return The last generated track id. + */ + public int getTrackId() { + maybeThrowUninitializedError(); + return trackId; + } + + /** + * Returns the last generated format id, with the format {@code "programNumber/trackId"}. If no + * {@code programNumber} was provided, the {@code trackId} alone is used as format id. Must be + * called after the first {@link #generateNewId()} call. + * + * @return The last generated format id, with the format {@code "programNumber/trackId"}. If no + * {@code programNumber} was provided, the {@code trackId} alone is used as + * format id. + */ + public String getFormatId() { + maybeThrowUninitializedError(); + return formatId; + } + + private void maybeThrowUninitializedError() { + if (trackId == ID_UNSET) { + throw new IllegalStateException("generateNewId() must be called before retrieving ids."); + } } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 3d9f8166ab..cb46aa5519 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -60,7 +60,7 @@ public final class WavExtractor implements Extractor, SeekMap { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = output.track(0); + trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); wavHeader = null; output.endTracks(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 166de37c50..6914b2f52c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.mediacodec; import android.annotation.TargetApi; +import android.graphics.Point; import android.media.MediaCodec; import android.media.MediaCodecInfo.AudioCapabilities; import android.media.MediaCodecInfo.CodecCapabilities; @@ -23,6 +24,7 @@ import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; import android.util.Log; import android.util.Pair; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -141,39 +143,6 @@ public final class MediaCodecInfo { return false; } - /** - * Whether the decoder supports video with a specified width and height. - *

- * Must not be called if the device SDK version is less than 21. - * - * @param width Width in pixels. - * @param height Height in pixels. - * @return Whether the decoder supports video with the given width and height. - */ - @TargetApi(21) - public boolean isVideoSizeSupportedV21(int width, int height) { - if (capabilities == null) { - logNoSupport("size.caps"); - return false; - } - VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); - if (videoCapabilities == null) { - logNoSupport("size.vCaps"); - return false; - } - if (!videoCapabilities.isSizeSupported(width, height)) { - // Capabilities are known to be inaccurately reported for vertical resolutions on some devices - // (b/31387661). If the video is vertical and the capabilities indicate support if the width - // and height are swapped, we assume that the vertical resolution is also supported. - if (width >= height || !videoCapabilities.isSizeSupported(height, width)) { - logNoSupport("size.support, " + width + "x" + height); - return false; - } - logAssumedSupport("size.rotated, " + width + "x" + height); - } - return true; - } - /** * Whether the decoder supports video with a given width, height and frame rate. *

@@ -181,7 +150,8 @@ public final class MediaCodecInfo { * * @param width Width in pixels. * @param height Height in pixels. - * @param frameRate Frame rate in frames per second. + * @param frameRate Optional frame rate in frames per second. Ignored if set to + * {@link Format#NO_VALUE} or any value less than or equal to 0. * @return Whether the decoder supports video with the given width, height and frame rate. */ @TargetApi(21) @@ -195,11 +165,12 @@ public final class MediaCodecInfo { logNoSupport("sizeAndRate.vCaps"); return false; } - if (!videoCapabilities.areSizeAndRateSupported(width, height, frameRate)) { + if (!areSizeAndRateSupported(videoCapabilities, width, height, frameRate)) { // Capabilities are known to be inaccurately reported for vertical resolutions on some devices // (b/31387661). If the video is vertical and the capabilities indicate support if the width // and height are swapped, we assume that the vertical resolution is also supported. - if (width >= height || !videoCapabilities.areSizeAndRateSupported(height, width, frameRate)) { + if (width >= height + || !areSizeAndRateSupported(videoCapabilities, height, width, frameRate)) { logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate); return false; } @@ -208,6 +179,35 @@ public final class MediaCodecInfo { return true; } + /** + * Returns the smallest video size greater than or equal to a specified size that also satisfies + * the {@link MediaCodec}'s width and height alignment requirements. + *

+ * Must not be called if the device SDK version is less than 21. + * + * @param width Width in pixels. + * @param height Height in pixels. + * @return The smallest video size greater than or equal to the specified size that also satisfies + * the {@link MediaCodec}'s width and height alignment requirements, or null if not a video + * codec. + */ + @TargetApi(21) + public Point alignVideoSizeV21(int width, int height) { + if (capabilities == null) { + logNoSupport("align.caps"); + return null; + } + VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); + if (videoCapabilities == null) { + logNoSupport("align.vCaps"); + return null; + } + int widthAlignment = videoCapabilities.getWidthAlignment(); + int heightAlignment = videoCapabilities.getHeightAlignment(); + return new Point(Util.ceilDivide(width, widthAlignment) * widthAlignment, + Util.ceilDivide(height, heightAlignment) * heightAlignment); + } + /** * Whether the decoder supports audio with a given sample rate. *

@@ -279,6 +279,14 @@ public final class MediaCodecInfo { return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback); } + @TargetApi(21) + private static boolean areSizeAndRateSupported(VideoCapabilities capabilities, int width, + int height, double frameRate) { + return frameRate == Format.NO_VALUE || frameRate <= 0 + ? capabilities.isSizeSupported(width, height) + : capabilities.areSizeAndRateSupported(width, height, frameRate); + } + private static boolean isTunneling(CodecCapabilities capabilities) { return Util.SDK_INT >= 21 && isTunnelingV21(capabilities); } diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 7e8b83b84c..9baf974b37 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -183,6 +183,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean codecNeedsAdaptationWorkaround; private boolean codecNeedsEosPropagationWorkaround; private boolean codecNeedsEosFlushWorkaround; + private boolean codecNeedsEosOutputExceptionWorkaround; private boolean codecNeedsMonoChannelCountWorkaround; private boolean codecNeedsAdaptationWorkaroundBuffer; private boolean shouldSkipAdaptationWorkaroundOutputBuffer; @@ -201,6 +202,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; + private boolean waitingForFirstSyncFrame; protected DecoderCounters decoderCounters; @@ -276,11 +278,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Configures a newly created {@link MediaCodec}. * + * @param codecInfo Information about the {@link MediaCodec} being configured. * @param codec The {@link MediaCodec} to configure. * @param format The format for which the codec is being configured. * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption. + * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - protected abstract void configureCodec(MediaCodec codec, Format format, MediaCrypto crypto); + protected abstract void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, + MediaCrypto crypto) throws DecoderQueryException; @SuppressWarnings("deprecation") protected final void maybeInitCodec() throws ExoPlaybackException { @@ -338,6 +343,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); + codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format); try { long codecInitializingTimestamp = SystemClock.elapsedRealtime(); @@ -345,7 +351,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codec = MediaCodec.createByCodecName(codecName); TraceUtil.endSection(); TraceUtil.beginSection("configureCodec"); - configureCodec(codec, format, mediaCrypto); + configureCodec(decoderInfo, codec, format, mediaCrypto); TraceUtil.endSection(); TraceUtil.beginSection("startCodec"); codec.start(); @@ -363,6 +369,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) : C.TIME_UNSET; inputIndex = C.INDEX_UNSET; outputIndex = C.INDEX_UNSET; + waitingForFirstSyncFrame = true; decoderCounters.decoderInitCount++; } @@ -501,13 +508,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecHotswapDeadlineMs = C.TIME_UNSET; inputIndex = C.INDEX_UNSET; outputIndex = C.INDEX_UNSET; + waitingForFirstSyncFrame = true; waitingForKeys = false; shouldSkipOutputBuffer = false; decodeOnlyPresentationTimestamps.clear(); codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; if (codecNeedsFlushWorkaround || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { - // Workaround framework bugs. See [Internal: b/8347958, b/8578467, b/8543366, b/23361053]. releaseCodec(); maybeInitCodec(); } else if (codecReinitializationState != REINITIALIZATION_STATE_NONE) { @@ -630,6 +637,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } return false; } + if (waitingForFirstSyncFrame && !buffer.isKeyFrame()) { + buffer.clear(); + if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { + // The buffer we just cleared contained reconfiguration data. We need to re-write this + // data into a subsequent buffer (if there is one). + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + } + return true; + } + waitingForFirstSyncFrame = false; boolean bufferEncrypted = buffer.isEncrypted(); waitingForKeys = shouldWaitForKeys(bufferEncrypted); if (waitingForKeys) { @@ -763,8 +780,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @param codec The {@link MediaCodec} instance. * @param outputFormat The new output format. + * @throws ExoPlaybackException Thrown if an error occurs handling the new output format. */ - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + throws ExoPlaybackException { // Do nothing. } @@ -849,7 +868,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputIndex < 0) { - outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); + if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { + try { + outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, + getDequeueOutputBufferTimeoutUs()); + } catch (IllegalStateException e) { + processEndOfStream(); + if (outputStreamEnded) { + // Release the codec, as it's in an error state. + releaseCodec(); + } + return false; + } + } else { + outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, + getDequeueOutputBufferTimeoutUs()); + } if (outputIndex >= 0) { // We've dequeued a buffer. if (shouldSkipAdaptationWorkaroundOutputBuffer) { @@ -888,9 +922,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } - if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex], - outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer)) { + boolean processedOutputBuffer; + if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { + try { + processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec, + outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer); + } catch (IllegalStateException e) { + processEndOfStream(); + if (outputStreamEnded) { + // Release the codec, as it's in an error state. + releaseCodec(); + } + return false; + } + } else { + processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec, + outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer); + } + + if (processedOutputBuffer) { onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs); outputIndex = C.INDEX_UNSET; return true; @@ -902,7 +954,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Processes a new output format. */ - private void processOutputFormat() { + private void processOutputFormat() throws ExoPlaybackException { MediaFormat format = codec.getOutputFormat(); if (codecNeedsAdaptationWorkaround && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT @@ -992,6 +1044,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

* If true is returned, the renderer will work around the issue by releasing the decoder and * instantiating a new one rather than flushing the current instance. + *

+ * See [Internal: b/8347958, b/8543366]. * * @param name The name of the decoder. * @return True if the decoder is known to fail when flushed. @@ -1061,6 +1115,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

* If true is returned, the renderer will work around the issue by instantiating a new decoder * when this case occurs. + *

+ * See [Internal: b/8578467, b/23361053]. * * @param name The name of the decoder. * @return True if the decoder is known to behave incorrectly if flushed after receiving an input @@ -1073,6 +1129,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer { || "OMX.amlogic.avc.decoder.awesome.secure".equals(name))); } + /** + * Returns whether the decoder may throw an {@link IllegalStateException} from + * {@link MediaCodec#dequeueOutputBuffer(MediaCodec.BufferInfo, long)} or + * {@link MediaCodec#releaseOutputBuffer(int, boolean)} after receiving an input + * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. + *

+ * See [Internal: b/17933838]. + * + * @param name The name of the decoder. + * @return True if the decoder may throw an exception after receiving an end-of-stream buffer. + */ + private static boolean codecNeedsEosOutputExceptionWorkaround(String name) { + return Util.SDK_INT == 21 && "OMX.google.aac.decoder".equals(name); + } + /** * Returns whether the decoder is known to set the number of audio channels in the output format * to 2 for the given input format, whilst only actually outputting a single channel. diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java index f75a1b46a4..beb4cb9b88 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java @@ -26,7 +26,6 @@ public final class PrivateCommand extends SpliceCommand { public final long ptsAdjustment; public final long identifier; - public final byte[] commandBytes; private PrivateCommand(long identifier, byte[] commandBytes, long ptsAdjustment) { diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java index 6e373a45e7..58c23d253a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; import java.nio.ByteBuffer; /** @@ -37,6 +38,8 @@ public final class SpliceInfoDecoder implements MetadataDecoder { private final ParsableByteArray sectionData; private final ParsableBitArray sectionHeader; + private TimestampAdjuster timestampAdjuster; + public SpliceInfoDecoder() { sectionData = new ParsableByteArray(); sectionHeader = new ParsableBitArray(); @@ -44,6 +47,13 @@ public final class SpliceInfoDecoder implements MetadataDecoder { @Override public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException { + // Internal timestamps adjustment. + if (timestampAdjuster == null + || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) { + timestampAdjuster = new TimestampAdjuster(inputBuffer.timeUs); + timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs); + } + ByteBuffer buffer = inputBuffer.data; byte[] data = buffer.array(); int size = buffer.limit(); @@ -69,10 +79,11 @@ public final class SpliceInfoDecoder implements MetadataDecoder { command = SpliceScheduleCommand.parseFromSection(sectionData); break; case TYPE_SPLICE_INSERT: - command = SpliceInsertCommand.parseFromSection(sectionData, ptsAdjustment); + command = SpliceInsertCommand.parseFromSection(sectionData, ptsAdjustment, + timestampAdjuster); break; case TYPE_TIME_SIGNAL: - command = TimeSignalCommand.parseFromSection(sectionData, ptsAdjustment); + command = TimeSignalCommand.parseFromSection(sectionData, ptsAdjustment, timestampAdjuster); break; case TYPE_PRIVATE_COMMAND: command = PrivateCommand.parseFromSection(sectionData, spliceCommandLength, ptsAdjustment); diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java index 1e025aeb35..7ce8b47e2a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java @@ -19,6 +19,7 @@ import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -34,6 +35,7 @@ public final class SpliceInsertCommand extends SpliceCommand { public final boolean programSpliceFlag; public final boolean spliceImmediateFlag; public final long programSplicePts; + public final long programSplicePlaybackPositionUs; public final List componentSpliceList; public final boolean autoReturn; public final long breakDuration; @@ -43,14 +45,16 @@ public final class SpliceInsertCommand extends SpliceCommand { private SpliceInsertCommand(long spliceEventId, boolean spliceEventCancelIndicator, boolean outOfNetworkIndicator, boolean programSpliceFlag, boolean spliceImmediateFlag, - long programSplicePts, List componentSpliceList, boolean autoReturn, - long breakDuration, int uniqueProgramId, int availNum, int availsExpected) { + long programSplicePts, long programSplicePlaybackPositionUs, + List componentSpliceList, boolean autoReturn, long breakDuration, + int uniqueProgramId, int availNum, int availsExpected) { this.spliceEventId = spliceEventId; this.spliceEventCancelIndicator = spliceEventCancelIndicator; this.outOfNetworkIndicator = outOfNetworkIndicator; this.programSpliceFlag = programSpliceFlag; this.spliceImmediateFlag = spliceImmediateFlag; this.programSplicePts = programSplicePts; + this.programSplicePlaybackPositionUs = programSplicePlaybackPositionUs; this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); this.autoReturn = autoReturn; this.breakDuration = breakDuration; @@ -66,6 +70,7 @@ public final class SpliceInsertCommand extends SpliceCommand { programSpliceFlag = in.readByte() == 1; spliceImmediateFlag = in.readByte() == 1; programSplicePts = in.readLong(); + programSplicePlaybackPositionUs = in.readLong(); int componentSpliceListSize = in.readInt(); List componentSpliceList = new ArrayList<>(componentSpliceListSize); for (int i = 0; i < componentSpliceListSize; i++) { @@ -80,7 +85,7 @@ public final class SpliceInsertCommand extends SpliceCommand { } /* package */ static SpliceInsertCommand parseFromSection(ParsableByteArray sectionData, - long ptsAdjustment) { + long ptsAdjustment, TimestampAdjuster timestampAdjuster) { long spliceEventId = sectionData.readUnsignedInt(); // splice_event_cancel_indicator(1), reserved(7). boolean spliceEventCancelIndicator = (sectionData.readUnsignedByte() & 0x80) != 0; @@ -88,7 +93,7 @@ public final class SpliceInsertCommand extends SpliceCommand { boolean programSpliceFlag = false; boolean spliceImmediateFlag = false; long programSplicePts = C.TIME_UNSET; - ArrayList componentSplices = new ArrayList<>(); + List componentSplices = Collections.emptyList(); int uniqueProgramId = 0; int availNum = 0; int availsExpected = 0; @@ -112,7 +117,8 @@ public final class SpliceInsertCommand extends SpliceCommand { if (!spliceImmediateFlag) { componentSplicePts = TimeSignalCommand.parseSpliceTime(sectionData, ptsAdjustment); } - componentSplices.add(new ComponentSplice(componentTag, componentSplicePts)); + componentSplices.add(new ComponentSplice(componentTag, componentSplicePts, + timestampAdjuster.adjustTsTimestamp(componentSplicePts))); } } if (durationFlag) { @@ -125,7 +131,8 @@ public final class SpliceInsertCommand extends SpliceCommand { availsExpected = sectionData.readUnsignedByte(); } return new SpliceInsertCommand(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator, - programSpliceFlag, spliceImmediateFlag, programSplicePts, componentSplices, autoReturn, + programSpliceFlag, spliceImmediateFlag, programSplicePts, + timestampAdjuster.adjustTsTimestamp(programSplicePts), componentSplices, autoReturn, duration, uniqueProgramId, availNum, availsExpected); } @@ -136,19 +143,23 @@ public final class SpliceInsertCommand extends SpliceCommand { public final int componentTag; public final long componentSplicePts; + public final long componentSplicePlaybackPositionUs; - private ComponentSplice(int componentTag, long componentSplicePts) { + private ComponentSplice(int componentTag, long componentSplicePts, + long componentSplicePlaybackPositionUs) { this.componentTag = componentTag; this.componentSplicePts = componentSplicePts; + this.componentSplicePlaybackPositionUs = componentSplicePlaybackPositionUs; } public void writeToParcel(Parcel dest) { dest.writeInt(componentTag); dest.writeLong(componentSplicePts); + dest.writeLong(componentSplicePlaybackPositionUs); } public static ComponentSplice createFromParcel(Parcel in) { - return new ComponentSplice(in.readInt(), in.readLong()); + return new ComponentSplice(in.readInt(), in.readLong(), in.readLong()); } } @@ -163,6 +174,7 @@ public final class SpliceInsertCommand extends SpliceCommand { dest.writeByte((byte) (programSpliceFlag ? 1 : 0)); dest.writeByte((byte) (spliceImmediateFlag ? 1 : 0)); dest.writeLong(programSplicePts); + dest.writeLong(programSplicePlaybackPositionUs); int componentSpliceListSize = componentSpliceList.size(); dest.writeInt(componentSpliceListSize); for (int i = 0; i < componentSpliceListSize; i++) { diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java index c31f4dedc8..f756b72d6d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.metadata.scte35; import android.os.Parcel; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; /** * Represents a time signal command as defined in SCTE35, Section 9.3.4. @@ -25,14 +26,18 @@ import com.google.android.exoplayer2.util.ParsableByteArray; public final class TimeSignalCommand extends SpliceCommand { public final long ptsTime; + public final long playbackPositionUs; - private TimeSignalCommand(long ptsTime) { + private TimeSignalCommand(long ptsTime, long playbackPositionUs) { this.ptsTime = ptsTime; + this.playbackPositionUs = playbackPositionUs; } /* package */ static TimeSignalCommand parseFromSection(ParsableByteArray sectionData, - long ptsAdjustment) { - return new TimeSignalCommand(parseSpliceTime(sectionData, ptsAdjustment)); + long ptsAdjustment, TimestampAdjuster timestampAdjuster) { + long ptsTime = parseSpliceTime(sectionData, ptsAdjustment); + long playbackPositionUs = timestampAdjuster.adjustTsTimestamp(ptsTime); + return new TimeSignalCommand(ptsTime, playbackPositionUs); } /** @@ -61,6 +66,7 @@ public final class TimeSignalCommand extends SpliceCommand { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(ptsTime); + dest.writeLong(playbackPositionUs); } public static final Creator CREATOR = @@ -68,7 +74,7 @@ public final class TimeSignalCommand extends SpliceCommand { @Override public TimeSignalCommand createFromParcel(Parcel in) { - return new TimeSignalCommand(in.readLong()); + return new TimeSignalCommand(in.readLong(), in.readLong()); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index bc0a3f1cf8..dc189058a6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -381,7 +381,7 @@ import java.io.IOException; // ExtractorOutput implementation. Called by the loading thread. @Override - public TrackOutput track(int id) { + public TrackOutput track(int id, int type) { DefaultTrackOutput trackOutput = sampleQueues.get(id); if (trackOutput == null) { trackOutput = new DefaultTrackOutput(allocator); @@ -519,7 +519,7 @@ import java.io.IOException; } private boolean isLoadableExceptionFatal(IOException e) { - return e instanceof ExtractorMediaSource.UnrecognizedInputFormatException; + return e instanceof UnrecognizedInputFormatException; } private void notifyLoadError(final IOException error) { @@ -625,7 +625,7 @@ import java.io.IOException; length += position; } input = new DefaultExtractorInput(dataSource, position, length); - Extractor extractor = extractorHolder.selectExtractor(input); + Extractor extractor = extractorHolder.selectExtractor(input, dataSource.getUri()); if (pendingExtractorSeek) { extractor.seek(position, seekTimeUs); pendingExtractorSeek = false; @@ -677,13 +677,13 @@ import java.io.IOException; * later calls. * * @param input The {@link ExtractorInput} from which data should be read. + * @param uri The {@link Uri} of the data. * @return An initialized extractor for reading {@code input}. - * @throws ExtractorMediaSource.UnrecognizedInputFormatException Thrown if the input format - * could not be detected. + * @throws UnrecognizedInputFormatException Thrown if the input format could not be detected. * @throws IOException Thrown if the input could not be read. * @throws InterruptedException Thrown if the thread was interrupted. */ - public Extractor selectExtractor(ExtractorInput input) + public Extractor selectExtractor(ExtractorInput input, Uri uri) throws IOException, InterruptedException { if (extractor != null) { return extractor; @@ -701,7 +701,8 @@ import java.io.IOException; } } if (extractor == null) { - throw new ExtractorMediaSource.UnrecognizedInputFormatException(extractors); + throw new UnrecognizedInputFormatException("None of the available extractors (" + + Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream.", uri); } extractor.init(extractorOutput); return extractor; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 7b571bc289..c560616aae 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -19,7 +19,6 @@ import android.net.Uri; import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; @@ -27,7 +26,6 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -57,18 +55,6 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List } - /** - * Thrown if the input format could not recognized. - */ - public static final class UnrecognizedInputFormatException extends ParserException { - - public UnrecognizedInputFormatException(Extractor[] extractors) { - super("None of the available extractors (" - + Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream."); - } - - } - /** * The default minimum number of times to retry loading prior to failing for on-demand streams. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java b/library/src/main/java/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java new file mode 100644 index 0000000000..508bf0e365 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 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.exoplayer2.source; + +import android.net.Uri; +import com.google.android.exoplayer2.ParserException; + +/** + * Thrown if the input format was not recognized. + */ +public class UnrecognizedInputFormatException extends ParserException { + + /** + * The {@link Uri} from which the unrecognized data was read. + */ + public final Uri uri; + + /** + * @param message The detail message for the exception. + * @param uri The {@link Uri} from which the unrecognized data was read. + */ + public UnrecognizedInputFormatException(String message, Uri uri) { + super(message); + this.uri = uri; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index 2623d31cef..2a641b80a6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer2.source.chunk; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -30,33 +30,19 @@ import java.io.IOException; /** * An {@link Extractor} wrapper for loading chunks containing a single track. *

- * The wrapper allows switching of the {@link SeekMapOutput} and {@link TrackOutput} that receive - * parsed data. + * The wrapper allows switching of the {@link TrackOutput} that receives parsed data. */ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput { - /** - * Receives {@link SeekMap}s extracted by the wrapped {@link Extractor}. - */ - public interface SeekMapOutput { - - /** - * @see ExtractorOutput#seekMap(SeekMap) - */ - void seekMap(SeekMap seekMap); - - } - public final Extractor extractor; private final Format manifestFormat; - private final boolean preferManifestDrmInitData; - private final boolean resendFormatOnInit; + private final int primaryTrackType; private boolean extractorInitialized; - private SeekMapOutput seekMapOutput; private TrackOutput trackOutput; - private Format sentFormat; + private SeekMap seekMap; + private Format sampleFormat; // Accessed only on the loader thread. private boolean seenTrack; @@ -66,36 +52,44 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput * @param extractor The extractor to wrap. * @param manifestFormat A manifest defined {@link Format} whose data should be merged into any * sample {@link Format} output from the {@link Extractor}. - * @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat} - * should be preferred when the sample and manifest {@link Format}s are merged. - * @param resendFormatOnInit Whether the extractor should resend the previous {@link Format} when - * it is initialized via {@link #init(SeekMapOutput, TrackOutput)}. + * @param primaryTrackType The type of the primary track. Typically one of the {@link C} + * {@code TRACK_TYPE_*} constants. */ - public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, - boolean preferManifestDrmInitData, boolean resendFormatOnInit) { + public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, int primaryTrackType) { this.extractor = extractor; this.manifestFormat = manifestFormat; - this.preferManifestDrmInitData = preferManifestDrmInitData; - this.resendFormatOnInit = resendFormatOnInit; + this.primaryTrackType = primaryTrackType; } /** - * Initializes the extractor to output to the provided {@link SeekMapOutput} and - * {@link TrackOutput} instances, and configures it to receive data from a new chunk. + * Returns the {@link SeekMap} most recently output by the extractor, or null. + */ + public SeekMap getSeekMap() { + return seekMap; + } + + /** + * Returns the sample {@link Format} most recently output by the extractor, or null. + */ + public Format getSampleFormat() { + return sampleFormat; + } + + /** + * Initializes the extractor to output to the provided {@link TrackOutput}, and configures it to + * receive data from a new chunk. * - * @param seekMapOutput The {@link SeekMapOutput} that will receive extracted {@link SeekMap}s. * @param trackOutput The {@link TrackOutput} that will receive sample data. */ - public void init(SeekMapOutput seekMapOutput, TrackOutput trackOutput) { - this.seekMapOutput = seekMapOutput; + public void init(TrackOutput trackOutput) { this.trackOutput = trackOutput; if (!extractorInitialized) { extractor.init(this); extractorInitialized = true; } else { extractor.seek(0, 0); - if (resendFormatOnInit && sentFormat != null) { - trackOutput.format(sentFormat); + if (sampleFormat != null && trackOutput != null) { + trackOutput.format(sampleFormat); } } } @@ -103,7 +97,10 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput // ExtractorOutput implementation. @Override - public TrackOutput track(int id) { + public TrackOutput track(int id, int type) { + if (primaryTrackType != C.TRACK_TYPE_UNKNOWN && primaryTrackType != type) { + return new DummyTrackOutput(); + } Assertions.checkState(!seenTrack || seenTrackId == id); seenTrack = true; seenTrackId = id; @@ -117,15 +114,17 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput @Override public void seekMap(SeekMap seekMap) { - seekMapOutput.seekMap(seekMap); + this.seekMap = seekMap; } // TrackOutput implementation. @Override public void format(Format format) { - sentFormat = format.copyWithManifestFormatInfo(manifestFormat, preferManifestDrmInitData); - trackOutput.format(sentFormat); + sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); + if (trackOutput != null) { + trackOutput.format(sampleFormat); + } } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index 060e6130cf..44fd45d5ff 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -20,8 +20,6 @@ import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.DefaultTrackOutput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; @@ -31,12 +29,11 @@ import java.io.IOException; /** * A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data. */ -public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput { +public class ContainerMediaChunk extends BaseMediaChunk { private final int chunkCount; private final long sampleOffsetUs; private final ChunkExtractorWrapper extractorWrapper; - private final Format sampleFormat; private volatile int bytesLoaded; private volatile boolean loadCanceled; @@ -56,19 +53,15 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput * underlying media are being merged into a single load. * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor. * @param extractorWrapper A wrapped extractor to use for parsing the data. - * @param sampleFormat The {@link Format} of the samples in the chunk, if known. May be null if - * the data is known to define its own sample format. */ public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper, - Format sampleFormat) { + int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper) { super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); this.chunkCount = chunkCount; this.sampleOffsetUs = sampleOffsetUs; this.extractorWrapper = extractorWrapper; - this.sampleFormat = sampleFormat; } @Override @@ -86,13 +79,6 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput return bytesLoaded; } - // SeekMapOutput implementation. - - @Override - public final void seekMap(SeekMap seekMap) { - // Do nothing. - } - // Loadable implementation. @Override @@ -116,8 +102,8 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput if (bytesLoaded == 0) { // Set the target to ourselves. DefaultTrackOutput trackOutput = getTrackOutput(); - trackOutput.formatWithOffset(sampleFormat, sampleOffsetUs); - extractorWrapper.init(this, trackOutput); + trackOutput.setSampleOffsetUs(sampleOffsetUs); + extractorWrapper.init(trackOutput); } // Load and decode the sample data. try { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java index c8c3389830..69474aa150 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java @@ -20,30 +20,19 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track. */ -public final class InitializationChunk extends Chunk implements SeekMapOutput, - TrackOutput { +public final class InitializationChunk extends Chunk { private final ChunkExtractorWrapper extractorWrapper; - // Initialization results. Set by the loader thread and read by any thread that knows loading - // has completed. These variables do not need to be volatile, since a memory barrier must occur - // for the reading thread to know that loading has completed. - private Format sampleFormat; - private SeekMap seekMap; - private volatile int bytesLoaded; private volatile boolean loadCanceled; @@ -68,55 +57,6 @@ public final class InitializationChunk extends Chunk implements SeekMapOutput, return bytesLoaded; } - /** - * Returns a {@link Format} parsed from the chunk, or null. - *

- * Should be called after loading has completed. - */ - public Format getSampleFormat() { - return sampleFormat; - } - - /** - * Returns a {@link SeekMap} parsed from the chunk, or null. - *

- * Should be called after loading has completed. - */ - public SeekMap getSeekMap() { - return seekMap; - } - - // SeekMapOutput implementation. - - @Override - public void seekMap(SeekMap seekMap) { - this.seekMap = seekMap; - } - - // TrackOutput implementation. - - @Override - public void format(Format format) { - this.sampleFormat = format; - } - - @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - - @Override - public void sampleData(ParsableByteArray data, int length) { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - - @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - // Loadable implementation. @Override @@ -138,8 +78,7 @@ public final class InitializationChunk extends Chunk implements SeekMapOutput, ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); if (bytesLoaded == 0) { - // Set the target to ourselves. - extractorWrapper.init(this, this); + extractorWrapper.init(null); } // Load and decode the initialization data. try { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index d7be74535e..1afce6f2ee 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -88,7 +88,8 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { } ExtractorInput extractorInput = new DefaultExtractorInput(dataSource, bytesLoaded, length); DefaultTrackOutput trackOutput = getTrackOutput(); - trackOutput.formatWithOffset(sampleFormat, 0); + trackOutput.setSampleOffsetUs(0); + trackOutput.format(sampleFormat); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 99845c057e..eec99521f1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -572,22 +572,28 @@ public final class DashMediaSource implements MediaSource { long availableStartTimeUs = 0; long availableEndTimeUs = Long.MAX_VALUE; boolean isIndexExplicit = false; + boolean seenEmptyIndex = false; for (int i = 0; i < adaptationSetCount; i++) { DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex(); if (index == null) { return new PeriodSeekInfo(true, 0, durationUs); } - int firstSegmentNum = index.getFirstSegmentNum(); - int lastSegmentNum = index.getLastSegmentNum(durationUs); isIndexExplicit |= index.isExplicit(); - long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); - availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); - if (lastSegmentNum != DashSegmentIndex.INDEX_UNBOUNDED) { - long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) - + index.getDurationUs(lastSegmentNum, durationUs); - availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); - } else { - // The available end time is unmodified, because this index is unbounded. + int segmentCount = index.getSegmentCount(durationUs); + if (segmentCount == 0) { + seenEmptyIndex = true; + availableStartTimeUs = 0; + availableEndTimeUs = 0; + } else if (!seenEmptyIndex) { + int firstSegmentNum = index.getFirstSegmentNum(); + long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); + availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); + if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED) { + int lastSegmentNum = firstSegmentNum + segmentCount - 1; + long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) + + index.getDurationUs(lastSegmentNum, durationUs); + availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); + } } } return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs); @@ -704,8 +710,8 @@ public final class DashMediaSource implements MediaSource { // not correspond to the start of a segment in both, but this is an edge case. DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex) .representations.get(0).getIndex(); - if (snapIndex == null) { - // Video adaptation set does not include an index for snapping. + if (snapIndex == null || snapIndex.getSegmentCount(periodDurationUs) == 0) { + // Video adaptation set does not include a non-empty index for snapping. return windowDefaultStartPositionUs; } int segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java index d002831c4f..2ddc7f4f80 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java @@ -26,12 +26,10 @@ public interface DashSegmentIndex { int INDEX_UNBOUNDED = -1; /** - * Returns the segment number of the segment containing a given media time. - *

- * If the given media time is outside the range of the index, then the returned segment number is - * clamped to {@link #getFirstSegmentNum()} (if the given media time is earlier the start of the - * first segment) or {@link #getLastSegmentNum(long)} (if the given media time is later then the - * end of the last segment). + * Returns {@code getFirstSegmentNum()} if the index has no segments or if the given media time is + * earlier than the start of the first segment. Returns {@code getFirstSegmentNum() + + * getSegmentCount() - 1} if the given media time is later than the end of the last segment. + * Otherwise, returns the segment number of the segment containing the given media time. * * @param timeUs The time in microseconds. * @param periodDurationUs The duration of the enclosing period in microseconds, or @@ -74,7 +72,7 @@ public interface DashSegmentIndex { int getFirstSegmentNum(); /** - * Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}. + * Returns the number of segments in the index, or {@link #INDEX_UNBOUNDED}. *

* An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a * SegmentTimeline element, and if the period duration is not yet known. In this case the caller @@ -82,9 +80,9 @@ public interface DashSegmentIndex { * * @param periodDurationUs The duration of the enclosing period in microseconds, or * {@link C#TIME_UNSET} if the period's duration is not yet known. - * @return The segment number of the last segment, or {@link #INDEX_UNBOUNDED}. + * @return The number of segments in the index, or {@link #INDEX_UNBOUNDED}. */ - int getLastSegmentNum(long periodDurationUs); + int getSegmentCount(long periodDurationUs); /** * Returns true if segments are defined explicitly by the index. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java index 56ea626120..40f3448f6a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java @@ -39,8 +39,8 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri; } @Override - public int getLastSegmentNum(long periodDurationUs) { - return chunkIndex.length - 1; + public int getSegmentCount(long periodDurationUs) { + return chunkIndex.length; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 74d53d3e32..4548bc75f8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; import com.google.android.exoplayer2.source.chunk.InitializationChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; @@ -119,11 +120,13 @@ public class DefaultDashChunkSource implements DashChunkSource { this.maxSegmentsPerLoad = maxSegmentsPerLoad; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - List representations = getRepresentations(); + AdaptationSet adaptationSet = getAdaptationSet(); + List representations = adaptationSet.representations; representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); - representationHolders[i] = new RepresentationHolder(periodDurationUs, representation); + representationHolders[i] = new RepresentationHolder(periodDurationUs, representation, + adaptationSet.type); } } @@ -133,7 +136,7 @@ public class DefaultDashChunkSource implements DashChunkSource { manifest = newManifest; periodIndex = newPeriodIndex; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - List representations = getRepresentations(); + List representations = getAdaptationSet().representations; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); representationHolders[i].updateRepresentation(periodDurationUs, representation); @@ -176,8 +179,7 @@ public class DefaultDashChunkSource implements DashChunkSource { RangedUri pendingInitializationUri = null; RangedUri pendingIndexUri = null; - Format sampleFormat = representationHolder.sampleFormat; - if (sampleFormat == null) { + if (representationHolder.extractorWrapper.getSampleFormat() == null) { pendingInitializationUri = selectedRepresentation.getInitializationUri(); } if (segmentIndex == null) { @@ -192,10 +194,16 @@ public class DefaultDashChunkSource implements DashChunkSource { } long nowUs = getNowUnixTimeUs(); + int availableSegmentCount = representationHolder.getSegmentCount(); + if (availableSegmentCount == 0) { + // The index doesn't define any segments. + out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1); + return; + } + int firstAvailableSegmentNum = representationHolder.getFirstSegmentNum(); - int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; - if (indexUnbounded) { + int lastAvailableSegmentNum; + if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { // The index is itself unbounded. We need to use the current time to calculate the range of // available segments. long liveEdgeTimeUs = nowUs - manifest.availabilityStartTime * 1000; @@ -209,6 +217,8 @@ public class DefaultDashChunkSource implements DashChunkSource { // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the // index of the last completed segment. lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs) - 1; + } else { + lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1; } int segmentNum; @@ -233,8 +243,8 @@ public class DefaultDashChunkSource implements DashChunkSource { int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(), - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), sampleFormat, - segmentNum, maxSegmentCount); + trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segmentNum, + maxSegmentCount); } @Override @@ -243,15 +253,11 @@ public class DefaultDashChunkSource implements DashChunkSource { InitializationChunk initializationChunk = (InitializationChunk) chunk; RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(initializationChunk.trackFormat)]; - Format sampleFormat = initializationChunk.getSampleFormat(); - if (sampleFormat != null) { - representationHolder.setSampleFormat(sampleFormat); - } // The null check avoids overwriting an index obtained from the manifest with one obtained // from the stream. If the manifest defines an index then the stream shouldn't, but in cases // where it does we should ignore it. if (representationHolder.segmentIndex == null) { - SeekMap seekMap = initializationChunk.getSeekMap(); + SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap(); if (seekMap != null) { representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap); } @@ -270,10 +276,13 @@ public class DefaultDashChunkSource implements DashChunkSource { && ((InvalidResponseCodeException) e).responseCode == 404) { RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(chunk.trackFormat)]; - int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { - missingLastSegment = true; - return true; + int segmentCount = representationHolder.getSegmentCount(); + if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) { + int lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1; + if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { + missingLastSegment = true; + return true; + } } } // Blacklist if appropriate. @@ -283,8 +292,8 @@ public class DefaultDashChunkSource implements DashChunkSource { // Private methods. - private List getRepresentations() { - return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex).representations; + private AdaptationSet getAdaptationSet() { + return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex); } private long getNowUnixTimeUs() { @@ -318,7 +327,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private static Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, Format sampleFormat, int firstSegmentNum, int maxSegmentCount) { + Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) { Representation representation = representationHolder.representation; long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum); @@ -347,7 +356,7 @@ public class DefaultDashChunkSource implements DashChunkSource { long sampleOffsetUs = -representation.presentationTimeOffsetUs; return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, segmentCount, - sampleOffsetUs, representationHolder.extractorWrapper, sampleFormat); + sampleOffsetUs, representationHolder.extractorWrapper); } } @@ -355,45 +364,40 @@ public class DefaultDashChunkSource implements DashChunkSource { protected static final class RepresentationHolder { + public final int trackType; public final ChunkExtractorWrapper extractorWrapper; public Representation representation; public DashSegmentIndex segmentIndex; - public Format sampleFormat; private long periodDurationUs; private int segmentNumShift; - public RepresentationHolder(long periodDurationUs, Representation representation) { + public RepresentationHolder(long periodDurationUs, Representation representation, + int trackType) { this.periodDurationUs = periodDurationUs; this.representation = representation; + this.trackType = trackType; String containerMimeType = representation.format.containerMimeType; if (mimeTypeIsRawText(containerMimeType)) { extractorWrapper = null; } else { - boolean resendFormatOnInit = false; Extractor extractor; if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { extractor = new RawCcExtractor(representation.format); - resendFormatOnInit = true; } else if (mimeTypeIsWebm(containerMimeType)) { extractor = new MatroskaExtractor(); } else { - extractor = new FragmentedMp4Extractor(); + extractor = new FragmentedMp4Extractor(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK + | FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK); } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - extractorWrapper = new ChunkExtractorWrapper(extractor, - representation.format, true /* preferManifestDrmInitData */, - resendFormatOnInit); + extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format, trackType); } segmentIndex = representation.getIndex(); } - public void setSampleFormat(Format sampleFormat) { - this.sampleFormat = sampleFormat; - } - public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) throws BehindLiveWindowException{ DashSegmentIndex oldIndex = representation.getIndex(); @@ -412,15 +416,20 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(periodDurationUs); + int oldIndexSegmentCount = oldIndex.getSegmentCount(periodDurationUs); + if (oldIndexSegmentCount == 0) { + // Segment numbers cannot shift if the old index was empty. + return; + } + + int oldIndexLastSegmentNum = oldIndex.getFirstSegmentNum() + oldIndexSegmentCount - 1; long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs); int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); if (oldIndexEndTimeUs == newIndexStartTimeUs) { // The new index continues where the old one ended, with no overlap. - segmentNumShift += oldIndex.getLastSegmentNum(periodDurationUs) + 1 - - newIndexFirstSegmentNum; + segmentNumShift += oldIndexLastSegmentNum + 1 - newIndexFirstSegmentNum; } else if (oldIndexEndTimeUs < newIndexStartTimeUs) { // There's a gap between the old index and the new one which means we've slipped behind the // live window and can't proceed. @@ -436,12 +445,8 @@ public class DefaultDashChunkSource implements DashChunkSource { return segmentIndex.getFirstSegmentNum() + segmentNumShift; } - public int getLastSegmentNum() { - int lastSegmentNum = segmentIndex.getLastSegmentNum(periodDurationUs); - if (lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) { - return DashSegmentIndex.INDEX_UNBOUNDED; - } - return lastSegmentNum + segmentNumShift; + public int getSegmentCount() { + return segmentIndex.getSegmentCount(periodDurationUs); } public long getSegmentStartTimeUs(int segmentNum) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java index c4a4a4446b..097676b89f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java @@ -45,17 +45,27 @@ public class AdaptationSet { */ public final List representations; + /** + * The accessibility descriptors in the adaptation set. + */ + public final List accessibilityDescriptors; + /** * @param id A non-negative identifier for the adaptation set that's unique in the scope of its * containing period, or {@link #ID_UNSET} if not specified. * @param type The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C} * {@code TRACK_TYPE_*} constants. * @param representations The {@link Representation}s in the adaptation set. + * @param accessibilityDescriptors The accessibility descriptors in the adaptation set. */ - public AdaptationSet(int id, int type, List representations) { + public AdaptationSet(int id, int type, List representations, + List accessibilityDescriptors) { this.id = id; this.type = type; this.representations = Collections.unmodifiableList(representations); + this.accessibilityDescriptors = accessibilityDescriptors == null + ? Collections.emptyList() + : Collections.unmodifiableList(accessibilityDescriptors); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index a9dc0a8665..d4338fd812 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -238,9 +238,9 @@ public class DashManifestParser extends DefaultHandler int audioChannels = Format.NO_VALUE; int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); String language = xpp.getAttributeValue(null, "lang"); - int accessibilityChannel = Format.NO_VALUE; ArrayList drmSchemeDatas = new ArrayList<>(); - ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList accessibilityDescriptors = new ArrayList<>(); List representationInfos = new ArrayList<>(); @C.SelectionFlags int selectionFlags = 0; @@ -265,11 +265,11 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) { audioChannels = parseAudioChannelConfiguration(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) { - accessibilityChannel = parseAccessibilityValue(xpp); + accessibilityDescriptors.add(parseAccessibility(xpp)); } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) { RepresentationInfo representationInfo = parseRepresentation(xpp, baseUrl, mimeType, codecs, width, height, frameRate, audioChannels, audioSamplingRate, language, - accessibilityChannel, selectionFlags, segmentBase); + selectionFlags, accessibilityDescriptors, segmentBase); contentType = checkContentTypeConsistency(contentType, getContentType(representationInfo.format)); representationInfos.add(representationInfo); @@ -293,12 +293,12 @@ public class DashManifestParser extends DefaultHandler drmSchemeDatas, inbandEventStreams)); } - return buildAdaptationSet(id, contentType, representations); + return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors); } protected AdaptationSet buildAdaptationSet(int id, int contentType, - List representations) { - return new AdaptationSet(id, contentType, representations); + List representations, List accessibilityDescriptors) { + return new AdaptationSet(id, contentType, representations, accessibilityDescriptors); } protected int parseContentType(XmlPullParser xpp) { @@ -335,30 +335,35 @@ public class DashManifestParser extends DefaultHandler */ protected SchemeData parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + boolean isPlayReady = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95".equals(schemeIdUri); byte[] data = null; UUID uuid = null; - boolean seenPsshElement = false; boolean requiresSecureDecoder = false; do { xpp.next(); - // The cenc:pssh element is defined in 23001-7:2015. - if (XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { - seenPsshElement = true; + if (data == null && XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") + && xpp.next() == XmlPullParser.TEXT) { + // The cenc:pssh element is defined in 23001-7:2015. data = Base64.decode(xpp.getText(), Base64.DEFAULT); uuid = PsshAtomUtil.parseUuid(data); + if (uuid == null) { + Log.w(TAG, "Skipping malformed cenc:pssh data"); + data = null; + } + } else if (data == null && isPlayReady && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") + && xpp.next() == XmlPullParser.TEXT) { + // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. + data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, + Base64.decode(xpp.getText(), Base64.DEFAULT)); + uuid = C.PLAYREADY_UUID; } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); - if (!seenPsshElement) { - return null; - } else if (uuid != null) { - return new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder); - } else { - Log.w(TAG, "Skipped unsupported ContentProtection element"); - return null; - } + return data != null ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) + : null; } /** @@ -367,16 +372,24 @@ public class DashManifestParser extends DefaultHandler * @param xpp The parser from which to read. * @throws XmlPullParserException If an error occurs parsing the element. * @throws IOException If an error occurs reading the element. - * @return {@link InbandEventStream} parsed from the element. + * @return A {@link SchemeValuePair} parsed from the element. */ - protected InbandEventStream parseInbandEventStream(XmlPullParser xpp) + protected SchemeValuePair parseInbandEventStream(XmlPullParser xpp) throws XmlPullParserException, IOException { - String schemeIdUri = parseString(xpp, "schemeIdUri", null); - String value = parseString(xpp, "value", null); - do { - xpp.next(); - } while (!XmlPullParserUtil.isEndTag(xpp, "InbandEventStream")); - return new InbandEventStream(schemeIdUri, value); + return parseSchemeValuePair(xpp, "InbandEventStream"); + } + + /** + * Parses an Accessibility element. + * + * @param xpp The parser from which to read. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return A {@link SchemeValuePair} parsed from the element. + */ + protected SchemeValuePair parseAccessibility(XmlPullParser xpp) + throws XmlPullParserException, IOException { + return parseSchemeValuePair(xpp, "Accessibility"); } /** @@ -415,8 +428,9 @@ public class DashManifestParser extends DefaultHandler String adaptationSetMimeType, String adaptationSetCodecs, int adaptationSetWidth, int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels, int adaptationSetAudioSamplingRate, String adaptationSetLanguage, - int adaptationSetAccessibilityChannel, @C.SelectionFlags int adaptationSetSelectionFlags, - SegmentBase segmentBase) throws XmlPullParserException, IOException { + @C.SelectionFlags int adaptationSetSelectionFlags, + List adaptationSetAccessibilityDescriptors, SegmentBase segmentBase) + throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -428,7 +442,7 @@ public class DashManifestParser extends DefaultHandler int audioChannels = adaptationSetAudioChannels; int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate); ArrayList drmSchemeDatas = new ArrayList<>(); - ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); boolean seenFirstBaseUrl = false; do { @@ -457,8 +471,8 @@ public class DashManifestParser extends DefaultHandler } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels, - audioSamplingRate, bandwidth, adaptationSetLanguage, adaptationSetAccessibilityChannel, - adaptationSetSelectionFlags, codecs); + audioSamplingRate, bandwidth, adaptationSetLanguage, adaptationSetSelectionFlags, + adaptationSetAccessibilityDescriptors, codecs); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeDatas, inbandEventStreams); @@ -466,7 +480,8 @@ public class DashManifestParser extends DefaultHandler protected Format buildFormat(String id, String containerMimeType, int width, int height, float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language, - int accessiblityChannel, @C.SelectionFlags int selectionFlags, String codecs) { + @C.SelectionFlags int selectionFlags, List accessibilityDescriptors, + String codecs) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); if (sampleMimeType != null) { if (MimeTypes.isVideo(sampleMimeType)) { @@ -476,8 +491,16 @@ public class DashManifestParser extends DefaultHandler return Format.createAudioContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, audioChannels, audioSamplingRate, null, selectionFlags, language); } else if (mimeTypeIsRawText(sampleMimeType)) { + int accessibilityChannel; + if (MimeTypes.APPLICATION_CEA608.equals(sampleMimeType)) { + accessibilityChannel = parseCea608AccessibilityChannel(accessibilityDescriptors); + } else if (MimeTypes.APPLICATION_CEA708.equals(sampleMimeType)) { + accessibilityChannel = parseCea708AccessibilityChannel(accessibilityDescriptors); + } else { + accessibilityChannel = Format.NO_VALUE; + } return Format.createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, - bitrate, selectionFlags, language, accessiblityChannel); + bitrate, selectionFlags, language, accessibilityChannel); } } return Format.createContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, @@ -486,14 +509,14 @@ public class DashManifestParser extends DefaultHandler protected Representation buildRepresentation(RepresentationInfo representationInfo, String contentId, ArrayList extraDrmSchemeDatas, - ArrayList extraInbandEventStreams) { + ArrayList extraInbandEventStreams) { Format format = representationInfo.format; ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; drmSchemeDatas.addAll(extraDrmSchemeDatas); if (!drmSchemeDatas.isEmpty()) { format = format.copyWithDrmInitData(new DrmInitData(drmSchemeDatas)); } - ArrayList inbandEventStremas = representationInfo.inbandEventStreams; + ArrayList inbandEventStremas = representationInfo.inbandEventStreams; inbandEventStremas.addAll(extraInbandEventStreams); return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format, representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStremas); @@ -785,52 +808,57 @@ public class DashManifestParser extends DefaultHandler } } - private static int parseAccessibilityValue(XmlPullParser xpp) - throws IOException, XmlPullParserException { + /** + * Parses a {@link SchemeValuePair} from an element. + * + * @param xpp The parser from which to read. + * @param tag The tag of the element being parsed. + * @throws XmlPullParserException If an error occurs parsing the element. + * @throws IOException If an error occurs reading the element. + * @return The parsed {@link SchemeValuePair}. + */ + protected static SchemeValuePair parseSchemeValuePair(XmlPullParser xpp, String tag) + throws XmlPullParserException, IOException { String schemeIdUri = parseString(xpp, "schemeIdUri", null); - String valueString = parseString(xpp, "value", null); - int accessibilityValue; - if (schemeIdUri == null || valueString == null) { - accessibilityValue = Format.NO_VALUE; - } else if ("urn:scte:dash:cc:cea-608:2015".equals(schemeIdUri)) { - accessibilityValue = parseCea608AccessibilityChannel(valueString); - } else if ("urn:scte:dash:cc:cea-708:2015".equals(schemeIdUri)) { - accessibilityValue = parseCea708AccessibilityChannel(valueString); - } else { - accessibilityValue = Format.NO_VALUE; - } + String value = parseString(xpp, "value", null); do { xpp.next(); - } while (!XmlPullParserUtil.isEndTag(xpp, "Accessibility")); - return accessibilityValue; + } while (!XmlPullParserUtil.isEndTag(xpp, tag)); + return new SchemeValuePair(schemeIdUri, value); } - static int parseCea608AccessibilityChannel(String accessibilityValueString) { - if (accessibilityValueString == null) { - return Format.NO_VALUE; - } - Matcher accessibilityValueMatcher = - CEA_608_ACCESSIBILITY_PATTERN.matcher(accessibilityValueString); - if (accessibilityValueMatcher.matches()) { - return Integer.parseInt(accessibilityValueMatcher.group(1)); - } else { - Log.w(TAG, "Unable to parse channel number from " + accessibilityValueString); - return Format.NO_VALUE; + protected static int parseCea608AccessibilityChannel( + List accessibilityDescriptors) { + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + SchemeValuePair descriptor = accessibilityDescriptors.get(i); + if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri) + && descriptor.value != null) { + Matcher accessibilityValueMatcher = CEA_608_ACCESSIBILITY_PATTERN.matcher(descriptor.value); + if (accessibilityValueMatcher.matches()) { + return Integer.parseInt(accessibilityValueMatcher.group(1)); + } else { + Log.w(TAG, "Unable to parse CEA-608 channel number from: " + descriptor.value); + } + } } + return Format.NO_VALUE; } - static int parseCea708AccessibilityChannel(String accessibilityValueString) { - if (accessibilityValueString == null) { - return Format.NO_VALUE; - } - Matcher accessibilityValueMatcher = - CEA_708_ACCESSIBILITY_PATTERN.matcher(accessibilityValueString); - if (accessibilityValueMatcher.matches()) { - return Integer.parseInt(accessibilityValueMatcher.group(1)); - } else { - Log.w(TAG, "Unable to parse service block number from " + accessibilityValueString); - return Format.NO_VALUE; + protected static int parseCea708AccessibilityChannel( + List accessibilityDescriptors) { + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + SchemeValuePair descriptor = accessibilityDescriptors.get(i); + if ("urn:scte:dash:cc:cea-708:2015".equals(descriptor.schemeIdUri) + && descriptor.value != null) { + Matcher accessibilityValueMatcher = CEA_708_ACCESSIBILITY_PATTERN.matcher(descriptor.value); + if (accessibilityValueMatcher.matches()) { + return Integer.parseInt(accessibilityValueMatcher.group(1)); + } else { + Log.w(TAG, "Unable to parse CEA-708 service block number from: " + descriptor.value); + } + } } + return Format.NO_VALUE; } protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) { @@ -897,10 +925,10 @@ public class DashManifestParser extends DefaultHandler public final String baseUrl; public final SegmentBase segmentBase; public final ArrayList drmSchemeDatas; - public final ArrayList inbandEventStreams; + public final ArrayList inbandEventStreams; public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, - ArrayList drmSchemeDatas, ArrayList inbandEventStreams) { + ArrayList drmSchemeDatas, ArrayList inbandEventStreams) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index cdf84f5f71..5960d4d7ba 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -63,9 +63,9 @@ public abstract class Representation { */ public final long presentationTimeOffsetUs; /** - * The {@link InbandEventStream}s in the representation. Never null, but may be empty. + * The in-band event streams in the representation. Never null, but may be empty. */ - public final List inbandEventStreams; + public final List inbandEventStreams; private final RangedUri initializationUri; @@ -92,11 +92,11 @@ public abstract class Representation { * @param format The format of the representation. * @param baseUrl The base URL. * @param segmentBase A segment base element for the representation. - * @param inbandEventStreams The {@link InbandEventStream}s in the representation. May be null. + * @param inbandEventStreams The in-band event streams in the representation. May be null. * @return The constructed instance. */ public static Representation newInstance(String contentId, long revisionId, Format format, - String baseUrl, SegmentBase segmentBase, List inbandEventStreams) { + String baseUrl, SegmentBase segmentBase, List inbandEventStreams) { return newInstance(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams, null); } @@ -109,13 +109,13 @@ public abstract class Representation { * @param format The format of the representation. * @param baseUrl The base URL of the representation. * @param segmentBase A segment base element for the representation. - * @param inbandEventStreams The {@link InbandEventStream}s in the representation. May be null. + * @param inbandEventStreams The in-band event streams in the representation. May be null. * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. This * parameter is ignored if {@code segmentBase} consists of multiple segments. * @return The constructed instance. */ public static Representation newInstance(String contentId, long revisionId, Format format, - String baseUrl, SegmentBase segmentBase, List inbandEventStreams, + String baseUrl, SegmentBase segmentBase, List inbandEventStreams, String customCacheKey) { if (segmentBase instanceof SingleSegmentBase) { return new SingleSegmentRepresentation(contentId, revisionId, format, baseUrl, @@ -130,13 +130,13 @@ public abstract class Representation { } private Representation(String contentId, long revisionId, Format format, String baseUrl, - SegmentBase segmentBase, List inbandEventStreams) { + SegmentBase segmentBase, List inbandEventStreams) { this.contentId = contentId; this.revisionId = revisionId; this.format = format; this.baseUrl = baseUrl; this.inbandEventStreams = inbandEventStreams == null - ? Collections.emptyList() + ? Collections.emptyList() : Collections.unmodifiableList(inbandEventStreams); initializationUri = segmentBase.getInitialization(this); presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs(); @@ -195,13 +195,13 @@ public abstract class Representation { * @param initializationEnd The offset of the last byte of initialization data. * @param indexStart The offset of the first byte of index data. * @param indexEnd The offset of the last byte of index data. - * @param inbandEventStreams The {@link InbandEventStream}s in the representation. May be null. + * @param inbandEventStreams The in-band event streams in the representation. May be null. * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown. */ public static SingleSegmentRepresentation newInstance(String contentId, long revisionId, Format format, String uri, long initializationStart, long initializationEnd, - long indexStart, long indexEnd, List inbandEventStreams, + long indexStart, long indexEnd, List inbandEventStreams, String customCacheKey, long contentLength) { RangedUri rangedUri = new RangedUri(null, initializationStart, initializationEnd - initializationStart + 1); @@ -217,12 +217,12 @@ public abstract class Representation { * @param format The format of the representation. * @param baseUrl The base URL of the representation. * @param segmentBase The segment base underlying the representation. - * @param inbandEventStreams The {@link InbandEventStream}s in the representation. May be null. + * @param inbandEventStreams The in-band event streams in the representation. May be null. * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown. */ public SingleSegmentRepresentation(String contentId, long revisionId, Format format, - String baseUrl, SingleSegmentBase segmentBase, List inbandEventStreams, + String baseUrl, SingleSegmentBase segmentBase, List inbandEventStreams, String customCacheKey, long contentLength) { super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.uri = Uri.parse(baseUrl); @@ -267,10 +267,10 @@ public abstract class Representation { * @param format The format of the representation. * @param baseUrl The base URL of the representation. * @param segmentBase The segment base underlying the representation. - * @param inbandEventStreams The {@link InbandEventStream}s in the representation. May be null. + * @param inbandEventStreams The in-band event streams in the representation. May be null. */ public MultiSegmentRepresentation(String contentId, long revisionId, Format format, - String baseUrl, MultiSegmentBase segmentBase, List inbandEventStreams) { + String baseUrl, MultiSegmentBase segmentBase, List inbandEventStreams) { super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.segmentBase = segmentBase; } @@ -318,8 +318,8 @@ public abstract class Representation { } @Override - public int getLastSegmentNum(long periodDurationUs) { - return segmentBase.getLastSegmentNum(periodDurationUs); + public int getSegmentCount(long periodDurationUs) { + return segmentBase.getSegmentCount(periodDurationUs); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/InbandEventStream.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java similarity index 87% rename from library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/InbandEventStream.java rename to library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java index 2f24603598..470bf0f989 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/InbandEventStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java @@ -18,14 +18,14 @@ package com.google.android.exoplayer2.source.dash.manifest; import com.google.android.exoplayer2.util.Util; /** - * Represents a DASH in-band event stream. + * A pair consisting of a scheme ID and value. */ -public class InbandEventStream { +public class SchemeValuePair { public final String schemeIdUri; public final String value; - public InbandEventStream(String schemeIdUri, String value) { + public SchemeValuePair(String schemeIdUri, String value) { this.schemeIdUri = schemeIdUri; this.value = value; } @@ -38,7 +38,7 @@ public class InbandEventStream { if (obj == null || getClass() != obj.getClass()) { return false; } - InbandEventStream other = (InbandEventStream) obj; + SchemeValuePair other = (SchemeValuePair) obj; return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index 70a65e932a..4f7dc81fc5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -130,18 +130,22 @@ public abstract class SegmentBase { */ public int getSegmentNum(long timeUs, long periodDurationUs) { final int firstSegmentNum = getFirstSegmentNum(); - int lowIndex = firstSegmentNum; - int highIndex = getLastSegmentNum(periodDurationUs); + final int segmentCount = getSegmentCount(periodDurationUs); + if (segmentCount == 0) { + return firstSegmentNum; + } if (segmentTimeline == null) { // All segments are of equal duration (with the possible exception of the last one). long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; int segmentNum = startNumber + (int) (timeUs / durationUs); // Ensure we stay within bounds. - return segmentNum < lowIndex ? lowIndex - : highIndex != DashSegmentIndex.INDEX_UNBOUNDED && segmentNum > highIndex ? highIndex - : segmentNum; + return segmentNum < firstSegmentNum ? firstSegmentNum + : segmentCount == DashSegmentIndex.INDEX_UNBOUNDED ? segmentNum + : Math.min(segmentNum, firstSegmentNum + segmentCount - 1); } else { - // The high index cannot be unbounded. Identify the segment using binary search. + // The index cannot be unbounded. Identify the segment using binary search. + int lowIndex = firstSegmentNum; + int highIndex = firstSegmentNum + segmentCount - 1; while (lowIndex <= highIndex) { int midIndex = lowIndex + (highIndex - lowIndex) / 2; long midTimeUs = getSegmentTimeUs(midIndex); @@ -165,7 +169,9 @@ public abstract class SegmentBase { long duration = segmentTimeline.get(sequenceNumber - startNumber).duration; return (duration * C.MICROS_PER_SECOND) / timescale; } else { - return sequenceNumber == getLastSegmentNum(periodDurationUs) + int segmentCount = getSegmentCount(periodDurationUs); + return segmentCount != DashSegmentIndex.INDEX_UNBOUNDED + && sequenceNumber == (getFirstSegmentNum() + segmentCount - 1) ? (periodDurationUs - getSegmentTimeUs(sequenceNumber)) : ((duration * C.MICROS_PER_SECOND) / timescale); } @@ -201,9 +207,9 @@ public abstract class SegmentBase { } /** - * @see DashSegmentIndex#getLastSegmentNum(long) + * @see DashSegmentIndex#getSegmentCount(long) */ - public abstract int getLastSegmentNum(long periodDurationUs); + public abstract int getSegmentCount(long periodDurationUs); /** * @see DashSegmentIndex#isExplicit() @@ -250,8 +256,8 @@ public abstract class SegmentBase { } @Override - public int getLastSegmentNum(long periodDurationUs) { - return startNumber + mediaSegments.size() - 1; + public int getSegmentCount(long periodDurationUs) { + return mediaSegments.size(); } @Override @@ -322,14 +328,14 @@ public abstract class SegmentBase { } @Override - public int getLastSegmentNum(long periodDurationUs) { + public int getSegmentCount(long periodDurationUs) { if (segmentTimeline != null) { - return segmentTimeline.size() + startNumber - 1; - } else if (periodDurationUs == C.TIME_UNSET) { - return DashSegmentIndex.INDEX_UNBOUNDED; - } else { + return segmentTimeline.size(); + } else if (periodDurationUs != C.TIME_UNSET) { long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; - return startNumber + (int) Util.ceilDivide(periodDurationUs, durationUs) - 1; + return (int) Util.ceilDivide(periodDurationUs, durationUs); + } else { + return DashSegmentIndex.INDEX_UNBOUNDED; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java index 083046d073..4ce49c5ffe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java @@ -57,8 +57,8 @@ import com.google.android.exoplayer2.source.dash.DashSegmentIndex; } @Override - public int getLastSegmentNum(long periodDurationUs) { - return 0; + public int getSegmentCount(long periodDurationUs) { + return 1; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index edd3c735c1..c7c66fbd61 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -19,7 +19,6 @@ import android.net.Uri; import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.Chunk; @@ -33,6 +32,7 @@ import com.google.android.exoplayer2.trackselection.BaseTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -194,15 +194,16 @@ import java.util.Locale; // Select the variant. trackSelection.updateSelectedTrack(bufferedDurationUs); - int newVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); + int selectedVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); - boolean switchingVariant = oldVariantIndex != newVariantIndex; - HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(variants[newVariantIndex]); - if (mediaPlaylist == null) { - out.playlist = variants[newVariantIndex]; + boolean switchingVariant = oldVariantIndex != selectedVariantIndex; + HlsUrl selectedUrl = variants[selectedVariantIndex]; + if (!playlistTracker.isSnapshotValid(selectedUrl)) { + out.playlist = selectedUrl; // Retry when playlist is refreshed. return; } + HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); // Select the chunk. int chunkMediaSequence; @@ -218,8 +219,9 @@ import java.util.Locale; if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) { // We try getting the next chunk without adapting in case that's the reason for falling // behind the live window. - newVariantIndex = oldVariantIndex; - mediaPlaylist = playlistTracker.getPlaylistSnapshot(variants[newVariantIndex]); + selectedVariantIndex = oldVariantIndex; + selectedUrl = variants[selectedVariantIndex]; + mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); chunkMediaSequence = previous.getNextChunkIndex(); } } @@ -236,7 +238,7 @@ import java.util.Locale; if (mediaPlaylist.hasEndTag) { out.endOfStream = true; } else /* Live */ { - out.playlist = variants[newVariantIndex]; + out.playlist = selectedUrl; } return; } @@ -249,7 +251,7 @@ import java.util.Locale; Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); if (!keyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. - out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, newVariantIndex, + out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex, trackSelection.getSelectionReason(), trackSelection.getSelectionData()); return; } @@ -279,7 +281,7 @@ import java.util.Locale; Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); - out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex], + out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, selectedUrl, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 0c411854d5..5885797896 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -21,7 +21,6 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; @@ -37,6 +36,7 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.concurrent.atomic.AtomicInteger; @@ -56,6 +56,7 @@ import java.util.concurrent.atomic.AtomicInteger; private static final String EC3_FILE_EXTENSION = ".ec3"; private static final String MP3_FILE_EXTENSION = ".mp3"; private static final String MP4_FILE_EXTENSION = ".mp4"; + private static final String M4_FILE_EXTENSION_PREFIX = ".m4"; private static final String VTT_FILE_EXTENSION = ".vtt"; private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; @@ -187,7 +188,7 @@ import java.util.concurrent.atomic.AtomicInteger; public void load() throws IOException, InterruptedException { if (extractor == null && !isPackedAudio) { // See HLS spec, version 20, Section 3.4 for more information on packed audio extraction. - extractor = buildExtractorByExtension(); + extractor = createExtractor(); } maybeLoadInitData(); if (!loadCanceled) { @@ -329,18 +330,20 @@ import java.util.concurrent.atomic.AtomicInteger; return new Aes128DataSource(dataSource, encryptionKey, encryptionIv); } - private Extractor buildExtractorByExtension() { - // Set the extractor that will read the chunk. + private Extractor createExtractor() { + // Select the extractor that will read the chunk. Extractor extractor; boolean usingNewExtractor = true; - if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) + if (MimeTypes.TEXT_VTT.equals(hlsUrl.format.sampleMimeType) + || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { extractor = new WebvttExtractor(trackFormat.language, timestampAdjuster); } else if (!needNewExtractor) { // Only reuse TS and fMP4 extractors. usingNewExtractor = false; extractor = previousExtractor; - } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) { + } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) + || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { extractor = new FragmentedMp4Extractor(0, timestampAdjuster); } else { // MPEG-2 TS segments, but we need a new extractor. @@ -376,7 +379,7 @@ import java.util.concurrent.atomic.AtomicInteger; || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { extractor = new Ac3Extractor(startTimeUs); } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { - extractor = new Mp3Extractor(startTimeUs); + extractor = new Mp3Extractor(0, startTimeUs); } else { throw new IllegalArgumentException("Unkown extension for audio file: " + lastPathSegment); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index a9bbddb69c..538acbeabf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -112,10 +112,9 @@ import java.util.LinkedList; * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. - * @param muxedAudioFormat If HLS master playlist indicates that the stream contains muxed audio, - * this is the audio {@link Format} as defined by the playlist. - * @param muxedCaptionFormat If HLS master playlist indicates that the stream contains muxed - * captions, this is the audio {@link Format} as defined by the playlist. + * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. + * @param muxedCaptionFormat Optional muxed closed caption {@link Format} as defined by the master + * playlist. * @param minLoadableRetryCount The minimum number of times that the source should retry a load * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. @@ -157,7 +156,7 @@ import java.util.LinkedList; * prepare. */ public void prepareSingleTrack(Format format) { - track(0).format(format); + track(0, C.TRACK_TYPE_UNKNOWN).format(format); sampleQueuesBuilt = true; maybeFinishPrepare(); } @@ -266,15 +265,6 @@ import java.util.LinkedList; released = true; } - public long getLargestQueuedTimestampUs() { - long largestQueuedTimestampUs = Long.MIN_VALUE; - for (int i = 0; i < sampleQueues.size(); i++) { - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); - } - return largestQueuedTimestampUs; - } - public void setIsTimestampMaster(boolean isTimestampMaster) { chunkSource.setIsTimestampMaster(isTimestampMaster); } @@ -466,7 +456,7 @@ import java.util.LinkedList; // ExtractorOutput implementation. Called by the loading thread. @Override - public DefaultTrackOutput track(int id) { + public DefaultTrackOutput track(int id, int type) { if (sampleQueues.indexOfKey(id) >= 0) { return sampleQueues.get(id); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java index 624e5fa4f8..41fb2c1512 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.source.hls; import android.util.SparseArray; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; +import com.google.android.exoplayer2.util.TimestampAdjuster; /** * Provides {@link TimestampAdjuster} instances for use during HLS playbacks. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java index 498dd55004..12ea2c16c7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java @@ -24,12 +24,12 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.text.webvtt.WebvttParserUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.IOException; import java.util.Arrays; import java.util.regex.Matcher; @@ -167,7 +167,7 @@ import java.util.regex.Pattern; } private TrackOutput buildTrackOutput(long subsampleOffsetUs) { - TrackOutput trackOutput = output.track(0); + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_TEXT); trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, language, null, subsampleOffsetUs)); output.endTracks(); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index b7426fd03d..ab18fda2f0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -31,27 +31,18 @@ public final class HlsMasterPlaylist extends HlsPlaylist { */ public static final class HlsUrl { - public final String name; public final String url; public final Format format; - public final Format videoFormat; - public final Format audioFormat; - public final Format[] textFormats; public static HlsUrl createMediaPlaylistHlsUrl(String baseUri) { Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null, Format.NO_VALUE, 0, null); - return new HlsUrl(null, baseUri, format, null, null, null); + return new HlsUrl(baseUri, format); } - public HlsUrl(String name, String url, Format format, Format videoFormat, Format audioFormat, - Format[] textFormats) { - this.name = name; + public HlsUrl(String url, Format format) { this.url = url; this.format = format; - this.videoFormat = videoFormat; - this.audioFormat = audioFormat; - this.textFormats = textFormats; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 0b61b9781e..b8d8d69af4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -15,7 +15,10 @@ */ package com.google.android.exoplayer2.source.hls.playlist; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; @@ -65,6 +68,18 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } + /** + * Type of the playlist as specified by #EXT-X-PLAYLIST-TYPE. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({PLAYLIST_TYPE_UNKNOWN, PLAYLIST_TYPE_VOD, PLAYLIST_TYPE_EVENT}) + public @interface PlaylistType {} + public static final int PLAYLIST_TYPE_UNKNOWN = 0; + public static final int PLAYLIST_TYPE_VOD = 1; + public static final int PLAYLIST_TYPE_EVENT = 2; + + @PlaylistType + public final int playlistType; public final long startOffsetUs; public final long startTimeUs; public final boolean hasDiscontinuitySequence; @@ -78,11 +93,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final List segments; public final long durationUs; - public HlsMediaPlaylist(String baseUri, long startOffsetUs, long startTimeUs, - boolean hasDiscontinuitySequence, int discontinuitySequence, int mediaSequence, int version, - long targetDurationUs, boolean hasEndTag, boolean hasProgramDateTime, - Segment initializationSegment, List segments) { + public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs, + long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, + int mediaSequence, int version, long targetDurationUs, boolean hasEndTag, + boolean hasProgramDateTime, Segment initializationSegment, List segments) { super(baseUri, HlsPlaylist.TYPE_MEDIA); + this.playlistType = playlistType; this.startTimeUs = startTimeUs; this.hasDiscontinuitySequence = hasDiscontinuitySequence; this.discontinuitySequence = discontinuitySequence; @@ -137,9 +153,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @return The playlist. */ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { - return new HlsMediaPlaylist(baseUri, startOffsetUs, startTimeUs, true, discontinuitySequence, - mediaSequence, version, targetDurationUs, hasEndTag, hasProgramDateTime, - initializationSegment, segments); + return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true, + discontinuitySequence, mediaSequence, version, targetDurationUs, hasEndTag, + hasProgramDateTime, initializationSegment, segments); } /** @@ -152,9 +168,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist { if (this.hasEndTag) { return this; } - return new HlsMediaPlaylist(baseUri, startOffsetUs, startTimeUs, hasDiscontinuitySequence, - discontinuitySequence, mediaSequence, version, targetDurationUs, true, hasProgramDateTime, - initializationSegment, segments); + return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, + hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs, + true, hasProgramDateTime, initializationSegment, segments); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index c349bbee05..6efd1fecb2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -19,6 +19,7 @@ import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.source.UnrecognizedInputFormatException; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.MimeTypes; @@ -39,23 +40,10 @@ import java.util.regex.Pattern; */ public final class HlsPlaylistParser implements ParsingLoadable.Parser { - /** - * Thrown if the input does not start with an HLS playlist header. - */ - public static final class UnrecognizedInputFormatException extends ParserException { - - public final Uri inputUri; - - public UnrecognizedInputFormatException(Uri inputUri) { - super("Input does not start with the #EXTM3U header. Uri: " + inputUri); - this.inputUri = inputUri; - } - - } - private static final String PLAYLIST_HEADER = "#EXTM3U"; private static final String TAG_VERSION = "#EXT-X-VERSION"; + private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE"; private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF"; private static final String TAG_MEDIA = "#EXT-X-MEDIA"; private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION"; @@ -87,6 +75,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser mediaPlaylistLoadable; private HlsMediaPlaylist playlistSnapshot; + private long lastSnapshotLoadMs; private long lastSnapshotAccessTimeMs; private long blacklistUntilMs; @@ -429,6 +446,18 @@ public final class HlsPlaylistTracker implements Loader.Callback currentTimeMs; + } + public void release() { mediaPlaylistLoader.release(); } @@ -488,6 +517,7 @@ public final class HlsPlaylistTracker implements Loader.Callback @@ -122,9 +129,8 @@ public class Cue { * {@code (line == -2 && lineAnchor == ANCHOR_TYPE_START)} position a cue so that only its first * line is visible at the bottom of the viewport. */ + @LineType public final int lineType; - @LineType - public final int lineType; /** * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. @@ -133,9 +139,8 @@ public class Cue { * and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box * respectively. */ + @AnchorType public final int lineAnchor; - @AnchorType - public final int lineAnchor; /** * The fractional position of the {@link #positionAnchor} of the cue box within the viewport in * the direction orthogonal to {@link #line}, or {@link #DIMEN_UNSET}. @@ -154,8 +159,7 @@ public class Cue { * and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box * respectively. */ - @AnchorType - public final int positionAnchor; + @AnchorType public final int positionAnchor; /** * The size of the cue box in the writing direction specified as a fraction of the viewport size @@ -174,7 +178,27 @@ public class Cue { public final int windowColor; /** - * Constructs a cue whose {@link #textAlignment} is null, whose type parameters are set to + * Creates an image cue. + * + * @param bitmap See {@link #bitmap}. + * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed + * as a fraction of the viewport width. + * @param horizontalPositionAnchor The horizontal anchor. One of {@link #ANCHOR_TYPE_START}, + * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * @param verticalPosition The position of the vertical anchor within the viewport, expressed as a + * fraction of the viewport height. + * @param verticalPositionAnchor The vertical anchor. One of {@link #ANCHOR_TYPE_START}, + * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + * @param width The width of the cue, expressed as a fraction of the viewport width. + */ + public Cue(Bitmap bitmap, float horizontalPosition, @AnchorType int horizontalPositionAnchor, + float verticalPosition, @AnchorType int verticalPositionAnchor, float width) { + this(null, null, bitmap, verticalPosition, LINE_TYPE_FRACTION, verticalPositionAnchor, + horizontalPosition, horizontalPositionAnchor, width, false, Color.BLACK); + } + + /** + * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}. * * @param text See {@link #text}. @@ -184,6 +208,8 @@ public class Cue { } /** + * Creates a text cue. + * * @param text See {@link #text}. * @param textAlignment See {@link #textAlignment}. * @param line See {@link #line}. @@ -200,6 +226,8 @@ public class Cue { } /** + * Creates a text cue. + * * @param text See {@link #text}. * @param textAlignment See {@link #textAlignment}. * @param line See {@link #line}. @@ -214,8 +242,16 @@ public class Cue { public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, boolean windowColorSet, int windowColor) { + this(text, textAlignment, null, line, lineType, lineAnchor, position, positionAnchor, size, + windowColorSet, windowColor); + } + + private Cue(CharSequence text, Alignment textAlignment, Bitmap bitmap, float line, + @LineType int lineType, @AnchorType int lineAnchor, float position, + @AnchorType int positionAnchor, float size, boolean windowColorSet, int windowColor) { this.text = text; this.textAlignment = textAlignment; + this.bitmap = bitmap; this.line = line; this.lineType = lineType; this.lineAnchor = lineAnchor; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index ae3bd309ff..dd25ef8345 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.SimpleDecoder; import java.nio.ByteBuffer; @@ -68,6 +69,8 @@ public abstract class SimpleSubtitleDecoder extends ByteBuffer inputData = inputBuffer.data; Subtitle subtitle = decode(inputData.array(), inputData.limit()); outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); + // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). + outputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); return null; } catch (SubtitleDecoderException e) { return e; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 8dbde1be5e..649575865e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -191,8 +191,6 @@ public final class TextRenderer extends BaseRenderer implements Callback { // Try and read the next subtitle from the source. int result = readSource(formatHolder, nextInputBuffer); if (result == C.RESULT_BUFFER_READ) { - // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]) and queue the buffer. - nextInputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); if (nextInputBuffer.isEndOfStream()) { inputStreamEnded = true; } else { diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 3ae8ded9ba..fe9a5fbc5c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -49,12 +49,6 @@ public final class Cea608Decoder extends CeaDecoder { private static final int NTSC_CC_FIELD_2 = 0x01; private static final int CC_VALID_608_ID = 0x04; - private static final int PAYLOAD_TYPE_CC = 4; - private static final int COUNTRY_CODE = 0xB5; - private static final int PROVIDER_CODE = 0x31; - private static final int USER_ID = 0x47413934; // "GA94" - private static final int USER_DATA_TYPE_CODE = 0x3; - private static final int CC_MODE_UNKNOWN = 0; private static final int CC_MODE_ROLL_UP = 1; private static final int CC_MODE_POP_ON = 2; @@ -370,7 +364,7 @@ public final class Cea608Decoder extends CeaDecoder { } else if (isPreambleAddressCode(cc1, cc2)) { handlePreambleAddressCode(cc1, cc2); } else if (isTabCtrlCode(cc1, cc2)) { - currentCueBuilder.tab(cc2 - 0x20); + currentCueBuilder.setTab(cc2 - 0x20); } else if (isMiscCode(cc1, cc2)) { handleMiscCode(cc2); } @@ -509,11 +503,14 @@ public final class Cea608Decoder extends CeaDecoder { return; } + int oldCaptionMode = this.captionMode; this.captionMode = captionMode; + // Clear the working memory. resetCueBuilders(); - if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) { - // When switching to roll-up or unknown, we also need to clear the caption. + if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP + || captionMode == CC_MODE_UNKNOWN) { + // When switching from paint-on or to roll-up or unknown, we also need to clear the caption. cues = null; } } @@ -573,31 +570,6 @@ public final class Cea608Decoder extends CeaDecoder { return (cc1 & 0xF0) == 0x10; } - /** - * Inspects an sei message to determine whether it contains CEA-608. - *

- * The position of {@code payload} is left unchanged. - * - * @param payloadType The payload type of the message. - * @param payloadLength The length of the payload. - * @param payload A {@link ParsableByteArray} containing the payload. - * @return Whether the sei message contains CEA-608. - */ - public static boolean isSeiMessageCea608(int payloadType, int payloadLength, - ParsableByteArray payload) { - if (payloadType != PAYLOAD_TYPE_CC || payloadLength < 8) { - return false; - } - int startPosition = payload.getPosition(); - int countryCode = payload.readUnsignedByte(); - int providerCode = payload.readUnsignedShort(); - int userIdentifier = payload.readInt(); - int userDataTypeCode = payload.readUnsignedByte(); - payload.setPosition(startPosition); - return countryCode == COUNTRY_CODE && providerCode == PROVIDER_CODE - && userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE; - } - private static class CueBuilder { private static final int POSITION_UNSET = -1; @@ -677,8 +649,8 @@ public final class Cea608Decoder extends CeaDecoder { this.indent = indent; } - public void tab(int tabs) { - tabOffset += tabs; + public void setTab(int tabs) { + tabOffset = tabs; } public void setPreambleStyle(CharacterStyle style) { diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 5ca5ce1270..e04c246ea0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -43,13 +43,6 @@ import java.util.List; /** * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708"). - * - *

This implementation does not provide full compatibility with the CEA-708 specification. Note - * that only the default pen/text and window/cue colors (i.e. text with - * {@link CueBuilder#COLOR_SOLID_WHITE} foreground and {@link CueBuilder#COLOR_SOLID_BLACK} - * background, and cues with {@link CueBuilder#COLOR_SOLID_BLACK} fill) will be overridden with - * device accessibility settings; all others will use the colors and opacity specified by the - * caption data. */ public final class Cea708Decoder extends CeaDecoder { @@ -218,7 +211,7 @@ public final class Cea708Decoder extends CeaDecoder { } if (!ccValid) { - finalizeCurrentPacket(); + // This byte-pair isn't valid, ignore it and continue. continue; } @@ -266,7 +259,8 @@ public final class Cea708Decoder extends CeaDecoder { if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) { Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1) + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number " - + currentDtvCcPacket.sequenceNumber + ")"); + + currentDtvCcPacket.sequenceNumber + "); ignoring packet"); + return; } serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex); diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java index ae92d7fab8..fac0982e65 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.cea; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoder; import com.google.android.exoplayer2.text.SubtitleDecoderException; @@ -74,7 +75,13 @@ import java.util.TreeSet; public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException { Assertions.checkArgument(inputBuffer != null); Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); - queuedInputBuffers.add(inputBuffer); + if (inputBuffer.isDecodeOnly()) { + // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow + // for decoding to begin mid-stream. + releaseInputBuffer(inputBuffer); + } else { + queuedInputBuffers.add(inputBuffer); + } dequeuedInputBuffer = null; } @@ -109,7 +116,7 @@ import java.util.TreeSet; Subtitle subtitle = createSubtitle(); if (!inputBuffer.isDecodeOnly()) { SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst(); - outputBuffer.setContent(inputBuffer.timeUs, subtitle, 0); + outputBuffer.setContent(inputBuffer.timeUs, subtitle, Format.OFFSET_SAMPLE_RELATIVE); releaseInputBuffer(inputBuffer); return outputBuffer; } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java index 620b2c7d80..7da2054a08 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaSubtitle.java @@ -15,8 +15,11 @@ */ package com.google.android.exoplayer2.text.cea; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.util.Assertions; +import java.util.Collections; import java.util.List; /** @@ -35,7 +38,7 @@ import java.util.List; @Override public int getNextEventTimeIndex(long timeUs) { - return 0; + return timeUs < 0 ? 0 : C.INDEX_UNSET; } @Override @@ -45,12 +48,13 @@ import java.util.List; @Override public long getEventTime(int index) { + Assertions.checkArgument(index == 0); return 0; } @Override public List getCues(long timeUs) { - return cues; + return timeUs >= 0 ? cues : Collections.emptyList(); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java new file mode 100644 index 0000000000..a39c8c8669 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2017 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.exoplayer2.text.cea; + +import android.util.Log; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.util.ParsableByteArray; + +/** + * Utility methods for handling CEA-608/708 messages. + */ +public final class CeaUtil { + + private static final String TAG = "CeaUtil"; + + private static final int PAYLOAD_TYPE_CC = 4; + private static final int COUNTRY_CODE = 0xB5; + private static final int PROVIDER_CODE = 0x31; + private static final int USER_ID = 0x47413934; // "GA94" + private static final int USER_DATA_TYPE_CODE = 0x3; + + /** + * Consumes the unescaped content of an SEI NAL unit, writing the content of any CEA-608 messages + * as samples to the provided output. + * + * @param presentationTimeUs The presentation time in microseconds for any samples. + * @param seiBuffer The unescaped SEI NAL unit data, excluding the NAL unit start code and type. + * @param output The output to which any samples should be written. + */ + public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer, + TrackOutput output) { + while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { + int payloadType = readNon255TerminatedValue(seiBuffer); + int payloadSize = readNon255TerminatedValue(seiBuffer); + // Process the payload. + if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) { + // This might occur if we're trying to read an encrypted SEI NAL unit. + Log.w(TAG, "Skipping remainder of malformed SEI NAL unit."); + seiBuffer.setPosition(seiBuffer.limit()); + } else if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) { + // Ignore country_code (1) + provider_code (2) + user_identifier (4) + // + user_data_type_code (1). + seiBuffer.skipBytes(8); + // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1). + int ccCount = seiBuffer.readUnsignedByte() & 0x1F; + // Ignore em_data (1) + seiBuffer.skipBytes(1); + // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2) + // + cc_data_1 (8) + cc_data_2 (8). + int sampleLength = ccCount * 3; + output.sampleData(seiBuffer, sampleLength); + output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null); + // Ignore trailing information in SEI, if any. + seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3)); + } else { + seiBuffer.skipBytes(payloadSize); + } + } + } + + /** + * Reads a value from the provided buffer consisting of zero or more 0xFF bytes followed by a + * terminating byte not equal to 0xFF. The returned value is ((0xFF * N) + T), where N is the + * number of 0xFF bytes and T is the value of the terminating byte. + * + * @param buffer The buffer from which to read the value. + * @returns The read value, or -1 if the end of the buffer is reached before a value is read. + */ + private static int readNon255TerminatedValue(ParsableByteArray buffer) { + int b; + int value = 0; + do { + if (buffer.bytesLeft() == 0) { + return -1; + } + b = buffer.readUnsignedByte(); + value += b; + } while (b == 0xFF); + return value; + } + + /** + * Inspects an sei message to determine whether it contains CEA-608. + *

+ * The position of {@code payload} is left unchanged. + * + * @param payloadType The payload type of the message. + * @param payloadLength The length of the payload. + * @param payload A {@link ParsableByteArray} containing the payload. + * @return Whether the sei message contains CEA-608. + */ + private static boolean isSeiMessageCea608(int payloadType, int payloadLength, + ParsableByteArray payload) { + if (payloadType != PAYLOAD_TYPE_CC || payloadLength < 8) { + return false; + } + int startPosition = payload.getPosition(); + int countryCode = payload.readUnsignedByte(); + int providerCode = payload.readUnsignedShort(); + int userIdentifier = payload.readInt(); + int userDataTypeCode = payload.readUnsignedByte(); + payload.setPosition(startPosition); + return countryCode == COUNTRY_CODE && providerCode == PROVIDER_CODE + && userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE; + } + + private CeaUtil() {} + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index c81ffb441f..054ee7973f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -148,7 +148,7 @@ public abstract class BaseTrackSelection implements TrackSelection { } /** - * Returns whether the track at the specified index in the selection is blaclisted. + * Returns whether the track at the specified index in the selection is blacklisted. * * @param index The index of the track in the selection. * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}. diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 79979401f7..f62d5d9075 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -560,6 +560,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; + int selectedBitrate = Format.NO_VALUE; int selectedPixelCount = Format.NO_VALUE; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); @@ -582,16 +583,24 @@ public class DefaultTrackSelector extends MappingTrackSelector { } boolean selectTrack = trackScore > selectedTrackScore; if (trackScore == selectedTrackScore) { - // Use the pixel count as a tie breaker. If we're within constraints prefer a higher - // pixel count, else prefer a lower count. If still tied then prefer the first track - // (i.e. the one that's already selected). - int pixelComparison = comparePixelCounts(format.getPixelCount(), selectedPixelCount); - selectTrack = isWithinConstraints ? pixelComparison > 0 : pixelComparison < 0; + // Use the pixel count as a tie breaker (or bitrate if pixel counts are tied). If we're + // within constraints prefer a higher pixel count (or bitrate), else prefer a lower + // count (or bitrate). If still tied then prefer the first track (i.e. the one that's + // already selected). + int comparisonResult; + int formatPixelCount = format.getPixelCount(); + if (formatPixelCount != selectedPixelCount) { + comparisonResult = compareFormatValues(format.getPixelCount(), selectedPixelCount); + } else { + comparisonResult = compareFormatValues(format.bitrate, selectedBitrate); + } + selectTrack = isWithinConstraints ? comparisonResult > 0 : comparisonResult < 0; } if (selectTrack) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; + selectedBitrate = format.bitrate; selectedPixelCount = format.getPixelCount(); } } @@ -602,20 +611,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Compares two pixel counts for order. A known pixel count is considered greater than + * Compares two format values for order. A known value is considered greater than * {@link Format#NO_VALUE}. * - * @param first The first pixel count. - * @param second The second pixel count. - * @return A negative integer if the first pixel count is less than the second. Zero if they are - * equal. A positive integer if the first pixel count is greater than the second. + * @param first The first value. + * @param second The second value. + * @return A negative integer if the first value is less than the second. Zero if they are equal. + * A positive integer if the first value is greater than the second. */ - private static int comparePixelCounts(int first, int second) { + private static int compareFormatValues(int first, int second) { return first == Format.NO_VALUE ? (second == Format.NO_VALUE ? 0 : -1) : (second == Format.NO_VALUE ? 1 : (first - second)); } - // Audio track selection implementation. protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport, diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 04f3b986bd..5ca97403f1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -18,11 +18,13 @@ package com.google.android.exoplayer2.ui; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Join; import android.graphics.Paint.Style; +import android.graphics.Rect; import android.graphics.RectF; import android.text.Layout.Alignment; import android.text.StaticLayout; @@ -65,6 +67,7 @@ import com.google.android.exoplayer2.util.Util; // Previous input variables. private CharSequence cueText; private Alignment cueTextAlignment; + private Bitmap cueBitmap; private float cueLine; @Cue.LineType private int cueLineType; @@ -93,6 +96,7 @@ import com.google.android.exoplayer2.util.Util; private int textLeft; private int textTop; private int textPaddingX; + private Rect bitmapRect; @SuppressWarnings("ResourceType") public SubtitlePainter(Context context) { @@ -141,21 +145,28 @@ import com.google.android.exoplayer2.util.Util; public void draw(Cue cue, boolean applyEmbeddedStyles, CaptionStyleCompat style, float textSizePx, float bottomPaddingFraction, Canvas canvas, int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) { - CharSequence cueText = cue.text; - if (TextUtils.isEmpty(cueText)) { - // Nothing to draw. - return; - } - - int windowColor = cue.windowColorSet ? cue.windowColor : style.windowColor; - - if (!applyEmbeddedStyles) { - // Strip out any embedded styling. - cueText = cueText.toString(); - windowColor = style.windowColor; + boolean isTextCue = cue.bitmap == null; + CharSequence cueText = null; + Bitmap cueBitmap = null; + int windowColor = Color.BLACK; + if (isTextCue) { + cueText = cue.text; + if (TextUtils.isEmpty(cueText)) { + // Nothing to draw. + return; + } + windowColor = cue.windowColorSet ? cue.windowColor : style.windowColor; + if (!applyEmbeddedStyles) { + // Strip out any embedded styling. + cueText = cueText.toString(); + windowColor = style.windowColor; + } + } else { + cueBitmap = cue.bitmap; } if (areCharSequencesEqual(this.cueText, cueText) && Util.areEqual(this.cueTextAlignment, cue.textAlignment) + && this.cueBitmap == cueBitmap && this.cueLine == cue.line && this.cueLineType == cue.lineType && Util.areEqual(this.cueLineAnchor, cue.lineAnchor) @@ -176,12 +187,13 @@ import com.google.android.exoplayer2.util.Util; && this.parentRight == cueBoxRight && this.parentBottom == cueBoxBottom) { // We can use the cached layout. - drawLayout(canvas); + drawLayout(canvas, isTextCue); return; } this.cueText = cueText; this.cueTextAlignment = cue.textAlignment; + this.cueBitmap = cueBitmap; this.cueLine = cue.line; this.cueLineType = cue.lineType; this.cueLineAnchor = cue.lineAnchor; @@ -202,6 +214,15 @@ import com.google.android.exoplayer2.util.Util; this.parentRight = cueBoxRight; this.parentBottom = cueBoxBottom; + if (isTextCue) { + setupTextLayout(); + } else { + setupBitmapLayout(); + } + drawLayout(canvas, isTextCue); + } + + private void setupTextLayout() { int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; @@ -237,7 +258,7 @@ import com.google.android.exoplayer2.util.Util; int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft; textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2 - : anchorPosition; + : anchorPosition; textLeft = Math.max(textLeft, parentLeft); textRight = Math.min(textLeft + textWidth, parentRight); } else { @@ -261,7 +282,7 @@ import com.google.android.exoplayer2.util.Util; } textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2 - : anchorPosition; + : anchorPosition; if (textTop + textHeight > parentBottom) { textTop = parentBottom - textHeight; } else if (textTop < parentTop) { @@ -279,17 +300,32 @@ import com.google.android.exoplayer2.util.Util; this.textLeft = textLeft; this.textTop = textTop; this.textPaddingX = textPaddingX; - - drawLayout(canvas); } - /** - * Draws {@link #textLayout} into the provided canvas. - * - * @param canvas The canvas into which to draw. - */ - private void drawLayout(Canvas canvas) { - final StaticLayout layout = textLayout; + private void setupBitmapLayout() { + int parentWidth = parentRight - parentLeft; + int parentHeight = parentBottom - parentTop; + float anchorX = parentLeft + (parentWidth * cuePosition); + float anchorY = parentTop + (parentHeight * cueLine); + int width = Math.round(parentWidth * cueSize); + int height = Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); + int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) + : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); + int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) + : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); + bitmapRect = new Rect(x, y, x + width, y + height); + } + + private void drawLayout(Canvas canvas, boolean isTextCue) { + if (isTextCue) { + drawTextLayout(canvas); + } else { + drawBitmapLayout(canvas); + } + } + + private void drawTextLayout(Canvas canvas) { + StaticLayout layout = textLayout; if (layout == null) { // Nothing to draw. return; @@ -347,6 +383,10 @@ import com.google.android.exoplayer2.util.Util; canvas.restoreToCount(saveCount); } + private void drawBitmapLayout(Canvas canvas) { + canvas.drawBitmap(cueBitmap, null, bitmapRect, null); + } + /** * This method is used instead of {@link TextUtils#equals(CharSequence, CharSequence)} because the * latter only checks the text of each sequence, and does not check for equality of styling that diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 8df8624102..a988cf1a33 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -41,8 +41,8 @@ public interface HttpDataSource extends DataSource { HttpDataSource createDataSource(); /** - * Sets a default request header field for {@link HttpDataSource} instances subsequently - * created by the factory. Previously created instances are not affected. + * Sets a default request header for {@link HttpDataSource} instances subsequently created by + * the factory. Previously created instances are not affected. * * @param name The name of the header field. * @param value The value of the field. @@ -50,16 +50,16 @@ public interface HttpDataSource extends DataSource { void setDefaultRequestProperty(String name, String value); /** - * Clears a default request header field for {@link HttpDataSource} instances subsequently - * created by the factory. Previously created instances are not affected. + * Clears a default request header for {@link HttpDataSource} instances subsequently created by + * the factory. Previously created instances are not affected. * * @param name The name of the header field. */ void clearDefaultRequestProperty(String name); /** - * Clears all default request header fields for all {@link HttpDataSource} instances - * subsequently created by the factory. Previously created instances are not affected. + * Clears all default request header for all {@link HttpDataSource} instances subsequently + * created by the factory. Previously created instances are not affected. */ void clearAllDefaultRequestProperties(); @@ -232,7 +232,7 @@ public interface HttpDataSource extends DataSource { int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException; /** - * Sets the value of a request header field. The value will be used for subsequent connections + * Sets the value of a request header. The value will be used for subsequent connections * established by the source. * * @param name The name of the header field. @@ -241,7 +241,7 @@ public interface HttpDataSource extends DataSource { void setRequestProperty(String name, String value); /** - * Clears the value of a request header field. The change will apply to subsequent connections + * Clears the value of a request header. The change will apply to subsequent connections * established by the source. * * @param name The name of the header field. @@ -249,7 +249,7 @@ public interface HttpDataSource extends DataSource { void clearRequestProperty(String name); /** - * Clears all request header fields that were set by {@link #setRequestProperty(String, String)}. + * Clears all request headers that were set by {@link #setRequestProperty(String, String)}. */ void clearAllRequestProperties(); diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index 64836dae4c..bca90ddc5c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -199,7 +199,7 @@ public final class Loader implements LoaderErrorThrower { currentTask.cancel(true); } if (postLoadAction != null) { - downloadExecutorService.submit(postLoadAction); + downloadExecutorService.execute(postLoadAction); } downloadExecutorService.shutdown(); } @@ -260,7 +260,7 @@ public final class Loader implements LoaderErrorThrower { if (delayMillis > 0) { sendEmptyMessageDelayed(MSG_START, delayMillis); } else { - submitToExecutor(); + execute(); } } @@ -334,7 +334,7 @@ public final class Loader implements LoaderErrorThrower { return; } if (msg.what == MSG_START) { - submitToExecutor(); + execute(); return; } if (msg.what == MSG_FATAL_ERROR) { @@ -367,9 +367,9 @@ public final class Loader implements LoaderErrorThrower { } } - private void submitToExecutor() { + private void execute() { currentError = null; - downloadExecutorService.submit(currentTask); + downloadExecutorService.execute(currentTask); } private void finish() { diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 71397bd403..33b1ca58b0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -32,6 +32,9 @@ import java.io.OutputStream; */ public final class CacheDataSink implements DataSink { + /** Default buffer size. */ + public static final int DEFAULT_BUFFER_SIZE = 20480; + private final Cache cache; private final long maxCacheFileSize; private final int bufferSize; @@ -56,13 +59,15 @@ public final class CacheDataSink implements DataSink { } /** + * Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}. + * * @param cache The cache into which data should be written. * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into * multiple cache files. */ public CacheDataSink(Cache cache, long maxCacheFileSize) { - this(cache, maxCacheFileSize, 0); + this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java index 0c8c006e2c..0b9ab66508 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java @@ -24,18 +24,27 @@ public final class CacheDataSinkFactory implements DataSink.Factory { private final Cache cache; private final long maxCacheFileSize; + private final int bufferSize; /** * @see CacheDataSink#CacheDataSink(Cache, long) */ public CacheDataSinkFactory(Cache cache, long maxCacheFileSize) { + this(cache, maxCacheFileSize, CacheDataSink.DEFAULT_BUFFER_SIZE); + } + + /** + * @see CacheDataSink#CacheDataSink(Cache, long, int) + */ + public CacheDataSinkFactory(Cache cache, long maxCacheFileSize, int bufferSize) { this.cache = cache; this.maxCacheFileSize = maxCacheFileSize; + this.bufferSize = bufferSize; } @Override public DataSink createDataSink() { - return new CacheDataSink(cache, maxCacheFileSize); + return new CacheDataSink(cache, maxCacheFileSize, bufferSize); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 4dc5431b47..dc8797362f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -142,7 +142,8 @@ public final class CacheDataSource implements DataSource { * @param cache The cache. * @param upstream A {@link DataSource} for reading data not in the cache. * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. - * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. + * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is + * accessed read-only. * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. * @param eventListener An optional {@link EventListener} to receive events. @@ -283,7 +284,6 @@ public final class CacheDataSource implements DataSource { currentDataSource = cacheReadDataSource; } else { // Data is not cached, and data is not locked, read from upstream with cache backing. - lockedSpan = span; long length; if (span.isOpenEnded()) { length = bytesRemaining; @@ -294,8 +294,13 @@ public final class CacheDataSource implements DataSource { } } dataSpec = new DataSpec(uri, readPosition, length, key, flags); - currentDataSource = cacheWriteDataSource != null ? cacheWriteDataSource - : upstreamDataSource; + if (cacheWriteDataSource != null) { + currentDataSource = cacheWriteDataSource; + lockedSpan = span; + } else { + currentDataSource = upstreamDataSource; + cache.releaseHoleSpan(span); + } } currentRequestUnbounded = dataSpec.length == C.LENGTH_UNSET; @@ -330,16 +335,16 @@ public final class CacheDataSource implements DataSource { // bytesRemaining == C.LENGTH_UNSET) and got a resolved length from open() request if (currentRequestUnbounded && currentBytesRemaining != C.LENGTH_UNSET) { bytesRemaining = currentBytesRemaining; - // If writing into cache - if (lockedSpan != null) { - setContentLength(dataSpec.position + bytesRemaining); - } + setContentLength(dataSpec.position + bytesRemaining); } return successful; } private void setContentLength(long length) throws IOException { - cache.setContentLength(key, length); + // If writing into cache + if (currentDataSource == cacheWriteDataSource) { + cache.setContentLength(key, length); + } } private void closeCurrentSource() throws IOException { diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 1eb4300142..e9debacc41 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -60,6 +60,7 @@ public final class MimeTypes { public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp"; public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb"; public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/x-flac"; + public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; public static final String TEXT_SSA = BASE_TYPE_TEXT + "/x-ssa"; diff --git a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java index a452871afc..ab2fec0db7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java @@ -103,7 +103,9 @@ public final class NalUnitUtil { 2f }; - private static final int NAL_UNIT_TYPE_SPS = 7; + private static final int H264_NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information + private static final int H264_NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set + private static final int H265_NAL_UNIT_TYPE_PREFIX_SEI = 39; private static final Object scratchEscapePositionsLock = new Object(); @@ -176,7 +178,7 @@ public final class NalUnitUtil { while (offset + 1 < length) { int value = data.get(offset) & 0xFF; if (consecutiveZeros == 3) { - if (value == 1 && (data.get(offset + 1) & 0x1F) == NAL_UNIT_TYPE_SPS) { + if (value == 1 && (data.get(offset + 1) & 0x1F) == H264_NAL_UNIT_TYPE_SPS) { // Copy from this NAL unit onwards to the start of the buffer. ByteBuffer offsetData = data.duplicate(); offsetData.position(offset - 3); @@ -197,6 +199,21 @@ public final class NalUnitUtil { data.clear(); } + /** + * Returns whether the NAL unit with the specified header contains supplemental enhancement + * information. + * + * @param mimeType The sample MIME type. + * @param nalUnitHeaderFirstByte The first byte of nal_unit(). + * @return Whether the NAL unit with the specified header is an SEI NAL unit. + */ + public static boolean isNalUnitSei(String mimeType, byte nalUnitHeaderFirstByte) { + return (MimeTypes.VIDEO_H264.equals(mimeType) + && (nalUnitHeaderFirstByte & 0x1F) == H264_NAL_UNIT_TYPE_SEI) + || (MimeTypes.VIDEO_H265.equals(mimeType) + && ((nalUnitHeaderFirstByte & 0x7E) >> 1) == H265_NAL_UNIT_TYPE_PREFIX_SEI); + } + /** * Returns the type of the NAL unit in {@code data} that starts at {@code offset}. * @@ -297,7 +314,8 @@ public final class NalUnitUtil { int frameCropRightOffset = data.readUnsignedExpGolombCodedInt(); int frameCropTopOffset = data.readUnsignedExpGolombCodedInt(); int frameCropBottomOffset = data.readUnsignedExpGolombCodedInt(); - int cropUnitX, cropUnitY; + int cropUnitX; + int cropUnitY; if (chromaFormatIdc == 0) { cropUnitX = 1; cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java b/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java rename to library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java index a4da5d8e66..ace300c6b1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.extractor; +package com.google.android.exoplayer2.util; import com.google.android.exoplayer2.C; @@ -34,7 +34,7 @@ public final class TimestampAdjuster { */ private static final long MAX_PTS_PLUS_ONE = 0x200000000L; - private final long firstSampleTimestampUs; + public final long firstSampleTimestampUs; private long timestampOffsetUs; @@ -93,6 +93,9 @@ public final class TimestampAdjuster { * @return The adjusted timestamp in microseconds. */ public long adjustTsTimestamp(long pts) { + if (pts == C.TIME_UNSET) { + return C.TIME_UNSET; + } if (lastSampleTimestamp != C.TIME_UNSET) { // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), // and we need to snap to the one closest to lastSampleTimestamp. @@ -113,6 +116,9 @@ public final class TimestampAdjuster { * @return The adjusted timestamp in microseconds. */ public long adjustSampleTimestamp(long timeUs) { + if (timeUs == C.TIME_UNSET) { + return C.TIME_UNSET; + } // Record the adjusted PTS to adjust for wraparound next time. if (lastSampleTimestamp != C.TIME_UNSET) { lastSampleTimestamp = timeUs; diff --git a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 62224a64d6..280f004211 100644 --- a/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.video; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; +import android.graphics.Point; import android.media.MediaCodec; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCrypto; @@ -56,6 +57,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private static final String KEY_CROP_BOTTOM = "crop-bottom"; private static final String KEY_CROP_TOP = "crop-top"; + // Long edge length in pixels for standard video formats, in decreasing in order. + private static final int[] STANDARD_LONG_EDGE_VIDEO_PX = new int[] { + 1920, 1600, 1440, 1280, 960, 854, 640, 540, 480}; + private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; private final EventDispatcher eventDispatcher; private final long allowedJoiningTimeMs; @@ -186,12 +191,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { boolean decoderCapable = decoderInfo.isCodecSupported(format.codecs); if (decoderCapable && format.width > 0 && format.height > 0) { if (Util.SDK_INT >= 21) { - if (format.frameRate > 0) { - decoderCapable = decoderInfo.isVideoSizeAndRateSupportedV21(format.width, format.height, - format.frameRate); - } else { - decoderCapable = decoderInfo.isVideoSizeSupportedV21(format.width, format.height); - } + decoderCapable = decoderInfo.isVideoSizeAndRateSupportedV21(format.width, format.height, + format.frameRate); } else { decoderCapable = format.width * format.height <= MediaCodecUtil.maxH264DecodableFrameSize(); if (!decoderCapable) { @@ -318,8 +319,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected void configureCodec(MediaCodec codec, Format format, MediaCrypto crypto) { - codecMaxValues = getCodecMaxValues(format, streamFormats); + protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, + MediaCrypto crypto) throws DecoderQueryException { + codecMaxValues = getCodecMaxValues(codecInfo, format, streamFormats); MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround, tunnelingAudioSessionId); codec.configure(mediaFormat, surface, crypto, 0); @@ -597,29 +599,92 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * Returns {@link CodecMaxValues} suitable for configuring a codec for {@code format} in a way * that will allow possible adaptation to other compatible formats in {@code streamFormats}. * + * @param codecInfo Information about the {@link MediaCodec} being configured. * @param format The format for which the codec is being configured. * @param streamFormats The possible stream formats. * @return Suitable {@link CodecMaxValues}. + * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - private static CodecMaxValues getCodecMaxValues(Format format, Format[] streamFormats) { + private static CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format, + Format[] streamFormats) throws DecoderQueryException { int maxWidth = format.width; int maxHeight = format.height; int maxInputSize = getMaxInputSize(format); + if (streamFormats.length == 1) { + // The single entry in streamFormats must correspond to the format for which the codec is + // being configured. + return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); + } + boolean haveUnknownDimensions = false; for (Format streamFormat : streamFormats) { if (areAdaptationCompatible(format, streamFormat)) { + haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE + || streamFormat.height == Format.NO_VALUE); maxWidth = Math.max(maxWidth, streamFormat.width); maxHeight = Math.max(maxHeight, streamFormat.height); maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat)); } } + if (haveUnknownDimensions) { + Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight); + Point codecMaxSize = getCodecMaxSize(codecInfo, format); + if (codecMaxSize != null) { + maxWidth = Math.max(maxWidth, codecMaxSize.x); + maxHeight = Math.max(maxHeight, codecMaxSize.y); + maxInputSize = Math.max(maxInputSize, + getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight)); + Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight); + } + } return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); } + /** + * Returns a maximum video size to use when configuring a codec for {@code format} in a way + * that will allow possible adaptation to other compatible formats that are expected to have the + * same aspect ratio, but whose sizes are unknown. + * + * @param codecInfo Information about the {@link MediaCodec} being configured. + * @param format The format for which the codec is being configured. + * @return The maximum video size to use, or null if the size of {@code format} should be used. + * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. + */ + private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) + throws DecoderQueryException { + boolean isVerticalVideo = format.height > format.width; + int formatLongEdgePx = isVerticalVideo ? format.height : format.width; + int formatShortEdgePx = isVerticalVideo ? format.width : format.height; + float aspectRatio = (float) formatShortEdgePx / formatLongEdgePx; + for (int longEdgePx : STANDARD_LONG_EDGE_VIDEO_PX) { + int shortEdgePx = (int) (longEdgePx * aspectRatio); + if (longEdgePx <= formatLongEdgePx || shortEdgePx <= formatShortEdgePx) { + // Don't return a size not larger than the format for which the codec is being configured. + return null; + } else if (Util.SDK_INT >= 21) { + Point alignedSize = codecInfo.alignVideoSizeV21(isVerticalVideo ? shortEdgePx : longEdgePx, + isVerticalVideo ? longEdgePx : shortEdgePx); + float frameRate = format.frameRate; + if (codecInfo.isVideoSizeAndRateSupportedV21(alignedSize.x, alignedSize.y, frameRate)) { + return alignedSize; + } + } else { + // Conservatively assume the codec requires 16px width and height alignment. + longEdgePx = Util.ceilDivide(longEdgePx, 16) * 16; + shortEdgePx = Util.ceilDivide(shortEdgePx, 16) * 16; + if (longEdgePx * shortEdgePx <= MediaCodecUtil.maxH264DecodableFrameSize()) { + return new Point(isVerticalVideo ? shortEdgePx : longEdgePx, + isVerticalVideo ? longEdgePx : shortEdgePx); + } + } + } + return null; + } + /** * Returns a maximum input size for a given format. * * @param format The format. - * @return An maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be + * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be * determined. */ private static int getMaxInputSize(Format format) { @@ -627,8 +692,20 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // The format defines an explicit maximum input size. return format.maxInputSize; } + return getMaxInputSize(format.sampleMimeType, format.width, format.height); + } - if (format.width == Format.NO_VALUE || format.height == Format.NO_VALUE) { + /** + * Returns a maximum input size for a given mime type, width and height. + * + * @param sampleMimeType The format mime type. + * @param width The width in pixels. + * @param height The height in pixels. + * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be + * determined. + */ + private static int getMaxInputSize(String sampleMimeType, int width, int height) { + if (width == Format.NO_VALUE || height == Format.NO_VALUE) { // We can't infer a maximum input size without video dimensions. return Format.NO_VALUE; } @@ -636,10 +713,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // Attempt to infer a maximum input size from the format. int maxPixels; int minCompressionRatio; - switch (format.sampleMimeType) { + switch (sampleMimeType) { case MimeTypes.VIDEO_H263: case MimeTypes.VIDEO_MP4V: - maxPixels = format.width * format.height; + maxPixels = width * height; minCompressionRatio = 2; break; case MimeTypes.VIDEO_H264: @@ -649,17 +726,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Format.NO_VALUE; } // Round up width/height to an integer number of macroblocks. - maxPixels = ((format.width + 15) / 16) * ((format.height + 15) / 16) * 16 * 16; + maxPixels = Util.ceilDivide(width, 16) * Util.ceilDivide(height, 16) * 16 * 16; minCompressionRatio = 2; break; case MimeTypes.VIDEO_VP8: // VPX does not specify a ratio so use the values from the platform's SoftVPX.cpp. - maxPixels = format.width * format.height; + maxPixels = width * height; minCompressionRatio = 2; break; case MimeTypes.VIDEO_H265: case MimeTypes.VIDEO_VP9: - maxPixels = format.width * format.height; + maxPixels = width * height; minCompressionRatio = 4; break; default: diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java new file mode 100644 index 0000000000..24765f282d --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2017 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.exoplayer2.playbacktests.gts; + +import static com.google.android.exoplayer2.C.WIDEVINE_UUID; + +import android.annotation.TargetApi; +import android.app.Instrumentation; +import android.media.MediaDrm; +import android.media.UnsupportedSchemeException; +import android.net.Uri; +import android.util.Log; +import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.drm.MediaDrmCallback; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; +import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; +import com.google.android.exoplayer2.playbacktests.util.DebugSimpleExoPlayer; +import com.google.android.exoplayer2.playbacktests.util.DecoderCountersUtil; +import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; +import com.google.android.exoplayer2.playbacktests.util.HostActivity; +import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest; +import com.google.android.exoplayer2.playbacktests.util.MetricsLogger; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.trackselection.FixedTrackSelection; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.RandomTrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import junit.framework.AssertionFailedError; + +/** + * A {@link HostedTest} for DASH playback tests. + */ +@TargetApi(16) +public final class DashHostedTest extends ExoHostedTest { + + /** {@link DashHostedTest} builder. */ + public static final class Builder { + + private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000; + + private static final String REPORT_NAME = "GtsExoPlayerTestCases"; + private static final String REPORT_OBJECT_NAME = "playbacktest"; + + // Whether adaptive tests should enable video formats beyond those mandated by the Android CDD + // if the device advertises support for them. + private static final boolean ALLOW_ADDITIONAL_VIDEO_FORMATS = Util.SDK_INT >= 24; + + private final String tag; + + private String streamName; + private boolean fullPlaybackNoSeeking; + private String audioFormat; + private boolean canIncludeAdditionalVideoFormats; + private ActionSchedule actionSchedule; + private byte[] offlineLicenseKeySetId; + private String[] videoFormats; + private String manifestUrl; + private boolean useL1Widevine; + private String widevineLicenseUrl; + + public Builder(String tag) { + this.tag = tag; + } + + public Builder setStreamName(String streamName) { + this.streamName = streamName; + return this; + } + + public Builder setFullPlaybackNoSeeking(boolean fullPlaybackNoSeeking) { + this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; + return this; + } + + public Builder setCanIncludeAdditionalVideoFormats( + boolean canIncludeAdditionalVideoFormats) { + this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats + && ALLOW_ADDITIONAL_VIDEO_FORMATS; + return this; + } + + public Builder setActionSchedule(ActionSchedule actionSchedule) { + this.actionSchedule = actionSchedule; + return this; + } + + public Builder setOfflineLicenseKeySetId(byte[] offlineLicenseKeySetId) { + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + return this; + } + + public Builder setAudioVideoFormats(String audioFormat, String... videoFormats) { + this.audioFormat = audioFormat; + this.videoFormats = videoFormats; + return this; + } + + public Builder setManifestUrl(String manifestUrl) { + this.manifestUrl = MANIFEST_URL_PREFIX + manifestUrl; + return this; + } + + public Builder setManifestUrlForWidevine(String manifestUrl, String videoMimeType) { + this.useL1Widevine = isL1WidevineAvailable(videoMimeType); + this.manifestUrl = getWidevineManifestUrl(manifestUrl, useL1Widevine); + this.widevineLicenseUrl = getWidevineLicenseUrl(useL1Widevine); + return this; + } + + private DashHostedTest createDashHostedTest(boolean canIncludeAdditionalVideoFormats, + boolean isCddLimitedRetry, Instrumentation instrumentation) { + MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(instrumentation, tag, + REPORT_NAME, REPORT_OBJECT_NAME); + return new DashHostedTest(tag, streamName, manifestUrl, metricsLogger, fullPlaybackNoSeeking, + audioFormat, canIncludeAdditionalVideoFormats, isCddLimitedRetry, actionSchedule, + offlineLicenseKeySetId, widevineLicenseUrl, useL1Widevine, videoFormats); + } + + public void runTest(HostActivity activity, Instrumentation instrumentation) { + DashHostedTest test = createDashHostedTest(canIncludeAdditionalVideoFormats, false, + instrumentation); + activity.runTest(test, TEST_TIMEOUT_MS); + // Retry test exactly once if adaptive test fails due to excessive dropped buffers when + // playing non-CDD required formats (b/28220076). + if (test.needsCddLimitedRetry) { + activity.runTest(createDashHostedTest(false, true, instrumentation), TEST_TIMEOUT_MS); + } + } + + } + + private static final String AUDIO_TAG_SUFFIX = ":Audio"; + private static final String VIDEO_TAG_SUFFIX = ":Video"; + static final int VIDEO_RENDERER_INDEX = 0; + static final int AUDIO_RENDERER_INDEX = 1; + + private static final int MIN_LOADABLE_RETRY_COUNT = 10; + private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10; + private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; + + private static final String MANIFEST_URL_PREFIX = "https://storage.googleapis.com/exoplayer-test-" + + "media-1/gen-3/screens/dash-vod-single-segment/"; + + private static final String WIDEVINE_L1_SUFFIX = "-hw.mpd"; + private static final String WIDEVINE_L3_SUFFIX = "-sw.mpd"; + + private static final String WIDEVINE_LICENSE_URL = + "https://proxy.uat.widevine.com/proxy?provider=widevine_test&video_id="; + private static final String WIDEVINE_SW_CRYPTO_CONTENT_ID = "exoplayer_test_1"; + private static final String WIDEVINE_HW_SECURE_DECODE_CONTENT_ID = "exoplayer_test_2"; + private static final String WIDEVINE_SECURITY_LEVEL_1 = "L1"; + private static final String WIDEVINE_SECURITY_LEVEL_3 = "L3"; + private static final String SECURITY_LEVEL_PROPERTY = "securityLevel"; + + private final String streamName; + private final String manifestUrl; + private final MetricsLogger metricsLogger; + private final boolean fullPlaybackNoSeeking; + private final boolean isCddLimitedRetry; + private final DashTestTrackSelector trackSelector; + private final byte[] offlineLicenseKeySetId; + private final String widevineLicenseUrl; + private final boolean useL1Widevine; + + boolean needsCddLimitedRetry; + + public static String getWidevineManifestUrl(String manifestUrl, boolean useL1Widevine) { + return MANIFEST_URL_PREFIX + manifestUrl + + (useL1Widevine ? WIDEVINE_L1_SUFFIX : WIDEVINE_L3_SUFFIX); + } + + public static String getWidevineLicenseUrl(boolean useL1Widevine) { + return WIDEVINE_LICENSE_URL + + (useL1Widevine ? WIDEVINE_HW_SECURE_DECODE_CONTENT_ID : WIDEVINE_SW_CRYPTO_CONTENT_ID); + } + + @TargetApi(18) + @SuppressWarnings("ResourceType") + public static boolean isL1WidevineAvailable(String videoMimeType) { + try { + // Force L3 if secure decoder is not available. + if (MediaCodecUtil.getDecoderInfo(videoMimeType, true) == null) { + return false; + } + + MediaDrm mediaDrm = new MediaDrm(WIDEVINE_UUID); + String securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY); + mediaDrm.release(); + return WIDEVINE_SECURITY_LEVEL_1.equals(securityProperty); + } catch (MediaCodecUtil.DecoderQueryException | UnsupportedSchemeException e) { + throw new IllegalStateException(e); + } + } + + /** + * @param tag A tag to use for logging. + * @param streamName The name of the test stream for metric logging. + * @param manifestUrl The manifest url. + * @param metricsLogger Logger to log metrics from the test. + * @param fullPlaybackNoSeeking Whether the test will play the entire source with no seeking. + * @param audioFormat The audio format. + * @param canIncludeAdditionalVideoFormats Whether to use video formats in addition to those + * listed in the videoFormats argument, if the device is capable of playing them. + * @param isCddLimitedRetry Whether this is a CDD limited retry following a previous failure. + * @param actionSchedule The action schedule for the test. + * @param offlineLicenseKeySetId The key set id of the license to be used. + * @param widevineLicenseUrl If the video is Widevine encrypted, this is the license url + * otherwise null. + * @param useL1Widevine Whether to use L1 Widevine. + * @param videoFormats The video formats. + */ + private DashHostedTest(String tag, String streamName, String manifestUrl, + MetricsLogger metricsLogger, boolean fullPlaybackNoSeeking, String audioFormat, + boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry, + ActionSchedule actionSchedule, byte[] offlineLicenseKeySetId, String widevineLicenseUrl, + boolean useL1Widevine, String... videoFormats) { + super(tag, fullPlaybackNoSeeking); + Assertions.checkArgument(!(isCddLimitedRetry && canIncludeAdditionalVideoFormats)); + this.streamName = streamName; + this.manifestUrl = manifestUrl; + this.metricsLogger = metricsLogger; + this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; + this.isCddLimitedRetry = isCddLimitedRetry; + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + this.widevineLicenseUrl = widevineLicenseUrl; + this.useL1Widevine = useL1Widevine; + trackSelector = new DashTestTrackSelector(tag, audioFormat, videoFormats, + canIncludeAdditionalVideoFormats); + if (actionSchedule != null) { + setSchedule(actionSchedule); + } + } + + @Override + protected MappingTrackSelector buildTrackSelector(HostActivity host, + BandwidthMeter bandwidthMeter) { + return trackSelector; + } + + @Override + protected DefaultDrmSessionManager buildDrmSessionManager( + final String userAgent) { + if (widevineLicenseUrl == null) { + return null; + } + try { + MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, + new DefaultHttpDataSourceFactory(userAgent)); + DefaultDrmSessionManager drmSessionManager = + DefaultDrmSessionManager.newWidevineInstance(drmCallback, null, null, null); + if (!useL1Widevine) { + drmSessionManager.setPropertyString( + SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); + } + if (offlineLicenseKeySetId != null) { + drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, + offlineLicenseKeySetId); + } + return drmSessionManager; + } catch (UnsupportedDrmException e) { + throw new IllegalStateException(e); + } + } + + @Override + protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, + MappingTrackSelector trackSelector, + DrmSessionManager drmSessionManager) { + SimpleExoPlayer player = new DebugSimpleExoPlayer(host, trackSelector, + new DefaultLoadControl(), drmSessionManager); + player.setVideoSurface(surface); + return player; + } + + @Override + protected MediaSource buildSource(HostActivity host, String userAgent, + TransferListener mediaTransferListener) { + DataSource.Factory manifestDataSourceFactory = new DefaultDataSourceFactory(host, userAgent); + DataSource.Factory mediaDataSourceFactory = new DefaultDataSourceFactory(host, userAgent, + mediaTransferListener); + Uri manifestUri = Uri.parse(manifestUrl); + DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( + mediaDataSourceFactory); + return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory, + MIN_LOADABLE_RETRY_COUNT, 0 /* livePresentationDelayMs */, null, null); + } + + @Override + protected void logMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters) { + metricsLogger.logMetric(MetricsLogger.KEY_TEST_NAME, streamName); + metricsLogger.logMetric(MetricsLogger.KEY_IS_CDD_LIMITED_RETRY, isCddLimitedRetry); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_DROPPED_COUNT, + videoCounters.droppedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT, + videoCounters.maxConsecutiveDroppedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_SKIPPED_COUNT, + videoCounters.skippedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_RENDERED_COUNT, + videoCounters.renderedOutputBufferCount); + metricsLogger.close(); + } + + @Override + protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { + if (fullPlaybackNoSeeking) { + // We shouldn't have skipped any output buffers. + DecoderCountersUtil.assertSkippedOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, 0); + DecoderCountersUtil.assertSkippedOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, 0); + // We allow one fewer output buffer due to the way that MediaCodecRenderer and the + // underlying decoders handle the end of stream. This should be tightened up in the future. + DecoderCountersUtil.assertTotalOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, + audioCounters.inputBufferCount - 1, audioCounters.inputBufferCount); + DecoderCountersUtil.assertTotalOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, + videoCounters.inputBufferCount - 1, videoCounters.inputBufferCount); + } + try { + int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION + * DecoderCountersUtil.getTotalOutputBuffers(videoCounters)); + // Assert that performance is acceptable. + // Assert that total dropped frames were within limit. + DecoderCountersUtil.assertDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, videoCounters, + droppedFrameLimit); + // Assert that consecutive dropped frames were within limit. + DecoderCountersUtil.assertConsecutiveDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, + videoCounters, MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES); + } catch (AssertionFailedError e) { + if (trackSelector.includedAdditionalVideoFormats) { + // Retry limiting to CDD mandated formats (b/28220076). + Log.e(tag, "Too many dropped or consecutive dropped frames.", e); + needsCddLimitedRetry = true; + } else { + throw e; + } + } + } + + private static final class DashTestTrackSelector extends MappingTrackSelector { + + private final String tag; + private final String audioFormatId; + private final String[] videoFormatIds; + private final boolean canIncludeAdditionalVideoFormats; + + public boolean includedAdditionalVideoFormats; + + private DashTestTrackSelector(String tag, String audioFormatId, String[] videoFormatIds, + boolean canIncludeAdditionalVideoFormats) { + this.tag = tag; + this.audioFormatId = audioFormatId; + this.videoFormatIds = videoFormatIds; + this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats; + } + + @Override + protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + throws ExoPlaybackException { + Assertions.checkState(rendererCapabilities[VIDEO_RENDERER_INDEX].getTrackType() + == C.TRACK_TYPE_VIDEO); + Assertions.checkState(rendererCapabilities[AUDIO_RENDERER_INDEX].getTrackType() + == C.TRACK_TYPE_AUDIO); + Assertions.checkState(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].length == 1); + Assertions.checkState(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].length == 1); + TrackSelection[] selections = new TrackSelection[rendererCapabilities.length]; + selections[VIDEO_RENDERER_INDEX] = new RandomTrackSelection( + rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), + getVideoTrackIndices(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), + rendererFormatSupports[VIDEO_RENDERER_INDEX][0], videoFormatIds, + canIncludeAdditionalVideoFormats), + 0 /* seed */); + selections[AUDIO_RENDERER_INDEX] = new FixedTrackSelection( + rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), + getTrackIndex(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), audioFormatId)); + includedAdditionalVideoFormats = + selections[VIDEO_RENDERER_INDEX].length() > videoFormatIds.length; + return selections; + } + + private int[] getVideoTrackIndices(TrackGroup trackGroup, int[] formatSupport, + String[] formatIds, boolean canIncludeAdditionalFormats) { + List trackIndices = new ArrayList<>(); + + // Always select explicitly listed representations. + for (String formatId : formatIds) { + int trackIndex = getTrackIndex(trackGroup, formatId); + Log.d(tag, "Adding base video format: " + + Format.toLogString(trackGroup.getFormat(trackIndex))); + trackIndices.add(trackIndex); + } + + // Select additional video representations, if supported by the device. + if (canIncludeAdditionalFormats) { + for (int i = 0; i < trackGroup.length; i++) { + if (!trackIndices.contains(i) && isFormatHandled(formatSupport[i])) { + Log.d(tag, "Adding extra video format: " + + Format.toLogString(trackGroup.getFormat(i))); + trackIndices.add(i); + } + } + } + + int[] trackIndicesArray = Util.toArray(trackIndices); + Arrays.sort(trackIndicesArray); + return trackIndicesArray; + } + + private static int getTrackIndex(TrackGroup trackGroup, String formatId) { + for (int i = 0; i < trackGroup.length; i++) { + if (trackGroup.getFormat(i).id.equals(formatId)) { + return i; + } + } + throw new IllegalStateException("Format " + formatId + " not found."); + } + + private static boolean isFormatHandled(int formatSupport) { + return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) + == RendererCapabilities.FORMAT_HANDLED; + } + + } + +} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java index 6b561bc81c..6ae66f24e1 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -15,57 +15,15 @@ */ package com.google.android.exoplayer2.playbacktests.gts; -import android.annotation.TargetApi; -import android.media.MediaDrm; -import android.media.UnsupportedSchemeException; -import android.net.Uri; import android.test.ActivityInstrumentationTestCase2; -import android.util.Log; -import android.view.Surface; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; -import com.google.android.exoplayer2.playbacktests.util.DebugSimpleExoPlayer; -import com.google.android.exoplayer2.playbacktests.util.DecoderCountersUtil; -import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; import com.google.android.exoplayer2.playbacktests.util.HostActivity; -import com.google.android.exoplayer2.playbacktests.util.MetricsLogger; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.trackselection.FixedTrackSelection; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.RandomTrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.upstream.BandwidthMeter; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import junit.framework.AssertionFailedError; /** * Tests DASH playbacks using {@link ExoPlayer}. @@ -73,147 +31,6 @@ import junit.framework.AssertionFailedError; public final class DashTest extends ActivityInstrumentationTestCase2 { private static final String TAG = "DashTest"; - private static final String VIDEO_TAG = TAG + ":Video"; - private static final String AUDIO_TAG = TAG + ":Audio"; - private static final String REPORT_NAME = "GtsExoPlayerTestCases"; - private static final String REPORT_OBJECT_NAME = "playbacktest"; - private static final int VIDEO_RENDERER_INDEX = 0; - private static final int AUDIO_RENDERER_INDEX = 1; - - private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000; - private static final int MIN_LOADABLE_RETRY_COUNT = 10; - private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10; - private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; - - private static final String MANIFEST_URL_PREFIX = "https://storage.googleapis.com/exoplayer-test-" - + "media-1/gen-3/screens/dash-vod-single-segment/"; - // Clear content manifests. - private static final String H264_MANIFEST = "manifest-h264.mpd"; - private static final String H265_MANIFEST = "manifest-h265.mpd"; - private static final String VP9_MANIFEST = "manifest-vp9.mpd"; - private static final String H264_23_MANIFEST = "manifest-h264-23.mpd"; - private static final String H264_24_MANIFEST = "manifest-h264-24.mpd"; - private static final String H264_29_MANIFEST = "manifest-h264-29.mpd"; - // Widevine encrypted content manifests. - private static final String WIDEVINE_H264_MANIFEST_PREFIX = "manifest-h264-enc"; - private static final String WIDEVINE_H265_MANIFEST_PREFIX = "manifest-h265-enc"; - private static final String WIDEVINE_VP9_MANIFEST_PREFIX = "manifest-vp9-enc"; - private static final String WIDEVINE_H264_23_MANIFEST_PREFIX = "manifest-h264-23-enc"; - private static final String WIDEVINE_H264_24_MANIFEST_PREFIX = "manifest-h264-24-enc"; - private static final String WIDEVINE_H264_29_MANIFEST_PREFIX = "manifest-h264-29-enc"; - private static final String WIDEVINE_L1_SUFFIX = "-hw.mpd"; - private static final String WIDEVINE_L3_SUFFIX = "-sw.mpd"; - - private static final String AAC_AUDIO_REPRESENTATION_ID = "141"; - private static final String H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "avc-baseline-240"; - private static final String H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "avc-baseline-480"; - private static final String H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "avc-main-240"; - private static final String H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "avc-main-480"; - // The highest quality H264 format mandated by the Android CDD. - private static final String H264_CDD_FIXED = Util.SDK_INT < 23 - ? H264_BASELINE_480P_VIDEO_REPRESENTATION_ID : H264_MAIN_480P_VIDEO_REPRESENTATION_ID; - // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile - // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 - // when switching between baseline and main profiles on certain devices. - private static final String[] H264_CDD_ADAPTIVE = Util.SDK_INT < 24 - ? new String[] { - H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} - : new String[] { - H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, - H264_MAIN_240P_VIDEO_REPRESENTATION_ID, - H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; - - private static final String H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = - "avc-baseline-480-23"; - private static final String H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = - "avc-baseline-480-24"; - private static final String H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = - "avc-baseline-480-29"; - - private static final String H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "hevc-main-288"; - private static final String H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "hevc-main-360"; - // The highest quality H265 format mandated by the Android CDD. - private static final String H265_CDD_FIXED = H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; - // Multiple H265 formats mandated by the Android CDD. - private static final String[] H265_CDD_ADAPTIVE = - new String[] { - H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, - H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; - - private static final String VORBIS_AUDIO_REPRESENTATION_ID = "4"; - private static final String VP9_180P_VIDEO_REPRESENTATION_ID = "0"; - private static final String VP9_360P_VIDEO_REPRESENTATION_ID = "1"; - // The highest quality VP9 format mandated by the Android CDD. - private static final String VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; - // Multiple VP9 formats mandated by the Android CDD. - private static final String[] VP9_CDD_ADAPTIVE = - new String[] { - VP9_180P_VIDEO_REPRESENTATION_ID, - VP9_360P_VIDEO_REPRESENTATION_ID}; - - // Widevine encrypted content representation ids. - private static final String WIDEVINE_AAC_AUDIO_REPRESENTATION_ID = "0"; - private static final String WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "1"; - private static final String WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "2"; - private static final String WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "3"; - private static final String WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "4"; - // The highest quality H264 format mandated by the Android CDD. - private static final String WIDEVINE_H264_CDD_FIXED = Util.SDK_INT < 23 - ? WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID - : WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID; - // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile - // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 - // when switching between baseline and main profiles on certain devices. - private static final String[] WIDEVINE_H264_CDD_ADAPTIVE = Util.SDK_INT < 24 - ? new String[] { - WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} - : new String[] { - WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; - - private static final String WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = "2"; - private static final String WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = "2"; - private static final String WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = "2"; - - private static final String WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "1"; - private static final String WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "2"; - // The highest quality H265 format mandated by the Android CDD. - private static final String WIDEVINE_H265_CDD_FIXED = - WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; - // Multiple H265 formats mandated by the Android CDD. - private static final String[] WIDEVINE_H265_CDD_ADAPTIVE = - new String[] { - WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; - - private static final String WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID = "0"; - private static final String WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID = "1"; - private static final String WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID = "2"; - // The highest quality VP9 format mandated by the Android CDD. - private static final String WIDEVINE_VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; - // Multiple VP9 formats mandated by the Android CDD. - private static final String[] WIDEVINE_VP9_CDD_ADAPTIVE = - new String[] { - WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID, - WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID}; - - private static final String WIDEVINE_LICENSE_URL = - "https://proxy.uat.widevine.com/proxy?provider=widevine_test&video_id="; - private static final String WIDEVINE_SW_CRYPTO_CONTENT_ID = "exoplayer_test_1"; - private static final String WIDEVINE_HW_SECURE_DECODE_CONTENT_ID = "exoplayer_test_2"; - private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); - private static final String WIDEVINE_SECURITY_LEVEL_1 = "L1"; - private static final String WIDEVINE_SECURITY_LEVEL_3 = "L3"; - private static final String SECURITY_LEVEL_PROPERTY = "securityLevel"; - - // Whether adaptive tests should enable video formats beyond those mandated by the Android CDD - // if the device advertises support for them. - private static final boolean ALLOW_ADDITIONAL_VIDEO_FORMATS = Util.SDK_INT >= 24; private static final ActionSchedule SEEKING_SCHEDULE = new ActionSchedule.Builder(TAG) .delay(10000).seek(15000) @@ -223,33 +40,33 @@ public final class DashTest extends ActivityInstrumentationTestCase2 buildDrmSessionManager( - final String userAgent) { - DefaultDrmSessionManager drmSessionManager = null; - if (isWidevineEncrypted) { - try { - // Force L3 if secure decoder is not available. - boolean forceL3Widevine = MediaCodecUtil.getDecoderInfo(videoMimeType, true) == null; - MediaDrm mediaDrm = new MediaDrm(WIDEVINE_UUID); - String securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY); - String widevineContentId = forceL3Widevine ? WIDEVINE_SW_CRYPTO_CONTENT_ID - : WIDEVINE_SECURITY_LEVEL_1.equals(securityProperty) - ? WIDEVINE_HW_SECURE_DECODE_CONTENT_ID : WIDEVINE_SW_CRYPTO_CONTENT_ID; - HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback( - WIDEVINE_LICENSE_URL + widevineContentId, - new DefaultHttpDataSourceFactory(userAgent)); - drmSessionManager = DefaultDrmSessionManager.newWidevineInstance(drmCallback, null, - null, null); - if (forceL3Widevine && !WIDEVINE_SECURITY_LEVEL_3.equals(securityProperty)) { - drmSessionManager.setPropertyString(SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); - } - // Check if secure video decoder is required. - securityProperty = drmSessionManager.getPropertyString(SECURITY_LEVEL_PROPERTY); - needsSecureVideoDecoder = WIDEVINE_SECURITY_LEVEL_1.equals(securityProperty); - } catch (MediaCodecUtil.DecoderQueryException | UnsupportedSchemeException - | UnsupportedDrmException e) { - throw new IllegalStateException(e); - } - } - return drmSessionManager; - } - - @Override - protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, - MappingTrackSelector trackSelector, - DrmSessionManager drmSessionManager) { - SimpleExoPlayer player = new DebugSimpleExoPlayer(host, trackSelector, - new DefaultLoadControl(), drmSessionManager); - player.setVideoSurface(surface); - return player; - } - - @Override - protected MediaSource buildSource(HostActivity host, String userAgent, - TransferListener mediaTransferListener) { - DataSource.Factory manifestDataSourceFactory = new DefaultDataSourceFactory(host, userAgent); - DataSource.Factory mediaDataSourceFactory = new DefaultDataSourceFactory(host, userAgent, - mediaTransferListener); - String manifestUrl = manifestPath; - manifestUrl += isWidevineEncrypted ? (needsSecureVideoDecoder ? WIDEVINE_L1_SUFFIX - : WIDEVINE_L3_SUFFIX) : ""; - Uri manifestUri = Uri.parse(manifestUrl); - DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( - mediaDataSourceFactory); - return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory, - MIN_LOADABLE_RETRY_COUNT, 0 /* livePresentationDelayMs */, null, null); - } - - @Override - protected void logMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters) { - metricsLogger.logMetric(MetricsLogger.KEY_TEST_NAME, streamName); - metricsLogger.logMetric(MetricsLogger.KEY_IS_CDD_LIMITED_RETRY, isCddLimitedRetry); - metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_DROPPED_COUNT, - videoCounters.droppedOutputBufferCount); - metricsLogger.logMetric(MetricsLogger.KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT, - videoCounters.maxConsecutiveDroppedOutputBufferCount); - metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_SKIPPED_COUNT, - videoCounters.skippedOutputBufferCount); - metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_RENDERED_COUNT, - videoCounters.renderedOutputBufferCount); - metricsLogger.close(); - } - - @Override - protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { - if (fullPlaybackNoSeeking) { - // We shouldn't have skipped any output buffers. - DecoderCountersUtil.assertSkippedOutputBufferCount(AUDIO_TAG, audioCounters, 0); - DecoderCountersUtil.assertSkippedOutputBufferCount(VIDEO_TAG, videoCounters, 0); - // We allow one fewer output buffer due to the way that MediaCodecRenderer and the - // underlying decoders handle the end of stream. This should be tightened up in the future. - DecoderCountersUtil.assertTotalOutputBufferCount(AUDIO_TAG, audioCounters, - audioCounters.inputBufferCount - 1, audioCounters.inputBufferCount); - DecoderCountersUtil.assertTotalOutputBufferCount(VIDEO_TAG, videoCounters, - videoCounters.inputBufferCount - 1, videoCounters.inputBufferCount); - } - try { - int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION - * DecoderCountersUtil.getTotalOutputBuffers(videoCounters)); - // Assert that performance is acceptable. - // Assert that total dropped frames were within limit. - DecoderCountersUtil.assertDroppedOutputBufferLimit(VIDEO_TAG, videoCounters, - droppedFrameLimit); - // Assert that consecutive dropped frames were within limit. - DecoderCountersUtil.assertConsecutiveDroppedOutputBufferLimit(VIDEO_TAG, videoCounters, - MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES); - } catch (AssertionFailedError e) { - if (trackSelector.includedAdditionalVideoFormats) { - // Retry limiting to CDD mandated formats (b/28220076). - Log.e(TAG, "Too many dropped or consecutive dropped frames.", e); - needsCddLimitedRetry = true; - } else { - throw e; - } - } - } - - } - - private static final class DashTestTrackSelector extends MappingTrackSelector { - - private final String audioFormatId; - private final String[] videoFormatIds; - private final boolean canIncludeAdditionalVideoFormats; - - public boolean includedAdditionalVideoFormats; - - private DashTestTrackSelector(String audioFormatId, String[] videoFormatIds, - boolean canIncludeAdditionalVideoFormats) { - this.audioFormatId = audioFormatId; - this.videoFormatIds = videoFormatIds; - this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats; - } - - @Override - protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) - throws ExoPlaybackException { - Assertions.checkState(rendererCapabilities[VIDEO_RENDERER_INDEX].getTrackType() - == C.TRACK_TYPE_VIDEO); - Assertions.checkState(rendererCapabilities[AUDIO_RENDERER_INDEX].getTrackType() - == C.TRACK_TYPE_AUDIO); - Assertions.checkState(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].length == 1); - Assertions.checkState(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].length == 1); - TrackSelection[] selections = new TrackSelection[rendererCapabilities.length]; - selections[VIDEO_RENDERER_INDEX] = new RandomTrackSelection( - rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), - getVideoTrackIndices(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), - rendererFormatSupports[VIDEO_RENDERER_INDEX][0], videoFormatIds, - canIncludeAdditionalVideoFormats), - 0 /* seed */); - selections[AUDIO_RENDERER_INDEX] = new FixedTrackSelection( - rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), - getTrackIndex(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), audioFormatId)); - includedAdditionalVideoFormats = - selections[VIDEO_RENDERER_INDEX].length() > videoFormatIds.length; - return selections; - } - - private static int[] getVideoTrackIndices(TrackGroup trackGroup, int[] formatSupport, - String[] formatIds, boolean canIncludeAdditionalFormats) { - List trackIndices = new ArrayList<>(); - - // Always select explicitly listed representations. - for (String formatId : formatIds) { - int trackIndex = getTrackIndex(trackGroup, formatId); - Log.d(TAG, "Adding base video format: " - + Format.toLogString(trackGroup.getFormat(trackIndex))); - trackIndices.add(trackIndex); - } - - // Select additional video representations, if supported by the device. - if (canIncludeAdditionalFormats) { - for (int i = 0; i < trackGroup.length; i++) { - if (!trackIndices.contains(i) && isFormatHandled(formatSupport[i])) { - Log.d(TAG, "Adding extra video format: " - + Format.toLogString(trackGroup.getFormat(i))); - trackIndices.add(i); - } - } - } - - int[] trackIndicesArray = Util.toArray(trackIndices); - Arrays.sort(trackIndicesArray); - return trackIndicesArray; - } - - private static int getTrackIndex(TrackGroup trackGroup, String formatId) { - for (int i = 0; i < trackGroup.length; i++) { - if (trackGroup.getFormat(i).id.equals(formatId)) { - return i; - } - } - throw new IllegalStateException("Format " + formatId + " not found."); - } - - private static boolean isFormatHandled(int formatSupport) { - return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) - == RendererCapabilities.FORMAT_HANDLED; - } - - } - } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java new file mode 100644 index 0000000000..c95614bc87 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2017 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.exoplayer2.playbacktests.gts; + +import com.google.android.exoplayer2.util.Util; + +/** + * Test data for {@link DashTest} and {@link DashWidevineOfflineTest). + */ +public final class DashTestData { + + // Clear content manifests. + public static final String H264_MANIFEST = "manifest-h264.mpd"; + public static final String H265_MANIFEST = "manifest-h265.mpd"; + public static final String VP9_MANIFEST = "manifest-vp9.mpd"; + public static final String H264_23_MANIFEST = "manifest-h264-23.mpd"; + public static final String H264_24_MANIFEST = "manifest-h264-24.mpd"; + public static final String H264_29_MANIFEST = "manifest-h264-29.mpd"; + // Widevine encrypted content manifests. + public static final String WIDEVINE_H264_MANIFEST_PREFIX = "manifest-h264-enc"; + public static final String WIDEVINE_H265_MANIFEST_PREFIX = "manifest-h265-enc"; + public static final String WIDEVINE_VP9_MANIFEST_PREFIX = "manifest-vp9-enc"; + public static final String WIDEVINE_H264_23_MANIFEST_PREFIX = "manifest-h264-23-enc"; + public static final String WIDEVINE_H264_24_MANIFEST_PREFIX = "manifest-h264-24-enc"; + public static final String WIDEVINE_H264_29_MANIFEST_PREFIX = "manifest-h264-29-enc"; + + public static final String AAC_AUDIO_REPRESENTATION_ID = "141"; + public static final String H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "avc-baseline-240"; + public static final String H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "avc-baseline-480"; + public static final String H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "avc-main-240"; + public static final String H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "avc-main-480"; + // The highest quality H264 format mandated by the Android CDD. + public static final String H264_CDD_FIXED = Util.SDK_INT < 23 + ? H264_BASELINE_480P_VIDEO_REPRESENTATION_ID : H264_MAIN_480P_VIDEO_REPRESENTATION_ID; + // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile + // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 + // when switching between baseline and main profiles on certain devices. + public static final String[] H264_CDD_ADAPTIVE = Util.SDK_INT < 24 + ? new String[] { + H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} + : new String[] { + H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, + H264_MAIN_240P_VIDEO_REPRESENTATION_ID, + H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; + + public static final String H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = + "avc-baseline-480-23"; + public static final String H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = + "avc-baseline-480-24"; + public static final String H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = + "avc-baseline-480-29"; + + public static final String H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "hevc-main-288"; + public static final String H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "hevc-main-360"; + // The highest quality H265 format mandated by the Android CDD. + public static final String H265_CDD_FIXED = H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; + // Multiple H265 formats mandated by the Android CDD. + public static final String[] H265_CDD_ADAPTIVE = + new String[] { + H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, + H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; + + public static final String VORBIS_AUDIO_REPRESENTATION_ID = "4"; + public static final String VP9_180P_VIDEO_REPRESENTATION_ID = "0"; + public static final String VP9_360P_VIDEO_REPRESENTATION_ID = "1"; + // The highest quality VP9 format mandated by the Android CDD. + public static final String VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; + // Multiple VP9 formats mandated by the Android CDD. + public static final String[] VP9_CDD_ADAPTIVE = + new String[] { + VP9_180P_VIDEO_REPRESENTATION_ID, + VP9_360P_VIDEO_REPRESENTATION_ID}; + + // Widevine encrypted content representation ids. + public static final String WIDEVINE_AAC_AUDIO_REPRESENTATION_ID = "0"; + public static final String WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "1"; + public static final String WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "3"; + public static final String WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "4"; + // The highest quality H264 format mandated by the Android CDD. + public static final String WIDEVINE_H264_CDD_FIXED = Util.SDK_INT < 23 + ? WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID + : WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID; + // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile + // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 + // when switching between baseline and main profiles on certain devices. + public static final String[] WIDEVINE_H264_CDD_ADAPTIVE = Util.SDK_INT < 24 + ? new String[] { + WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} + : new String[] { + WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; + + public static final String WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = "2"; + + public static final String WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "1"; + public static final String WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "2"; + // The highest quality H265 format mandated by the Android CDD. + public static final String WIDEVINE_H265_CDD_FIXED = + WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; + // Multiple H265 formats mandated by the Android CDD. + public static final String[] WIDEVINE_H265_CDD_ADAPTIVE = + new String[] { + WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; + + public static final String WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID = "0"; + public static final String WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID = "1"; + public static final String WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID = "2"; + // The highest quality VP9 format mandated by the Android CDD. + public static final String WIDEVINE_VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; + // Multiple VP9 formats mandated by the Android CDD. + public static final String[] WIDEVINE_VP9_CDD_ADAPTIVE = + new String[] { + WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID, + WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID}; + + private DashTestData() { + } + +} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java new file mode 100644 index 0000000000..3bf9508128 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2017 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.exoplayer2.playbacktests.gts; + +import android.media.MediaDrm.MediaDrmStateException; +import android.test.ActivityInstrumentationTestCase2; +import android.util.Pair; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.OfflineLicenseHelper; +import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; +import com.google.android.exoplayer2.playbacktests.util.HostActivity; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import junit.framework.Assert; + +/** + * Tests Widevine encrypted DASH playbacks using offline keys. + */ +public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCase2 { + + private static final String TAG = "DashWidevineOfflineTest"; + private static final String USER_AGENT = "ExoPlayerPlaybackTests"; + + private DashHostedTest.Builder builder; + private String widevineManifestUrl; + private DefaultHttpDataSourceFactory httpDataSourceFactory; + private OfflineLicenseHelper offlineLicenseHelper; + private byte[] offlineLicenseKeySetId; + + public DashWidevineOfflineTest() { + super(HostActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + builder = new DashHostedTest.Builder(TAG) + .setStreamName("test_widevine_h264_fixed_offline") + .setManifestUrlForWidevine(DashTestData.WIDEVINE_H264_MANIFEST_PREFIX, MimeTypes.VIDEO_H264) + .setFullPlaybackNoSeeking(true) + .setCanIncludeAdditionalVideoFormats(false) + .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, + DashTestData.WIDEVINE_H264_CDD_FIXED); + + boolean useL1Widevine = DashHostedTest.isL1WidevineAvailable(MimeTypes.VIDEO_H264); + widevineManifestUrl = DashHostedTest + .getWidevineManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST_PREFIX, useL1Widevine); + String widevineLicenseUrl = DashHostedTest.getWidevineLicenseUrl(useL1Widevine); + httpDataSourceFactory = new DefaultHttpDataSourceFactory(USER_AGENT); + offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(widevineLicenseUrl, + httpDataSourceFactory); + } + + @Override + protected void tearDown() throws Exception { + if (offlineLicenseKeySetId != null) { + releaseLicense(); + } + if (offlineLicenseHelper != null) { + offlineLicenseHelper.releaseResources(); + } + super.tearDown(); + } + + // Offline license tests + + public void testWidevineOfflineLicense() throws Exception { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + builder.runTest(getActivity(), getInstrumentation()); + + // Renew license after playback should still work + offlineLicenseKeySetId = offlineLicenseHelper.renew(offlineLicenseKeySetId); + Assert.assertNotNull(offlineLicenseKeySetId); + } + + public void testWidevineOfflineReleasedLicense() throws Throwable { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + releaseLicense(); // keySetId no longer valid. + + try { + builder.runTest(getActivity(), getInstrumentation()); + fail("Playback should fail because the license has been released."); + } catch (Throwable e) { + // Get the root cause + while (true) { + Throwable cause = e.getCause(); + if (cause == null || cause == e) { + break; + } + e = cause; + } + // It should be a MediaDrmStateException instance + if (!(e instanceof MediaDrmStateException)) { + throw e; + } + } + } + + public void testWidevineOfflineExpiredLicense() throws Exception { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + + // Wait until the license expires + long licenseDuration = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId).first; + assertTrue("License duration should be less than 30 sec. " + + "Server settings might have changed.", licenseDuration < 30); + while (licenseDuration > 0) { + synchronized (this) { + wait(licenseDuration * 1000 + 2000); + } + long previousDuration = licenseDuration; + licenseDuration = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId).first; + assertTrue("License duration should be decreasing.", previousDuration > licenseDuration); + } + + // DefaultDrmSessionManager should renew the license and stream play fine + builder.runTest(getActivity(), getInstrumentation()); + } + + public void testWidevineOfflineLicenseExpiresOnPause() throws Exception { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + + // During playback pause until the license expires then continue playback + Pair licenseDurationRemainingSec = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId); + long licenseDuration = licenseDurationRemainingSec.first; + assertTrue("License duration should be less than 30 sec. " + + "Server settings might have changed.", licenseDuration < 30); + ActionSchedule schedule = new ActionSchedule.Builder(TAG) + .delay(3000).pause().delay(licenseDuration * 1000 + 2000).play().build(); + + // DefaultDrmSessionManager should renew the license and stream play fine + builder + .setActionSchedule(schedule) + .runTest(getActivity(), getInstrumentation()); + } + + private void downloadLicense() throws InterruptedException, DrmSessionException, IOException { + offlineLicenseKeySetId = offlineLicenseHelper.download( + httpDataSourceFactory.createDataSource(), widevineManifestUrl); + Assert.assertNotNull(offlineLicenseKeySetId); + Assert.assertTrue(offlineLicenseKeySetId.length > 0); + builder.setOfflineLicenseKeySetId(offlineLicenseKeySetId); + } + + private void releaseLicense() throws DrmSessionException { + offlineLicenseHelper.release(offlineLicenseKeySetId); + offlineLicenseKeySetId = null; + } + +} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java index 7bf8985b64..74262f4422 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java @@ -63,7 +63,8 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen public static final long EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS = -2; public static final long EXPECTED_PLAYING_TIME_UNSET = -1; - private final String tag; + protected final String tag; + private final boolean failOnPlayerError; private final long expectedPlayingTimeMs; private final DecoderCounters videoDecoderCounters; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java index 3716c6d37f..ee8927ea21 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java @@ -47,13 +47,13 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab } @Override - public FakeTrackOutput track(int trackId) { - FakeTrackOutput output = trackOutputs.get(trackId); + public FakeTrackOutput track(int id, int type) { + FakeTrackOutput output = trackOutputs.get(id); if (output == null) { Assert.assertFalse(tracksEnded); numberOfTracks++; output = new FakeTrackOutput(); - trackOutputs.put(trackId, output); + trackOutputs.put(id, output); } return output; }