From db4b478cc0a6ff34d25e5c60882000babf0a4b0a Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Wed, 30 Nov 2016 18:47:36 -0500 Subject: [PATCH 001/142] fix 6.1 channel passthrough failing --- .../com/google/android/exoplayer2/audio/AudioTrack.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 8e6cf68dc8..487ce28382 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 @@ -443,6 +443,14 @@ public final class AudioTrack { } boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); + if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) { + if (channelConfig == (AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER) ) + channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; + else if (channelConfig == (AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER) + || channelConfig == (AudioFormat.CHANNEL_OUT_QUAD) + || channelConfig == (AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER)) + channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; + } @C.Encoding int sourceEncoding; if (passthrough) { sourceEncoding = getEncodingForMimeType(mimeType); From e22c42c7c22bcc4fa10d8282163ce1f6f5100c60 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Thu, 1 Dec 2016 19:30:27 -0500 Subject: [PATCH 002/142] requested changes for simplicity --- .../google/android/exoplayer2/audio/AudioTrack.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 487ce28382..da89821cb8 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 @@ -441,16 +441,14 @@ public final class AudioTrack { default: throw new IllegalArgumentException("Unsupported channel count: " + channelCount); } - - boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) { - if (channelConfig == (AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER) ) + if (channelCount == 7) channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; - else if (channelConfig == (AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER) - || channelConfig == (AudioFormat.CHANNEL_OUT_QUAD) - || channelConfig == (AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER)) + else if (channelCount >=3 && channelCount <= 5) channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; } + + boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); @C.Encoding int sourceEncoding; if (passthrough) { sourceEncoding = getEncodingForMimeType(mimeType); From cc24c4e38bd222f8b43efaba513c76d0a9effe4b Mon Sep 17 00:00:00 2001 From: drhill Date: Tue, 6 Dec 2016 08:08:58 -0500 Subject: [PATCH 003/142] change to switch to avoid changing 4.0 tracks --- .../android/exoplayer2/audio/AudioTrack.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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 da89821cb8..a079c0df60 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 @@ -442,10 +442,17 @@ public final class AudioTrack { throw new IllegalArgumentException("Unsupported channel count: " + channelCount); } if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) { - if (channelCount == 7) - channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; - else if (channelCount >=3 && channelCount <= 5) - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; + switch(channelCount) { + case 7: + channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; + break; + case 3: + case 5: + channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; + break; + default: + break; + } } boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); From 63966a373696adc13ff7fbd420911db05fc2cb77 Mon Sep 17 00:00:00 2001 From: WeiChungChang Date: Mon, 12 Dec 2016 20:34:09 +0800 Subject: [PATCH 004/142] Fix the issue when the sequence of PTS is out of order by bidirectional prediction for skipToKeyframeBefore() --- .../android/exoplayer2/extractor/DefaultTrackOutput.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 cb9e41aa62..44756a507e 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 @@ -786,9 +786,7 @@ public final class DefaultTrackOutput implements TrackOutput { return C.POSITION_UNSET; } - int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1; - long lastTimeUs = timesUs[lastWriteIndex]; - if (timeUs > lastTimeUs) { + if (timeUs > largestQueuedTimestampUs) { return C.POSITION_UNSET; } From d422d2e6e606a213dd37aadb00bb71e4bf8e5a9d Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 14 Dec 2016 09:47:02 -0800 Subject: [PATCH 005/142] Add missing # chars to release notes! ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142028608 --- RELEASENOTES.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 23334c99f6..feaa240220 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,19 +6,19 @@ This release contains important bug fixes. Users of r2.0.x should proactively update to this version. * HLS: Support for seeking in live streams - ([87](https://github.com/google/ExoPlayer/issues/87)). + ([#87](https://github.com/google/ExoPlayer/issues/87)). * HLS: Improved support: * Support for EXT-X-PROGRAM-DATE-TIME - ([747](https://github.com/google/ExoPlayer/issues/747)). + ([#747](https://github.com/google/ExoPlayer/issues/747)). * Improved handling of sample timestamps and their alignment across variants and renditions. * Fix issue that could cause playbacks to get stuck in an endless initial buffering state. * Correctly propagate BehindLiveWindowException instead of IndexOutOfBoundsException exception - ([1695](https://github.com/google/ExoPlayer/issues/1695)). + ([#1695](https://github.com/google/ExoPlayer/issues/1695)). * MP3/MP4: Support for ID3 metadata, including embedded album art - ([979](https://github.com/google/ExoPlayer/issues/979)). + ([#979](https://github.com/google/ExoPlayer/issues/979)). * Improved customization of UI components. You can read about customization of ExoPlayer's UI components [here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi). @@ -31,30 +31,30 @@ update to this version. * Support SCTE-35 splice information messages. * Support multiple table sections in a single PSI section. * Fix NullPointerException when an unsupported stream type is encountered - ([2149](https://github.com/google/ExoPlayer/issues/2149)). + ([#2149](https://github.com/google/ExoPlayer/issues/2149)). * Avoid failure when expected ID3 header not found - ([1966](https://github.com/google/ExoPlayer/issues/1966)). + ([#1966](https://github.com/google/ExoPlayer/issues/1966)). * Improvements to the upstream cache package. * Support caching of media segments for DASH, HLS and SmoothStreaming. Note that caching of manifest and playlist files is still not supported in the (normal) case where the corresponding responses are compressed. * Support caching for ExtractorMediaSource based playbacks. * Improved flexibility of SimpleExoPlayer - ([2102](https://github.com/google/ExoPlayer/issues/2102)). + ([#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)) + ([#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)). + buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)). * Correctly set SimpleExoPlayerView surface aspect ratio when an active player - is attached ([2077](https://github.com/google/ExoPlayer/issues/1976)). + is attached ([#2077](https://github.com/google/ExoPlayer/issues/1976)). * OGG: Fix playback of short OGG files - ([1976](https://github.com/google/ExoPlayer/issues/1976)). + ([#1976](https://github.com/google/ExoPlayer/issues/1976)). * MP4: Support `.mp3` tracks - ([2066](https://github.com/google/ExoPlayer/issues/2066)). + ([#2066](https://github.com/google/ExoPlayer/issues/2066)). * SubRip: Don't fail playbacks if SubRip file contains negative timestamps - ([2145](https://github.com/google/ExoPlayer/issues/2145)). + ([#2145](https://github.com/google/ExoPlayer/issues/2145)). * Misc bugfixes. ### r2.0.4 ### @@ -180,11 +180,11 @@ V2 release. * Improvements to the upstream cache package. * MP4: Support `.mp3` tracks - ([2066](https://github.com/google/ExoPlayer/issues/2066)). + ([#2066](https://github.com/google/ExoPlayer/issues/2066)). * SubRip: Don't fail playbacks if SubRip file contains negative timestamps - ([2145](https://github.com/google/ExoPlayer/issues/2145)). + ([#2145](https://github.com/google/ExoPlayer/issues/2145)). * MPEG-TS: Avoid failure when expected ID3 header not found - ([1966](https://github.com/google/ExoPlayer/issues/1966)). + ([#1966](https://github.com/google/ExoPlayer/issues/1966)). * Misc bugfixes. ### r1.5.12 ### From 86adc6440330d8e05ac4e132b35c53a0bbd4e70a Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 14 Dec 2016 23:00:10 +0000 Subject: [PATCH 006/142] Delete CS classes --- .../playbacktests/Mp3PlaybackTest.java | 94 ---------------- .../playbacktests/Mp4PlaybackTest.java | 100 ------------------ 2 files changed, 194 deletions(-) delete mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/Mp3PlaybackTest.java delete mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/Mp4PlaybackTest.java diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/Mp3PlaybackTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/Mp3PlaybackTest.java deleted file mode 100644 index b640a058ee..0000000000 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/Mp3PlaybackTest.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.playbacktests; - -import android.annotation.TargetApi; -import android.net.Uri; -import android.test.ActivityInstrumentationTestCase2; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; -import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; -import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; -import com.google.android.exoplayer2.playbacktests.util.HostActivity; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.ClosedSource; -import com.google.android.exoplayer2.util.Util; - -/** - * Tests MP3 playback using {@link ExoPlayer}. - */ -@ClosedSource(reason = "Not yet ready") -public final class Mp3PlaybackTest extends ActivityInstrumentationTestCase2 { - - private static final String TAG = "Mp3PlaybackTest"; - private static final String URL = "http://storage.googleapis.com/exoplayer-test-media-0/play.mp3"; - - private static final long TEST_TIMEOUT_MS = 2 * 60 * 1000; - - public Mp3PlaybackTest() { - super(HostActivity.class); - } - - public void testPlayback() { - if (Util.SDK_INT < 16) { - // Pass. - return; - } - Mp3HostedTest test = new Mp3HostedTest(URL, true); - getActivity().runTest(test, TEST_TIMEOUT_MS); - } - - public void testPlaybackWithSeeking() { - if (Util.SDK_INT < 16) { - // Pass. - return; - } - Mp3HostedTest test = new Mp3HostedTest(URL, false); - ActionSchedule schedule = new ActionSchedule.Builder(TAG) - .delay(5000).seek(30000) - .delay(5000).seek(0) - .delay(5000).seek(30000) - .delay(5000).stop() - .build(); - test.setSchedule(schedule); - getActivity().runTest(test, TEST_TIMEOUT_MS); - } - - @TargetApi(16) - private static class Mp3HostedTest extends ExoHostedTest { - - private final Uri uri; - - public Mp3HostedTest(String uriString, boolean fullPlaybackNoSeeking) { - super("Mp3PlaybackTest", fullPlaybackNoSeeking); - uri = Uri.parse(uriString); - } - - @Override - public MediaSource buildSource(HostActivity host, String userAgent, - TransferListener mediaTransferListener) { - DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(host, userAgent, - mediaTransferListener); - return new ExtractorMediaSource(uri, dataSourceFactory, Mp3Extractor.FACTORY, null, null); - } - - } - -} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/Mp4PlaybackTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/Mp4PlaybackTest.java deleted file mode 100644 index 3069063b65..0000000000 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/Mp4PlaybackTest.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.playbacktests; - -import android.annotation.TargetApi; -import android.net.Uri; -import android.test.ActivityInstrumentationTestCase2; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; -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.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.ClosedSource; -import com.google.android.exoplayer2.util.Util; - -/** - * Tests MP4 playback using {@link ExoPlayer}. - */ -@ClosedSource(reason = "Not yet ready") -public final class Mp4PlaybackTest extends ActivityInstrumentationTestCase2 { - - private static final String SOURCE_URL = "http://redirector.c.youtube.com/videoplayback?id=604ed5" - + "ce52eda7ee&itag=22&source=youtube&sparams=ip,ipbits,expire,source,id&ip=0.0.0.0&ipbits=0&" - + "expire=19000000000&signature=513F28C7FDCBEC60A66C86C9A393556C99DC47FB.04C88036EEE12565A1ED" - + "864A875A58F15D8B5300&key=ik0"; - private static final String VIDEO_TAG = "Video"; - - private static final long TEST_TIMEOUT_MS = 15 * 60 * 1000; - private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; - private static final int EXPECTED_VIDEO_FRAME_COUNT = 14316; - - public Mp4PlaybackTest() { - super(HostActivity.class); - } - - public void testPlayback() { - if (Util.SDK_INT < 16) { - // Pass. - return; - } - Mp4HostedTest test = new Mp4HostedTest(SOURCE_URL, true); - getActivity().runTest(test, TEST_TIMEOUT_MS); - } - - @TargetApi(16) - private static class Mp4HostedTest extends ExoHostedTest { - - private final Uri uri; - - public Mp4HostedTest(String uriString, boolean fullPlaybackNoSeeking) { - super("Mp4PlaybackTest", fullPlaybackNoSeeking); - uri = Uri.parse(uriString); - } - - @Override - public MediaSource buildSource(HostActivity host, String userAgent, - TransferListener mediaTransferListener) { - DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(host, userAgent); - return new ExtractorMediaSource(uri, dataSourceFactory, Mp4Extractor.FACTORY, null, null); - } - - @Override - public void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { - assertEquals(1, videoCounters.decoderInitCount); - assertEquals(1, videoCounters.decoderReleaseCount); - 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(VIDEO_TAG, videoCounters, - EXPECTED_VIDEO_FRAME_COUNT - 1, EXPECTED_VIDEO_FRAME_COUNT); - - int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION - * DecoderCountersUtil.getTotalOutputBuffers(videoCounters)); - DecoderCountersUtil.assertDroppedOutputBufferLimit(VIDEO_TAG, videoCounters, - droppedFrameLimit); - } - - } - -} From 37317520f48d8629eaf0012b61e42ec5cf011e00 Mon Sep 17 00:00:00 2001 From: cblay Date: Wed, 14 Dec 2016 17:37:27 -0800 Subject: [PATCH 007/142] Improving handling of atoms with size less than header in FragmentedMp4Extractor. These currently lead to cryptic ArrayIndexOutOfBoundsExceptions being thrown from System.arraycopy() so my proposal is to throw a more useful ParserException instead. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142087132 --- .../mp4/sample_fragmented_zero_size_atom.mp4 | Bin 0 -> 1903 bytes .../mp4/FragmentedMp4ExtractorTest.java | 21 +++-- .../extractor/mp4/FragmentedMp4Extractor.java | 4 + .../android/exoplayer2/testutil/TestUtil.java | 75 ++++++++++++++++++ 4 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 library/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 diff --git a/library/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 b/library/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..3d3c63786ef7f40a9b4307fd17fcdc47f006f350 GIT binary patch literal 1903 zcmb_cO-NKx6h3!;RP4t|4V9wCw3TEk7PhDcl$s#0h$JG&mv1H~=lSHlH$z4XsZ9h$ zl#3QcK~O}tX<>`XBKmKUB`u0t^dl4!Ve7)abMLizjTtSv%=gaEcka3Oo^$UIQ8elG z_oZChA_@>opvlN~HClbmjYOfFmThN=C~alCO-LGZ-LDLy;3u|8$e&cF?VO=_za8@% zGxY#ml_}eFnTiYy3=~rP6c4QO)^m&=xOaIyaxW#hz3?uGR2*x*A(`3j7^qMSPCv>q zqTe!829&5}=ASpy!1=e|<2YK;ZKfTm;ge07iD{i>2W&fT^qT1e$B0@h)tiJ;p0#9B z|CVY^#Vt0S1jq1Tes0D|N45UZ_4cHppLRW0HbMK3aHF8}@aL3{Pz#O}hsxkFBSN`- z-%2hsar;|^NlT~RQPp0^p;xiC@W^fvAzF8W=3k`A!&RMv3c4pgJY9ANGa|0%$%g4% zu-z`LvnYpsz-P0Hm@Yf#6IcGyT)q_f%@y)E`Dei0iL#rrmWfS|6E9yB?@>0CZ+)ml^JI`z9^PL&fGvME0 zC8Xpz)&6&db~#30A7A}nqb`+pJyIx*GtuC%zF$S2!|g8)gIt1~TjX5?PgM81Oi^!n#C56SBX@PEtm==&dPrb;1e7UF58 z+A*qX7T}uIli2!0`%7VSAmc2~fWHip0Zsxqueb)>1+Zr+Z$mx?SUv)d1FwPezykn1 z5U8&>1H1 expectedThrowable) + throws IOException, InterruptedException { + byte[] fileData = getByteArray(instrumentation, sampleFile); + assertThrows(factory, fileData, expectedThrowable); + } + + /** + * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all + * possible combinations of "simulate" parameters. + * + * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} + * class which is to be tested. + * @param fileData Content of the input file. + * @param expectedThrowable Expected {@link Throwable} class. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean) + */ + public static void assertThrows(ExtractorFactory factory, byte[] fileData, + Class expectedThrowable) throws IOException, InterruptedException { + assertThrows(factory.create(), fileData, expectedThrowable, false, false, false); + assertThrows(factory.create(), fileData, expectedThrowable, true, false, false); + assertThrows(factory.create(), fileData, expectedThrowable, false, true, false); + assertThrows(factory.create(), fileData, expectedThrowable, true, true, false); + assertThrows(factory.create(), fileData, expectedThrowable, false, false, true); + assertThrows(factory.create(), fileData, expectedThrowable, true, false, true); + assertThrows(factory.create(), fileData, expectedThrowable, false, true, true); + assertThrows(factory.create(), fileData, expectedThrowable, true, true, true); + } + + /** + * Asserts {@code extractor} throws {@code expectedThrowable} while consuming {@code sampleFile}. + * + * @param extractor The {@link Extractor} to be tested. + * @param fileData Content of the input file. + * @param expectedThrowable Expected {@link Throwable} class. + * @param simulateIOErrors If true simulates IOErrors. + * @param simulateUnknownLength If true simulates unknown input length. + * @param simulatePartialReads If true simulates partial reads. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + public static void assertThrows(Extractor extractor, byte[] fileData, + Class expectedThrowable, boolean simulateIOErrors, + boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, + InterruptedException { + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) + .setSimulateIOErrors(simulateIOErrors) + .setSimulateUnknownLength(simulateUnknownLength) + .setSimulatePartialReads(simulatePartialReads).build(); + try { + consumeTestData(extractor, input, 0, true); + throw new AssertionError(expectedThrowable.getSimpleName() + " expected but not thrown"); + } catch (Throwable throwable) { + if (expectedThrowable.equals(throwable.getClass())) { + return; // Pass! + } + throw throwable; + } + } + public static void recursiveDelete(File fileOrDirectory) { if (fileOrDirectory.isDirectory()) { for (File child : fileOrDirectory.listFiles()) { From 2c3ce7fee373e27bc11fa3f9f25bd1e028727a3a Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 15 Dec 2016 03:31:18 -0800 Subject: [PATCH 008/142] Fix playback of media with >1MB preparation data Also clarify when getNextLoadPositionUs and continueLoading can be called. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142124497 --- .../exoplayer2/ExoPlayerImplInternal.java | 7 ++--- .../source/ExtractorMediaPeriod.java | 2 +- .../exoplayer2/source/MediaPeriod.java | 28 +++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) 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 66be6b7478..8866bb7c48 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -676,6 +676,7 @@ import java.io.IOException; standaloneMediaClock.stop(); rendererMediaClock = null; rendererMediaClockSource = null; + rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US; for (Renderer renderer : enabledRenderers) { try { ensureStopped(renderer); @@ -823,9 +824,6 @@ import java.io.IOException; } private boolean haveSufficientBuffer(boolean rebuffering) { - if (loadingPeriodHolder == null) { - return false; - } long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared ? loadingPeriodHolder.startPositionUs : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs(); @@ -1287,7 +1285,8 @@ import java.io.IOException; } private void maybeContinueLoading() { - long nextLoadPositionUs = loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs(); + long nextLoadPositionUs = !loadingPeriodHolder.prepared ? 0 + : loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs(); if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { setIsLoading(false); } else { 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 0b7190d382..8ab4d45c47 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 @@ -244,7 +244,7 @@ import java.io.IOException; @Override public long getNextLoadPositionUs() { - return getBufferedPositionUs(); + return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs(); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index a3c1c88df4..f4a9665b10 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -133,4 +133,32 @@ public interface MediaPeriod extends SequenceableLoader { */ long seekToUs(long positionUs); + // SequenceableLoader interface. Overridden to provide more specific documentation. + + /** + * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished. + *

+ * This method should only be called after the period has been prepared. It may be called when no + * tracks are selected. + */ + @Override + long getNextLoadPositionUs(); + + /** + * Attempts to continue loading. + *

+ * This method may be called both during and after the period has been prepared. + *

+ * A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the + * {@link Callback} passed to {@link #prepare(Callback)} to request that this method be called + * when the period is permitted to continue loading data. A period may do this both during and + * after preparation. + * + * @param positionUs The current playback position. + * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return + * a different value than prior to the call. False otherwise. + */ + @Override + boolean continueLoading(long positionUs); + } From 5bb1d5dc99c8afc72b653cb19859470f4405a8e9 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 15 Dec 2016 07:09:45 -0800 Subject: [PATCH 009/142] Add tunneling functionality to AudioTrack Although the underlying platform AudioTrack is capable of writing the AV sync header from M onward, I've opted not to use the functionality since it appears to allocate an unnecessary (and large) number of ByteBuffers. We can swap over from O if this is addressed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142138988 --- .../android/exoplayer2/audio/AudioTrack.java | 148 +++++++++++++++--- 1 file changed, 124 insertions(+), 24 deletions(-) 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 8e6cf68dc8..57884ce8a9 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 @@ -17,7 +17,9 @@ package com.google.android.exoplayer2.audio; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.media.AudioAttributes; import android.media.AudioFormat; +import android.media.AudioManager; import android.media.AudioTimestamp; import android.media.PlaybackParams; import android.os.ConditionVariable; @@ -30,26 +32,28 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.reflect.Method; import java.nio.ByteBuffer; +import java.nio.ByteOrder; /** * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles * playback position smoothing, non-blocking writes and reconfiguration. *

* Before starting playback, specify the input format by calling - * {@link #configure(String, int, int, int, int)}. Next call {@link #initialize(int)}, optionally - * specifying an audio session. + * {@link #configure(String, int, int, int, int)}. Next call {@link #initialize(int)} or + * {@link #initializeV21(int, boolean)}, optionally specifying an audio session and whether the + * track is to be used with tunneling video playback. *

* Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. *

* Call {@link #configure(String, int, int, int, int)} whenever the input format changes. If * {@link #isInitialized()} returns {@code false} after the call, it is necessary to call - * {@link #initialize(int)} before writing more data. + * {@link #initialize(int)} or {@link #initializeV21(int, boolean)} before writing more data. *

* The underlying {@link android.media.AudioTrack} is created by {@link #initialize(int)} and * released by {@link #reset()} (and {@link #configure(String, int, int, int, int)} unless the input - * format is unchanged). It is safe to call {@link #initialize(int)} after calling {@link #reset()} - * without reconfiguration. + * format is unchanged). It is safe to call {@link #initialize(int)} or + * {@link #initializeV21(int, boolean)} after calling {@link #reset()} without reconfiguration. *

* Call {@link #release()} when the instance is no longer required. */ @@ -144,9 +148,11 @@ public final class AudioTrack { public static final int RESULT_BUFFER_CONSUMED = 2; /** - * Represents an unset {@link android.media.AudioTrack} session identifier. + * Represents an unset {@link android.media.AudioTrack} session identifier. Equal to + * {@link AudioManager#AUDIO_SESSION_ID_GENERATE}. */ - public static final int SESSION_ID_NOT_SET = 0; + @SuppressWarnings("InlinedApi") + public static final int SESSION_ID_NOT_SET = AudioManager.AUDIO_SESSION_ID_GENERATE; /** * Returned by {@link #getCurrentPositionUs} when the position is not set. @@ -273,6 +279,10 @@ public final class AudioTrack { private int bufferSize; private long bufferSizeUs; + private boolean useHwAvSync; + private ByteBuffer avSyncHeader; + private int bytesUntilNextAvSync; + private int nextPlayheadOffsetIndex; private int playheadOffsetCount; private long smoothedPlayheadOffsetUs; @@ -341,8 +351,8 @@ public final class AudioTrack { } /** - * Returns whether the audio track has been successfully initialized via {@link #initialize} and - * not yet {@link #reset}. + * Returns whether the audio track has been successfully initialized via {@link #initialize} or + * {@link #initializeV21(int, boolean)}, and has not yet been {@link #reset}. */ public boolean isInitialized() { return audioTrack != null; @@ -498,11 +508,26 @@ public final class AudioTrack { /** * Initializes the audio track for writing new buffers using {@link #handleBuffer}. * - * @param sessionId Audio track session identifier to re-use, or {@link #SESSION_ID_NOT_SET} to - * create a new one. - * @return The new (or re-used) session identifier. + * @param sessionId Audio track session identifier, or {@link #SESSION_ID_NOT_SET} to create one. + * @return The audio track session identifier. */ public int initialize(int sessionId) throws InitializationException { + return initializeInternal(sessionId, false); + } + + /** + * Initializes the audio track for writing new buffers using {@link #handleBuffer}. + * + * @param sessionId Audio track session identifier, or {@link #SESSION_ID_NOT_SET} to create one. + * @param tunneling Whether the audio track is to be used with tunneling video playback. + * @return The audio track session identifier. + */ + public int initializeV21(int sessionId, boolean tunneling) throws InitializationException { + Assertions.checkState(Util.SDK_INT >= 21); + return initializeInternal(sessionId, tunneling); + } + + private int initializeInternal(int sessionId, boolean tunneling) throws InitializationException { // If we're asynchronously releasing a previous audio track then we block until it has been // released. This guarantees that we cannot end up in a state where we have multiple audio // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust @@ -510,7 +535,11 @@ public final class AudioTrack { // initialization of the audio track to fail. releasingConditionVariable.block(); - if (sessionId == SESSION_ID_NOT_SET) { + useHwAvSync = tunneling; + if (useHwAvSync) { + audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding, + bufferSize, sessionId); + } else if (sessionId == SESSION_ID_NOT_SET) { audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, targetEncoding, bufferSize, MODE_STREAM); } else { @@ -692,7 +721,9 @@ public final class AudioTrack { buffer.position(buffer.position() + bytesWritten); } } else { - bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); + bytesWritten = useHwAvSync + ? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs) + : writeNonBlockingV21(audioTrack, buffer, bytesRemaining); } if (bytesWritten < 0) { @@ -718,6 +749,7 @@ public final class AudioTrack { public void handleEndOfStream() { if (isInitialized()) { audioTrackUtil.handleEndOfStream(getSubmittedFrames()); + bytesUntilNextAvSync = 0; } } @@ -743,19 +775,27 @@ public final class AudioTrack { } /** - * Sets the stream type for audio track. If the stream type has changed, {@link #isInitialized()} - * will return {@code false} and the caller must re-{@link #initialize(int)} the audio track - * before writing more data. The caller must not reuse the audio session identifier when - * re-initializing with a new stream type. + * Sets the stream type for audio track. If the stream type has changed and if the audio track + * is not configured for use with video tunneling, then the audio track is reset and the caller + * must re-initialize the audio track before writing more data. The caller must not reuse the + * audio session identifier when re-initializing with a new stream type. + *

+ * If the audio track is configured for use with video tunneling then the stream type is ignored + * and the audio track is not reset. The passed stream type will be used if the audio track is + * later re-configured into non-tunneled mode. * * @param streamType The {@link C.StreamType} to use for audio output. - * @return Whether the stream type changed. + * @return Whether the audio track was reset as a result of this call. */ public boolean setStreamType(@C.StreamType int streamType) { if (this.streamType == streamType) { return false; } this.streamType = streamType; + if (useHwAvSync) { + // The stream type is ignored in tunneling mode, so no need to reset. + return false; + } reset(); return true; } @@ -795,9 +835,9 @@ public final class AudioTrack { /** * Releases the underlying audio track asynchronously. *

- * Calling {@link #initialize(int)} will block until the audio track has been released, so it is - * safe to initialize immediately after a reset. The audio session may remain active until - * {@link #release()} is called. + * Calling {@link #initialize(int)} or {@link #initializeV21(int, boolean)} will block until the + * audio track has been released, so it is safe to initialize immediately after a reset. The audio + * session may remain active until {@link #release()} is called. */ public void reset() { if (isInitialized()) { @@ -805,6 +845,7 @@ public final class AudioTrack { submittedEncodedFrames = 0; framesPerEncodedSample = 0; currentSourceBuffer = null; + avSyncHeader = null; startMediaTimeState = START_NOT_SET; latencyUs = 0; resetSyncParams(); @@ -1020,6 +1061,26 @@ public final class AudioTrack { && audioTrack.getPlaybackHeadPosition() == 0; } + /** + * Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback. + */ + @TargetApi(21) + private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate, + int channelConfig, int encoding, int bufferSize, int sessionId) { + AudioAttributes attributesBuilder = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) + .setFlags(AudioAttributes.FLAG_HW_AV_SYNC) + .build(); + AudioFormat format = new AudioFormat.Builder() + .setChannelMask(channelConfig) + .setEncoding(encoding) + .setSampleRate(sampleRate) + .build(); + return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM, + sessionId); + } + /** * Converts the provided buffer into 16-bit PCM. * @@ -1125,11 +1186,50 @@ public final class AudioTrack { } @TargetApi(21) - private static int writeNonBlockingV21( - android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) { + private static int writeNonBlockingV21(android.media.AudioTrack audioTrack, ByteBuffer buffer, + int size) { return audioTrack.write(buffer, size, WRITE_NON_BLOCKING); } + @TargetApi(21) + private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack, + ByteBuffer buffer, int size, long presentationTimeUs) { + // TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed. + // if (Util.SDK_INT >= 23) { + // // The underlying platform AudioTrack writes AV sync headers directly. + // return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000); + // } + if (avSyncHeader == null) { + avSyncHeader = ByteBuffer.allocate(16); + avSyncHeader.order(ByteOrder.BIG_ENDIAN); + avSyncHeader.putInt(0x55550001); + } + if (bytesUntilNextAvSync == 0) { + avSyncHeader.putInt(4, size); + avSyncHeader.putLong(8, presentationTimeUs * 1000); + avSyncHeader.position(0); + bytesUntilNextAvSync = size; + } + int avSyncHeaderBytesRemaining = avSyncHeader.remaining(); + if (avSyncHeaderBytesRemaining > 0) { + int result = audioTrack.write(avSyncHeader, avSyncHeaderBytesRemaining, WRITE_NON_BLOCKING); + if (result < 0) { + bytesUntilNextAvSync = 0; + return result; + } + if (result < avSyncHeaderBytesRemaining) { + return 0; + } + } + int result = writeNonBlockingV21(audioTrack, buffer, size); + if (result < 0) { + bytesUntilNextAvSync = 0; + return result; + } + bytesUntilNextAvSync -= result; + return result; + } + @TargetApi(21) private static void setAudioTrackVolumeV21(android.media.AudioTrack audioTrack, float volume) { audioTrack.setVolume(volume); From 588124da766e1d9caaa1257d4e3b10721f990171 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 15 Dec 2016 10:23:06 -0800 Subject: [PATCH 010/142] Test playback of empty timeline completes successfully ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142157778 --- .../android/exoplayer2/ExoPlayerTest.java | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index be18d64195..211ae954b2 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; +import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -48,12 +49,25 @@ public final class ExoPlayerTest extends TestCase { */ private static final int TIMEOUT_MS = 10000; + /** + * Tests playback of a source that exposes a single period. + */ public void testPlayToEnd() throws Exception { PlayerWrapper playerWrapper = new PlayerWrapper(); Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, null, null); - playerWrapper.setup(new SinglePeriodTimeline(0, false), new Object(), format); - playerWrapper.blockUntilEndedOrError(TIMEOUT_MS); + playerWrapper.setup(new SinglePeriodTimeline(0, false), null, format); + playerWrapper.blockUntilEnded(TIMEOUT_MS); + } + + /** + * Tests playback of a source that exposes an empty timeline. Playback is expected to end without + * error. + */ + public void testPlayEmptyTimeline() throws Exception { + PlayerWrapper playerWrapper = new PlayerWrapper(); + playerWrapper.setup(Timeline.EMPTY, null, null); + playerWrapper.blockUntilEnded(TIMEOUT_MS); } /** @@ -81,12 +95,11 @@ public final class ExoPlayerTest extends TestCase { // Called on the test thread. - public void blockUntilEndedOrError(long timeoutMs) throws Exception { + public void blockUntilEnded(long timeoutMs) throws Exception { if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { exception = new TimeoutException("Test playback timed out."); } release(); - // Throw any pending exception (from playback, timing out or releasing). if (exception != null) { throw exception; @@ -194,17 +207,16 @@ public final class ExoPlayerTest extends TestCase { private final Timeline timeline; private final Object manifest; private final Format format; + private final ArrayList activeMediaPeriods; - private FakeMediaPeriod mediaPeriod; private boolean preparedSource; - private boolean releasedPeriod; private boolean releasedSource; public FakeMediaSource(Timeline timeline, Object manifest, Format format) { - Assertions.checkArgument(timeline.getPeriodCount() == 1); this.timeline = timeline; this.manifest = manifest; this.format = format; + activeMediaPeriods = new ArrayList<>(); } @Override @@ -221,33 +233,29 @@ public final class ExoPlayerTest extends TestCase { @Override public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { + Assertions.checkIndex(index, 0, timeline.getPeriodCount()); assertTrue(preparedSource); - assertNull(mediaPeriod); - assertFalse(releasedPeriod); assertFalse(releasedSource); assertEquals(0, index); assertEquals(0, positionUs); - mediaPeriod = new FakeMediaPeriod(format); + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(format); + activeMediaPeriods.add(mediaPeriod); return mediaPeriod; } @Override public void releasePeriod(MediaPeriod mediaPeriod) { assertTrue(preparedSource); - assertNotNull(this.mediaPeriod); - assertFalse(releasedPeriod); assertFalse(releasedSource); - assertEquals(this.mediaPeriod, mediaPeriod); - this.mediaPeriod.release(); - releasedPeriod = true; + assertTrue(activeMediaPeriods.remove(mediaPeriod)); + ((FakeMediaPeriod) mediaPeriod).release(); } @Override public void releaseSource() { assertTrue(preparedSource); - assertNotNull(this.mediaPeriod); - assertTrue(releasedPeriod); assertFalse(releasedSource); + assertTrue(activeMediaPeriods.isEmpty()); releasedSource = true; } @@ -400,7 +408,6 @@ public final class ExoPlayerTest extends TestCase { public FakeVideoRenderer(Format expectedFormat) { super(C.TRACK_TYPE_VIDEO); - Assertions.checkArgument(MimeTypes.isVideo(expectedFormat.sampleMimeType)); this.expectedFormat = expectedFormat; } From 04992fdaac6e3c320d8640e84e771c4f67715fad Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 30 Aug 2016 10:28:31 +0100 Subject: [PATCH 011/142] Move AudioTrack.SESSION_ID_NOT_SET to C It's a nicer place for it to live once it starts being passed around more widely (e.g. through the video renderer, for tunneling) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142158460 --- .../java/com/google/android/exoplayer2/C.java | 7 +++++++ .../android/exoplayer2/SimpleExoPlayer.java | 7 +++---- .../android/exoplayer2/audio/AudioTrack.java | 16 +++++----------- .../audio/MediaCodecAudioRenderer.java | 10 +++++----- .../audio/SimpleDecoderAudioRenderer.java | 10 +++++----- 5 files changed, 25 insertions(+), 25 deletions(-) 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 3e6fac4a5e..5cef177517 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -96,6 +96,13 @@ public final class C { @SuppressWarnings("InlinedApi") public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC; + /** + * Represents an unset {@link android.media.AudioTrack} session identifier. Equal to + * {@link AudioManager#AUDIO_SESSION_ID_GENERATE}. + */ + @SuppressWarnings("InlinedApi") + public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE; + /** * Represents an audio encoding, or an invalid or unset value. */ 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 36753309e2..73df6a1e7a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -29,7 +29,6 @@ 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.AudioTrack; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -178,7 +177,7 @@ public class SimpleExoPlayer implements ExoPlayer { // Set initial values. audioVolume = 1; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioStreamType = C.STREAM_TYPE_DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; @@ -393,7 +392,7 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Returns the audio session identifier, or {@code AudioTrack.SESSION_ID_NOT_SET} if not set. + * Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set. */ public int getAudioSessionId() { return audioSessionId; @@ -949,7 +948,7 @@ public class SimpleExoPlayer implements ExoPlayer { } audioFormat = null; audioDecoderCounters = null; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; } // TextRenderer.Output implementation 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 57884ce8a9..5758c481f9 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 @@ -19,7 +19,6 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.AudioAttributes; import android.media.AudioFormat; -import android.media.AudioManager; import android.media.AudioTimestamp; import android.media.PlaybackParams; import android.os.ConditionVariable; @@ -147,13 +146,6 @@ public final class AudioTrack { */ public static final int RESULT_BUFFER_CONSUMED = 2; - /** - * Represents an unset {@link android.media.AudioTrack} session identifier. Equal to - * {@link AudioManager#AUDIO_SESSION_ID_GENERATE}. - */ - @SuppressWarnings("InlinedApi") - public static final int SESSION_ID_NOT_SET = AudioManager.AUDIO_SESSION_ID_GENERATE; - /** * Returned by {@link #getCurrentPositionUs} when the position is not set. */ @@ -508,7 +500,8 @@ public final class AudioTrack { /** * Initializes the audio track for writing new buffers using {@link #handleBuffer}. * - * @param sessionId Audio track session identifier, or {@link #SESSION_ID_NOT_SET} to create one. + * @param sessionId Audio track session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} to create + * one. * @return The audio track session identifier. */ public int initialize(int sessionId) throws InitializationException { @@ -518,7 +511,8 @@ public final class AudioTrack { /** * Initializes the audio track for writing new buffers using {@link #handleBuffer}. * - * @param sessionId Audio track session identifier, or {@link #SESSION_ID_NOT_SET} to create one. + * @param sessionId Audio track session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} to create + * one. * @param tunneling Whether the audio track is to be used with tunneling video playback. * @return The audio track session identifier. */ @@ -539,7 +533,7 @@ public final class AudioTrack { if (useHwAvSync) { audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding, bufferSize, sessionId); - } else if (sessionId == SESSION_ID_NOT_SET) { + } else if (sessionId == C.AUDIO_SESSION_ID_UNSET) { audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, targetEncoding, bufferSize, MODE_STREAM); } else { 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 648bfd5762..d3cde10afb 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 @@ -129,7 +129,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media boolean playClearSamplesWithoutKeys, Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioTrack = new AudioTrack(audioCapabilities, this); eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -274,7 +274,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onDisabled() { - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; try { audioTrack.release(); } finally { @@ -328,8 +328,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media if (!audioTrack.isInitialized()) { // Initialize the AudioTrack now. try { - if (audioSessionId == AudioTrack.SESSION_ID_NOT_SET) { - audioSessionId = audioTrack.initialize(AudioTrack.SESSION_ID_NOT_SET); + if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { + audioSessionId = audioTrack.initialize(C.AUDIO_SESSION_ID_UNSET); eventDispatcher.audioSessionId(audioSessionId); onAudioSessionId(audioSessionId); } else { @@ -387,7 +387,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media case C.MSG_SET_STREAM_TYPE: @C.StreamType int streamType = (Integer) message; if (audioTrack.setStreamType(streamType)) { - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; } break; default: 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 572f7b54c1..5c9acc7739 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 @@ -145,7 +145,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements this.drmSessionManager = drmSessionManager; formatHolder = new FormatHolder(); this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; decoderReinitializationState = REINITIALIZATION_STATE_NONE; audioTrackNeedsConfigure = true; } @@ -245,8 +245,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } if (!audioTrack.isInitialized()) { - if (audioSessionId == AudioTrack.SESSION_ID_NOT_SET) { - audioSessionId = audioTrack.initialize(AudioTrack.SESSION_ID_NOT_SET); + if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { + audioSessionId = audioTrack.initialize(C.AUDIO_SESSION_ID_UNSET); eventDispatcher.audioSessionId(audioSessionId); onAudioSessionId(audioSessionId); } else { @@ -425,7 +425,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override protected void onDisabled() { inputFormat = null; - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioTrackNeedsConfigure = true; waitingForKeys = false; try { @@ -554,7 +554,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements case C.MSG_SET_STREAM_TYPE: @C.StreamType int streamType = (Integer) message; if (audioTrack.setStreamType(streamType)) { - audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; } break; default: From 72097432bdf46804f1612c1b0a8b6a0ae41d729a Mon Sep 17 00:00:00 2001 From: kylealexander Date: Thu, 15 Dec 2016 11:02:37 -0800 Subject: [PATCH 012/142] Adding Widevine subsample VP9 clips These are the new subsample clips used by the android-drm-team. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142162918 --- demo/src/main/assets/media.exolist.json | 32 +++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json index 6fa46d7451..5a3015d506 100644 --- a/demo/src/main/assets/media.exolist.json +++ b/demo/src/main/assets/media.exolist.json @@ -183,29 +183,53 @@ "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd" }, { - "name": "WV: Secure SD & HD (WebM,VP9)", + "name": "WV: Secure Fullsample SD & HD (WebM,VP9)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure SD (WebM,VP9)", + "name": "WV: Secure Fullsample SD (WebM,VP9)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure HD (WebM,VP9)", + "name": "WV: Secure Fullsample HD (WebM,VP9)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, { - "name": "WV: Secure UHD (WebM,VP9)", + "name": "WV: Secure Fullsample UHD (WebM,VP9)", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd", "drm_scheme": "widevine", "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" }, + { + "name": "WV: Secure Subsample SD & HD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Subsample SD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Subsample HD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, + { + "name": "WV: Secure Subsample UHD (WebM,VP9)", + "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd", + "drm_scheme": "widevine", + "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" + }, { "name": "WV: Secure Subsample (WebM, VP9 with altref)", "uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_altref_subsample/sintel_1080p_vp9_altref_subsample.mpd", From 65490f52f8b5c5f7925ec121801b59e3ec958d40 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Thu, 15 Dec 2016 12:23:48 -0800 Subject: [PATCH 013/142] Added support for handling window/cue priority and fill color to Cue and SubtitlePainter. Cue has been modified to optionally accept a fill color and a toggle specifying when to use the fill color. When the fill color toggle is set, then SubtitlePainter will use the fill color value instead of the color specified by the device's Accessibility settings. Cea708Decoder has also been modified to propagate that value, as well as cleaned up (in terms of documentation) to prepare it for inclusion in the open-source project. There is also a new Cea708Cue, extending Cue, which holds the Cue's priority, which is used to sort potentially overlapping cues/windows. Note that I've left the @ClosedSource annotation and logging in this CL. I intend to start testing the 608 and 708 functionality in the Fiber app to ensure that it works as expected on a wide-range of channels (as opposed to the single channel in ExoPlayer Demo) before removing these. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142173264 --- .../google/android/exoplayer2/text/Cue.java | 45 +++++++++++++++++++ .../exoplayer2/ui/SubtitlePainter.java | 8 +++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java index 1c29f10c84..c4c5a7e4ca 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import android.graphics.Color; import android.support.annotation.IntDef; import android.text.Layout.Alignment; import java.lang.annotation.Retention; @@ -36,19 +37,23 @@ public class Cue { @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_UNSET, ANCHOR_TYPE_START, ANCHOR_TYPE_MIDDLE, ANCHOR_TYPE_END}) public @interface AnchorType {} + /** * An unset anchor or line type value. */ public static final int TYPE_UNSET = Integer.MIN_VALUE; + /** * Anchors the left (for horizontal positions) or top (for vertical positions) edge of the cue * box. */ public static final int ANCHOR_TYPE_START = 0; + /** * Anchors the middle of the cue box. */ public static final int ANCHOR_TYPE_MIDDLE = 1; + /** * Anchors the right (for horizontal positions) or bottom (for vertical positions) edge of the cue * box. @@ -61,10 +66,12 @@ public class Cue { @Retention(RetentionPolicy.SOURCE) @IntDef({TYPE_UNSET, LINE_TYPE_FRACTION, LINE_TYPE_NUMBER}) public @interface LineType {} + /** * Value for {@link #lineType} when {@link #line} is a fractional position. */ public static final int LINE_TYPE_FRACTION = 0; + /** * Value for {@link #lineType} when {@link #line} is a line number. */ @@ -74,10 +81,12 @@ public class Cue { * The cue text. Note the {@link CharSequence} may be decorated with styling spans. */ public final CharSequence text; + /** * The alignment of the cue text within the cue box, or null if the alignment is undefined. */ public final Alignment textAlignment; + /** * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction * orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of @@ -86,6 +95,7 @@ public class Cue { * For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the * fractional vertical position relative to the top of the viewport. */ + public final float line; /** * The type of the {@link #line} value. @@ -112,6 +122,7 @@ 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; /** @@ -122,6 +133,7 @@ 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; /** @@ -133,6 +145,7 @@ public class Cue { * text. */ public final float position; + /** * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. @@ -143,12 +156,23 @@ public class Cue { */ @AnchorType public final int positionAnchor; + /** * The size of the cue box in the writing direction specified as a fraction of the viewport size * in that direction, or {@link #DIMEN_UNSET}. */ public final float size; + /** + * Specifies whether or not the {@link #windowColor} property is set. + */ + public final boolean windowColorSet; + + /** + * The fill color of the window. + */ + public final int windowColor; + /** * Constructs a 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}. @@ -171,6 +195,25 @@ public class Cue { */ public Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) { + this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, false, + Color.BLACK); + } + + /** + * @param text See {@link #text}. + * @param textAlignment See {@link #textAlignment}. + * @param line See {@link #line}. + * @param lineType See {@link #lineType}. + * @param lineAnchor See {@link #lineAnchor}. + * @param position See {@link #position}. + * @param positionAnchor See {@link #positionAnchor}. + * @param size See {@link #size}. + * @param windowColorSet See {@link #windowColorSet}. + * @param windowColor See {@link #windowColor}. + */ + 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 = text; this.textAlignment = textAlignment; this.line = line; @@ -179,6 +222,8 @@ public class Cue { this.position = position; this.positionAnchor = positionAnchor; this.size = size; + this.windowColorSet = windowColorSet; + this.windowColor = windowColor; } } 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 de461ecf0d..04f3b986bd 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 @@ -146,9 +146,13 @@ import com.google.android.exoplayer2.util.Util; // 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; } if (areCharSequencesEqual(this.cueText, cueText) && Util.areEqual(this.cueTextAlignment, cue.textAlignment) @@ -161,7 +165,7 @@ import com.google.android.exoplayer2.util.Util; && this.applyEmbeddedStyles == applyEmbeddedStyles && this.foregroundColor == style.foregroundColor && this.backgroundColor == style.backgroundColor - && this.windowColor == style.windowColor + && this.windowColor == windowColor && this.edgeType == style.edgeType && this.edgeColor == style.edgeColor && Util.areEqual(this.textPaint.getTypeface(), style.typeface) @@ -187,7 +191,7 @@ import com.google.android.exoplayer2.util.Util; this.applyEmbeddedStyles = applyEmbeddedStyles; this.foregroundColor = style.foregroundColor; this.backgroundColor = style.backgroundColor; - this.windowColor = style.windowColor; + this.windowColor = windowColor; this.edgeType = style.edgeType; this.edgeColor = style.edgeColor; this.textPaint.setTypeface(style.typeface); From 6c4795b49631ba33d76b39f28c89deba150b9222 Mon Sep 17 00:00:00 2001 From: anjalibh Date: Thu, 15 Dec 2016 14:42:37 -0800 Subject: [PATCH 014/142] Prevent frozen frames when the decoder is always late. Create a MediaCodecVideoTrackRenderer.shouldDropFrame function that can be overriden by a child class. The YouTube override prevents a frame drop if we haven't rendered anything in the last 35 ms. The YouTube override is off at the moment, I plan to use a server side flag to do a slow and controlled experiment. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142190774 --- .../exoplayer2/video/MediaCodecVideoRenderer.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 2a13953106..f68b72fb65 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 @@ -403,7 +403,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs); earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; - if (earlyUs < -30000) { + if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { // We're more than 30ms late rendering the frame. dropOutputBuffer(codec, bufferIndex); return true; @@ -437,6 +437,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return false; } + /** + * Returns true if the current frame should be dropped. + * + * @param earlyUs Time indicating how early the frame is. Negative values indicate late frame. + * @param elapsedRealtimeUs Wall clock time. + */ + protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { + // Drop the frame if we're more than 30ms late rendering the frame. + return earlyUs < -30000; + } + private void skipOutputBuffer(MediaCodec codec, int bufferIndex) { TraceUtil.beginSection("skipVideoBuffer"); codec.releaseOutputBuffer(bufferIndex, false); From e0586a48f02131d66fa2988d29a5064e5836ea6f Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 16 Dec 2016 06:03:28 -0800 Subject: [PATCH 015/142] Correctly offset subsample timestamps. This has always been broken in V2, but the issue is now also visible for the very first period in the timeline because we offset if by 60s. Previously the issue would only have been visible from the start of the second period. Issue: #2208 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142252702 --- .../java/com/google/android/exoplayer2/BaseRenderer.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 447e39bf52..514bbca8f4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -267,6 +267,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ; } buffer.timeUs += streamOffsetUs; + } else if (result == C.RESULT_FORMAT_READ) { + Format format = formatHolder.format; + if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { + format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + streamOffsetUs); + formatHolder.format = format; + } } return result; } From 4bb8793203ff280189d15849669304ebb138d814 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 16 Dec 2016 07:24:51 -0800 Subject: [PATCH 016/142] Deduplicate reported position discontinuities ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142257743 --- .../android/exoplayer2/ExoPlayerTest.java | 20 ++++++------------- .../android/exoplayer2/ExoPlayerImpl.java | 6 ++++-- .../exoplayer2/ExoPlayerImplInternal.java | 10 +++++++--- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 211ae954b2..0f6f3b07b1 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -84,7 +84,6 @@ public final class ExoPlayerTest extends TestCase { private Format expectedFormat; private ExoPlayer player; private Exception exception; - private boolean seenPositionDiscontinuity; public PlayerWrapper() { endedCountDownLatch = new CountDownLatch(1); @@ -121,7 +120,7 @@ public final class ExoPlayerTest extends TestCase { player.setPlayWhenReady(true); player.prepare(new FakeMediaSource(timeline, manifest, format)); } catch (Exception e) { - handlePlayerException(e); + handleError(e); } } }); @@ -136,7 +135,7 @@ public final class ExoPlayerTest extends TestCase { player.release(); } } catch (Exception e) { - handlePlayerException(e); + handleError(e); } finally { playerThread.quit(); } @@ -145,7 +144,7 @@ public final class ExoPlayerTest extends TestCase { playerThread.join(); } - private void handlePlayerException(Exception exception) { + private void handleError(Exception exception) { if (this.exception == null) { this.exception = exception; } @@ -180,20 +179,13 @@ public final class ExoPlayerTest extends TestCase { @Override public void onPlayerError(ExoPlaybackException exception) { - this.exception = exception; - endedCountDownLatch.countDown(); + handleError(exception); } @Override public void onPositionDiscontinuity() { - assertFalse(seenPositionDiscontinuity); - assertEquals(0, player.getCurrentWindowIndex()); - assertEquals(0, player.getCurrentPeriodIndex()); - assertEquals(0, player.getCurrentPosition()); - assertEquals(0, player.getBufferedPosition()); - assertEquals(expectedTimeline, player.getCurrentTimeline()); - assertEquals(expectedManifest, player.getCurrentManifest()); - seenPositionDiscontinuity = true; + // Should never happen. + handleError(new IllegalStateException("Received position discontinuity")); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index a7cbeb524c..ab4e59e08f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -332,8 +332,10 @@ import java.util.concurrent.CopyOnWriteArraySet; case ExoPlayerImplInternal.MSG_SEEK_ACK: { if (--pendingSeekAcks == 0) { playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(); + if (msg.arg1 != 0) { + for (EventListener listener : listeners) { + listener.onPositionDiscontinuity(); + } } } break; 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 8866bb7c48..afae56f1aa 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -559,7 +559,7 @@ import java.io.IOException; // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. playbackInfo = new PlaybackInfo(0, 0); - eventHandler.obtainMessage(MSG_SEEK_ACK, playbackInfo).sendToTarget(); + eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget(); // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't // ignored. playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); @@ -569,6 +569,7 @@ import java.io.IOException; return; } + boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; int periodIndex = periodPosition.first; long periodPositionUs = periodPosition.second; @@ -578,10 +579,13 @@ import java.io.IOException; // Seek position equals the current position. Do nothing. return; } - periodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs); + long newPeriodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs); + seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; + periodPositionUs = newPeriodPositionUs; } finally { playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs); - eventHandler.obtainMessage(MSG_SEEK_ACK, playbackInfo).sendToTarget(); + eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo) + .sendToTarget(); } } From ab88821614cd2762b4fb4f79234b8963c4deb11c Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 19 Dec 2016 06:56:04 -0800 Subject: [PATCH 017/142] Fix large timestamps for HLS playbacks - If there's no program-date-time then this change is a no-op. - If there is a program-date-time this change considers the period as having started at the epoch rather than at the start of the content. The window is then set to start at the start of the content. This is a little weird, but is required so that the period sample timestamps match the start of the period. Note that this also brings the handling of on-demand in line with how the live case is handled, meaning there wont be weird changes if a live stream changes into an on-demand one. Issue: #2224 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142442719 --- .../android/exoplayer2/source/hls/HlsMediaSource.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 2f46fc694c..869efa6cdc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -104,15 +104,14 @@ public final class HlsMediaSource implements MediaSource, SinglePeriodTimeline timeline; if (playlistTracker.isLive()) { // TODO: fix windowPositionInPeriodUs when playlist is empty. - long windowPositionInPeriodUs = playlist.startTimeUs; List segments = playlist.segments; long windowDefaultStartPositionUs = segments.isEmpty() ? 0 : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs; timeline = new SinglePeriodTimeline(C.TIME_UNSET, playlist.durationUs, - windowPositionInPeriodUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); + playlist.startTimeUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag); } else /* not live */ { - timeline = new SinglePeriodTimeline(playlist.durationUs, playlist.durationUs, 0, 0, true, - false); + timeline = new SinglePeriodTimeline(playlist.startTimeUs + playlist.durationUs, + playlist.durationUs, playlist.startTimeUs, 0, true, false); } sourceListener.onSourceInfoRefreshed(timeline, playlist); } From a007d9a2e73a5cac811f82c8f57ac84aed527c03 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 20 Dec 2016 03:28:19 -0800 Subject: [PATCH 018/142] Bump version + update release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=142539314 --- RELEASENOTES.md | 16 +++++++++++++--- build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../android/exoplayer2/ExoPlayerLibraryInfo.java | 4 ++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index feaa240220..8ddbe4068c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,9 +1,19 @@ # Release notes # -### r2.1.0 ### +### r2.1.1 ### -This release contains important bug fixes. Users of r2.0.x should proactively -update to this version. +Bugfix release only. Users of r2.1.0 and r2.0.x should proactively update to +this version. + +* Fix some subtitle types (e.g. WebVTT) being displayed out of sync + ([#2208](https://github.com/google/ExoPlayer/issues/2208)). +* Fix incorrect position reporting for on-demand HLS media that includes + EXT-X-PROGRAM-DATE-TIME tags + ([#2224](https://github.com/google/ExoPlayer/issues/2224)). +* Fix issue where playbacks could get stuck in the initial buffering state if + over 1MB of data needs to be read to initialize the playback. + +### r2.1.0 ### * HLS: Support for seeking in live streams ([#87](https://github.com/google/ExoPlayer/issues/87)). diff --git a/build.gradle b/build.gradle index 0ea3ad66f3..358b8f1404 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ allprojects { releaseRepoName = 'exoplayer' releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.1.0' + releaseVersion = 'r2.1.1' releaseWebsite = 'https://github.com/google/ExoPlayer' } } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index d1b44abafe..4c6d832211 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2101" + android:versionName="2.1.1"> 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 45f63d713d..ea522ac4c8 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.0"; + String VERSION = "2.1.1"; /** * 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 = 2001000; + int VERSION_INT = 2001001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 0d135d37b8e3d9118d80c3475aa08e2575efcf14 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 20 Dec 2016 12:33:04 +0000 Subject: [PATCH 019/142] Add comment for workaround --- .../java/com/google/android/exoplayer2/audio/AudioTrack.java | 2 ++ 1 file changed, 2 insertions(+) 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 485fc36c46..072180db94 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 @@ -443,6 +443,8 @@ public final class AudioTrack { default: throw new IllegalArgumentException("Unsupported channel count: " + channelCount); } + + // Workaround for overly strict channel configuration checks on nVidia Shield. if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) { switch(channelCount) { case 7: From 163a3a7bb876d309f5d28cc62473f093802d8aea Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 21 Dec 2016 00:51:30 +0000 Subject: [PATCH 020/142] Delete HlsTest.java --- .../exoplayer2/playbacktests/hls/HlsTest.java | 171 ------------------ 1 file changed, 171 deletions(-) delete mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/hls/HlsTest.java diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/hls/HlsTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/hls/HlsTest.java deleted file mode 100644 index 99f8944c48..0000000000 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/hls/HlsTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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.playbacktests.hls; - -import android.annotation.TargetApi; -import android.net.Uri; -import android.test.ActivityInstrumentationTestCase2; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; -import com.google.android.exoplayer2.playbacktests.util.HostActivity; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.ClosedSource; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; - -/** - * Tests HLS playbacks using {@link ExoPlayer}. - */ -@ClosedSource(reason = "Streams are internal") -public final class HlsTest extends ActivityInstrumentationTestCase2 { - - private static final String TAG = "HlsTest"; - private static final String BASE_URL = "https://storage.googleapis.com/" - + "exoplayer-test-media-internal-63834241aced7884c2544af1a3452e01/hls/bipbop/"; - private static final long TIMEOUT_MS = 3 * 60 * 1000; - - public HlsTest() { - super(HostActivity.class); - } - - /** - * Tests playback for two variants with all segments available. - */ - public void testAllSegmentsAvailable() throws IOException { - testPlaybackForPath("bipbop-all-200.m3u8"); - } - - /** - * Tests playback for a single variant with all segments available. - */ - public void testSingleGearAllSegmentsAvailable() throws IOException { - testPlaybackForPath("gear1/prog_index.m3u8"); - } - - /** - * Tests playback for two variants where the first has an unavailable playlist. Playback should - * succeed using the second variant. - */ - public void testGear1PlaylistMissing() throws IOException { - testPlaybackForPath("bipbop-gear1-playlist-404.m3u8"); - } - - /** - * Tests playback for two variants where the second has an unavailable playlist. Playback should - * succeed using the first variant. - */ - public void testGear2PlaylistMissing() throws IOException { - testPlaybackForPath("bipbop-gear2-playlist-404.m3u8"); - } - - /** - * Tests playback for two variants where the first has a missing first segment. Playback should - * succeed using the first segment from the second variant. - */ - public void testGear1Seg1Missing() throws IOException { - testPlaybackForPath("bipbop-gear1-seg1-404.m3u8"); - } - - /** - * Tests playback for two variants where the second has a missing first segment. Playback should - * succeed using the first segment from the first variant. - */ - public void testGear2Seg1Missing() throws IOException { - testPlaybackForPath("bipbop-gear2-seg1-404.m3u8"); - } - - /** - * Tests playback for two variants where the first has a missing second segment. Playback should - * succeed using the second segment from the second variant. - */ - public void testGear1Seg2Missing() throws IOException { - testPlaybackForPath("bipbop-gear1-seg2-404.m3u8"); - } - - /** - * Tests playback for two variants where the second has a missing second segment. Playback should - * succeed using the second segment from the first variant. - */ - public void testGear2Seg2Missing() throws IOException { - testPlaybackForPath("bipbop-gear2-seg2-404.m3u8"); - } - - /** - * Tests playback for two variants where the first has a missing sixth segment. Playback should - * succeed using the sixth segment from the second variant. - */ - public void testGear1Seg6Missing() throws IOException { - testPlaybackForPath("bipbop-gear1-seg6-404.m3u8"); - } - - /** - * Tests playback for two variants where the second has a missing sixth segment. Playback should - * succeed using the sixth segment from the first variant. - */ - public void testGear2Seg6Missing() throws IOException { - testPlaybackForPath("bipbop-gear2-seg6-404.m3u8"); - } - - /** - * Tests playback of a single variant with a missing sixth segment. Playback should fail, however - * should not do so until playback reaches the missing segment at 60 seconds. - */ - public void testSingleGearSeg6Missing() throws IOException { - testPlaybackForPath("gear1/prog_index-seg6-404.m3u8", 60000); - } - - private void testPlaybackForPath(String path) throws IOException { - testPlaybackForPath(path, C.TIME_UNSET); - } - - private void testPlaybackForPath(String path, long expectedFailureTimeMs) throws IOException { - if (Util.SDK_INT < 16) { - // Pass. - return; - } - HlsHostedTest test = new HlsHostedTest(Uri.parse(BASE_URL + path), expectedFailureTimeMs); - getActivity().runTest(test, TIMEOUT_MS); - } - - @TargetApi(16) - private static class HlsHostedTest extends ExoHostedTest { - - private final Uri playlistUri; - - public HlsHostedTest(Uri playlistUri, long expectedFailureTimeMs) { - super(TAG, expectedFailureTimeMs == C.TIME_UNSET - ? ExoHostedTest.EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS : expectedFailureTimeMs, - expectedFailureTimeMs == C.TIME_UNSET); - this.playlistUri = Assertions.checkNotNull(playlistUri); - } - - @Override - public MediaSource buildSource(HostActivity host, String userAgent, - TransferListener mediaTransferListener) { - DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(host, userAgent, - mediaTransferListener); - return new HlsMediaSource(playlistUri, dataSourceFactory, null, null); - } - - } - -} From eda393ba824e3345cdef20f060377a71e54c1a18 Mon Sep 17 00:00:00 2001 From: meteoorkip Date: Sat, 31 Dec 2016 22:48:14 +0100 Subject: [PATCH 021/142] Add default artwork support to SimpleExoPlayerView Add support for a default artwork image that is displayed if no artwork can be found in the metadata. --- .../exoplayer2/ui/SimpleExoPlayerView.java | 64 +++++++++++++++---- library/src/main/res/values/attrs.xml | 1 + 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index d094266fcc..8ac0c64082 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -65,6 +65,13 @@ import java.util.List; *

  • Default: {@code true}
  • * * + *
  • {@code default_artwork} - Default artwork to use if no artwork available in audio + * streams. + *
      + *
    • Corresponding method: {@link #setDefaultArtwork(Bitmap)}
    • + *
    • Default: {@code null}
    • + *
    + *
  • *
  • {@code use_controller} - Whether playback controls are displayed. * *
  • - *
  • {@code default_artwork} - Default artwork to use if no artwork available in audio - * streams. - *
      - *
    • Corresponding method: {@link #setDefaultArtwork(Bitmap)}
    • - *
    • Default: {@code null}
    • - *
    - *
  • *
  • {@code use_controller} - Whether playback controls are displayed. * *
  • + *
  • {@code default_artwork} - Default artwork to use if no artwork available in audio + * streams. + *
      + *
    • Corresponding method: {@link #setDefaultArtwork(Bitmap)}
    • + *
    • Default: {@code null}
    • + *
    + *
  • *
  • {@code use_controller} - Whether playback controls are displayed. * */ SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() { @@ -78,6 +80,9 @@ public interface SubtitleDecoderFactory { || format.sampleMimeType.equals(MimeTypes.APPLICATION_MP4CEA608)) { return clazz.asSubclass(SubtitleDecoder.class).getConstructor(String.class, Integer.TYPE) .newInstance(format.sampleMimeType, format.accessibilityChannel); + } else if (format.sampleMimeType.equals(MimeTypes.APPLICATION_CEA708)) { + return clazz.asSubclass(SubtitleDecoder.class).getConstructor(Integer.TYPE) + .newInstance(format.accessibilityChannel); } else { return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance(); } @@ -105,6 +110,8 @@ public interface SubtitleDecoderFactory { case MimeTypes.APPLICATION_CEA608: case MimeTypes.APPLICATION_MP4CEA608: return Class.forName("com.google.android.exoplayer2.text.cea.Cea608Decoder"); + case MimeTypes.APPLICATION_CEA708: + return Class.forName("com.google.android.exoplayer2.text.cea.Cea708Decoder"); default: return null; } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java new file mode 100644 index 0000000000..e63d1d4118 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java @@ -0,0 +1,67 @@ +/* + * 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.text.cea; + +import android.text.Layout.Alignment; +import com.google.android.exoplayer2.text.Cue; + +/** + * A {@link Cue} for CEA-708. + */ +/* package */ final class Cea708Cue extends Cue implements Comparable { + + /** + * An unset priority. + */ + public static final int PRIORITY_UNSET = -1; + + /** + * The priority of the cue box. + */ + public final int priority; + + /** + * @param text See {@link #text}. + * @param textAlignment See {@link #textAlignment}. + * @param line See {@link #line}. + * @param lineType See {@link #lineType}. + * @param lineAnchor See {@link #lineAnchor}. + * @param position See {@link #position}. + * @param positionAnchor See {@link #positionAnchor}. + * @param size See {@link #size}. + * @param windowColorSet See {@link #windowColorSet}. + * @param windowColor See {@link #windowColor}. + * @param priority See (@link #priority}. + */ + public Cea708Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, + @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, + boolean windowColorSet, int windowColor, int priority) { + super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, + windowColorSet, windowColor); + this.priority = priority; + } + + @Override + public int compareTo(Cea708Cue other) { + if (other.priority < priority) { + return -1; + } else if (other.priority > priority) { + return 1; + } + return 0; + } + +} 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 new file mode 100644 index 0000000000..5ca5ce1270 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -0,0 +1,1225 @@ +/* + * 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.text.cea; + +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.Layout.Alignment; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; +import android.util.Log; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Cue.AnchorType; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.text.SubtitleDecoder; +import com.google.android.exoplayer2.text.SubtitleInputBuffer; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +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 { + + private static final String TAG = "Cea708Decoder"; + + private static final int NUM_WINDOWS = 8; + + private static final int DTVCC_PACKET_DATA = 0x02; + private static final int DTVCC_PACKET_START = 0x03; + private static final int CC_VALID_FLAG = 0x04; + + // Base Commands + private static final int GROUP_C0_END = 0x1F; // Miscellaneous Control Codes + private static final int GROUP_G0_END = 0x7F; // ASCII Printable Characters + private static final int GROUP_C1_END = 0x9F; // Captioning Command Control Codes + private static final int GROUP_G1_END = 0xFF; // ISO 8859-1 LATIN-1 Character Set + + // Extended Commands + private static final int GROUP_C2_END = 0x1F; // Extended Control Code Set 1 + private static final int GROUP_G2_END = 0x7F; // Extended Miscellaneous Characters + private static final int GROUP_C3_END = 0x9F; // Extended Control Code Set 2 + private static final int GROUP_G3_END = 0xFF; // Future Expansion + + // Group C0 Commands + private static final int COMMAND_NUL = 0x00; // Nul + private static final int COMMAND_ETX = 0x03; // EndOfText + private static final int COMMAND_BS = 0x08; // Backspace + private static final int COMMAND_FF = 0x0C; // FormFeed (Flush) + private static final int COMMAND_CR = 0x0D; // CarriageReturn + private static final int COMMAND_HCR = 0x0E; // ClearLine + private static final int COMMAND_EXT1 = 0x10; // Extended Control Code Flag + private static final int COMMAND_EXT1_START = 0x11; + private static final int COMMAND_EXT1_END = 0x17; + private static final int COMMAND_P16_START = 0x18; + private static final int COMMAND_P16_END = 0x1F; + + // Group C1 Commands + private static final int COMMAND_CW0 = 0x80; // SetCurrentWindow to 0 + private static final int COMMAND_CW1 = 0x81; // SetCurrentWindow to 1 + private static final int COMMAND_CW2 = 0x82; // SetCurrentWindow to 2 + private static final int COMMAND_CW3 = 0x83; // SetCurrentWindow to 3 + private static final int COMMAND_CW4 = 0x84; // SetCurrentWindow to 4 + private static final int COMMAND_CW5 = 0x85; // SetCurrentWindow to 5 + private static final int COMMAND_CW6 = 0x86; // SetCurrentWindow to 6 + private static final int COMMAND_CW7 = 0x87; // SetCurrentWindow to 7 + private static final int COMMAND_CLW = 0x88; // ClearWindows (+1 byte) + private static final int COMMAND_DSW = 0x89; // DisplayWindows (+1 byte) + private static final int COMMAND_HDW = 0x8A; // HideWindows (+1 byte) + private static final int COMMAND_TGW = 0x8B; // ToggleWindows (+1 byte) + private static final int COMMAND_DLW = 0x8C; // DeleteWindows (+1 byte) + private static final int COMMAND_DLY = 0x8D; // Delay (+1 byte) + private static final int COMMAND_DLC = 0x8E; // DelayCancel + private static final int COMMAND_RST = 0x8F; // Reset + private static final int COMMAND_SPA = 0x90; // SetPenAttributes (+2 bytes) + private static final int COMMAND_SPC = 0x91; // SetPenColor (+3 bytes) + private static final int COMMAND_SPL = 0x92; // SetPenLocation (+2 bytes) + private static final int COMMAND_SWA = 0x97; // SetWindowAttributes (+4 bytes) + private static final int COMMAND_DF0 = 0x98; // DefineWindow 0 (+6 bytes) + private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes) + private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes) + private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes) + private static final int COMMAND_DS4 = 0x9C; // DefineWindow 4 (+6 bytes) + private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes) + private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes) + private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes) + + // G0 Table Special Chars + private static final int CHARACTER_MN = 0x7F; // MusicNote + + // G2 Table Special Chars + private static final int CHARACTER_TSP = 0x20; + private static final int CHARACTER_NBTSP = 0x21; + private static final int CHARACTER_ELLIPSIS = 0x25; + private static final int CHARACTER_BIG_CARONS = 0x2A; + private static final int CHARACTER_BIG_OE = 0x2C; + private static final int CHARACTER_SOLID_BLOCK = 0x30; + private static final int CHARACTER_OPEN_SINGLE_QUOTE = 0x31; + private static final int CHARACTER_CLOSE_SINGLE_QUOTE = 0x32; + private static final int CHARACTER_OPEN_DOUBLE_QUOTE = 0x33; + private static final int CHARACTER_CLOSE_DOUBLE_QUOTE = 0x34; + private static final int CHARACTER_BOLD_BULLET = 0x35; + private static final int CHARACTER_TM = 0x39; + private static final int CHARACTER_SMALL_CARONS = 0x3A; + private static final int CHARACTER_SMALL_OE = 0x3C; + private static final int CHARACTER_SM = 0x3D; + private static final int CHARACTER_DIAERESIS_Y = 0x3F; + private static final int CHARACTER_ONE_EIGHTH = 0x76; + private static final int CHARACTER_THREE_EIGHTHS = 0x77; + private static final int CHARACTER_FIVE_EIGHTHS = 0x78; + private static final int CHARACTER_SEVEN_EIGHTHS = 0x79; + private static final int CHARACTER_VERTICAL_BORDER = 0x7A; + private static final int CHARACTER_UPPER_RIGHT_BORDER = 0x7B; + private static final int CHARACTER_LOWER_LEFT_BORDER = 0x7C; + private static final int CHARACTER_HORIZONTAL_BORDER = 0x7D; + private static final int CHARACTER_LOWER_RIGHT_BORDER = 0x7E; + private static final int CHARACTER_UPPER_LEFT_BORDER = 0x7F; + + private final ParsableByteArray ccData; + private final ParsableBitArray serviceBlockPacket; + + private final int selectedServiceNumber; + private final CueBuilder[] cueBuilders; + + private CueBuilder currentCueBuilder; + private List cues; + private List lastCues; + + private DtvCcPacket currentDtvCcPacket; + private int currentWindow; + + public Cea708Decoder(int accessibilityChannel) { + ccData = new ParsableByteArray(); + serviceBlockPacket = new ParsableBitArray(); + selectedServiceNumber = (accessibilityChannel == Format.NO_VALUE) ? 1 : accessibilityChannel; + + cueBuilders = new CueBuilder[NUM_WINDOWS]; + for (int i = 0; i < NUM_WINDOWS; i++) { + cueBuilders[i] = new CueBuilder(); + } + + currentCueBuilder = cueBuilders[0]; + resetCueBuilders(); + } + + @Override + public String getName() { + return "Cea708Decoder"; + } + + @Override + public void flush() { + super.flush(); + cues = null; + lastCues = null; + currentWindow = 0; + currentCueBuilder = cueBuilders[currentWindow]; + resetCueBuilders(); + currentDtvCcPacket = null; + } + + @Override + protected boolean isNewSubtitleDataAvailable() { + return cues != lastCues; + } + + @Override + protected Subtitle createSubtitle() { + lastCues = cues; + return new CeaSubtitle(cues); + } + + @Override + protected void decode(SubtitleInputBuffer inputBuffer) { + ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit()); + while (ccData.bytesLeft() >= 3) { + int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07); + + int ccType = ccTypeAndValid & (DTVCC_PACKET_DATA | DTVCC_PACKET_START); + boolean ccValid = (ccTypeAndValid & CC_VALID_FLAG) == CC_VALID_FLAG; + byte ccData1 = (byte) ccData.readUnsignedByte(); + byte ccData2 = (byte) ccData.readUnsignedByte(); + + // Ignore any non-CEA-708 data + if (ccType != DTVCC_PACKET_DATA && ccType != DTVCC_PACKET_START) { + continue; + } + + if (!ccValid) { + finalizeCurrentPacket(); + continue; + } + + if (ccType == DTVCC_PACKET_START) { + finalizeCurrentPacket(); + + int sequenceNumber = (ccData1 & 0xC0) >> 6; // first 2 bits + int packetSize = ccData1 & 0x3F; // last 6 bits + if (packetSize == 0) { + packetSize = 64; + } + + currentDtvCcPacket = new DtvCcPacket(sequenceNumber, packetSize); + currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2; + } else { + // The only remaining valid packet type is DTVCC_PACKET_DATA + Assertions.checkArgument(ccType == DTVCC_PACKET_DATA); + + if (currentDtvCcPacket == null) { + Log.e(TAG, "Encountered DTVCC_PACKET_DATA before DTVCC_PACKET_START"); + continue; + } + + currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData1; + currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2; + } + + if (currentDtvCcPacket.currentIndex == (currentDtvCcPacket.packetSize * 2 - 1)) { + finalizeCurrentPacket(); + } + } + } + + private void finalizeCurrentPacket() { + if (currentDtvCcPacket == null) { + // No packet to finalize; + return; + } + + processCurrentPacket(); + currentDtvCcPacket = null; + } + + private void processCurrentPacket() { + 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 + ")"); + } + + serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex); + + int serviceNumber = serviceBlockPacket.readBits(3); + int blockSize = serviceBlockPacket.readBits(5); + if (serviceNumber == 7) { + // extended service numbers + serviceBlockPacket.skipBits(2); + serviceNumber += serviceBlockPacket.readBits(6); + } + + // Ignore packets in which blockSize is 0 + if (blockSize == 0) { + if (serviceNumber != 0) { + Log.w(TAG, "serviceNumber is non-zero (" + serviceNumber + ") when blockSize is 0"); + } + return; + } + + if (serviceNumber != selectedServiceNumber) { + return; + } + + while (serviceBlockPacket.bitsLeft() > 0) { + int command = serviceBlockPacket.readBits(8); + if (command != COMMAND_EXT1) { + if (command <= GROUP_C0_END) { + handleC0Command(command); + } else if (command <= GROUP_G0_END) { + handleG0Character(command); + } else if (command <= GROUP_C1_END) { + handleC1Command(command); + // Cues are always updated after a C1 command + cues = getDisplayCues(); + } else if (command <= GROUP_G1_END) { + handleG1Character(command); + } else { + Log.w(TAG, "Invalid base command: " + command); + } + } else { + // Read the extended command + command = serviceBlockPacket.readBits(8); + if (command <= GROUP_C2_END) { + handleC2Command(command); + } else if (command <= GROUP_G2_END) { + handleG2Character(command); + } else if (command <= GROUP_C3_END) { + handleC3Command(command); + } else if (command <= GROUP_G3_END) { + handleG3Character(command); + } else { + Log.w(TAG, "Invalid extended command: " + command); + } + } + } + } + + private void handleC0Command(int command) { + switch (command) { + case COMMAND_NUL: + // Do nothing. + break; + case COMMAND_ETX: + cues = getDisplayCues(); + break; + case COMMAND_BS: + currentCueBuilder.backspace(); + break; + case COMMAND_FF: + resetCueBuilders(); + break; + case COMMAND_CR: + currentCueBuilder.append('\n'); + break; + case COMMAND_HCR: + // TODO: Add support for this command. + break; + default: + if (command >= COMMAND_EXT1_START && command <= COMMAND_EXT1_END) { + Log.w(TAG, "Currently unsupported COMMAND_EXT1 Command: " + command); + serviceBlockPacket.skipBits(8); + } else if (command >= COMMAND_P16_START && command <= COMMAND_P16_END) { + Log.w(TAG, "Currently unsupported COMMAND_P16 Command: " + command); + serviceBlockPacket.skipBits(16); + } else { + Log.w(TAG, "Invalid C0 command: " + command); + } + } + } + + private void handleC1Command(int command) { + int window; + switch (command) { + case COMMAND_CW0: + case COMMAND_CW1: + case COMMAND_CW2: + case COMMAND_CW3: + case COMMAND_CW4: + case COMMAND_CW5: + case COMMAND_CW6: + case COMMAND_CW7: + window = (command - COMMAND_CW0); + if (currentWindow != window) { + currentWindow = window; + currentCueBuilder = cueBuilders[window]; + } + break; + case COMMAND_CLW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (serviceBlockPacket.readBit()) { + cueBuilders[NUM_WINDOWS - i].clear(); + } + } + break; + case COMMAND_DSW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (serviceBlockPacket.readBit()) { + cueBuilders[NUM_WINDOWS - i].setVisibility(true); + } + } + break; + case COMMAND_HDW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (serviceBlockPacket.readBit()) { + cueBuilders[NUM_WINDOWS - i].setVisibility(false); + } + } + break; + case COMMAND_TGW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (serviceBlockPacket.readBit()) { + CueBuilder cueBuilder = cueBuilders[NUM_WINDOWS - i]; + cueBuilder.setVisibility(!cueBuilder.isVisible()); + } + } + break; + case COMMAND_DLW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (serviceBlockPacket.readBit()) { + cueBuilders[NUM_WINDOWS - i].reset(); + } + } + break; + case COMMAND_DLY: + // TODO: Add support for delay commands. + serviceBlockPacket.skipBits(8); + break; + case COMMAND_DLC: + // TODO: Add support for delay commands. + break; + case COMMAND_RST: + resetCueBuilders(); + break; + case COMMAND_SPA: + if (!currentCueBuilder.isDefined()) { + // ignore this command if the current window/cue isn't defined + serviceBlockPacket.skipBits(16); + } else { + handleSetPenAttributes(); + } + break; + case COMMAND_SPC: + if (!currentCueBuilder.isDefined()) { + // ignore this command if the current window/cue isn't defined + serviceBlockPacket.skipBits(24); + } else { + handleSetPenColor(); + } + break; + case COMMAND_SPL: + if (!currentCueBuilder.isDefined()) { + // ignore this command if the current window/cue isn't defined + serviceBlockPacket.skipBits(16); + } else { + handleSetPenLocation(); + } + break; + case COMMAND_SWA: + if (!currentCueBuilder.isDefined()) { + // ignore this command if the current window/cue isn't defined + serviceBlockPacket.skipBits(32); + } else { + handleSetWindowAttributes(); + } + break; + case COMMAND_DF0: + case COMMAND_DF1: + case COMMAND_DF2: + case COMMAND_DF3: + case COMMAND_DS4: + case COMMAND_DF5: + case COMMAND_DF6: + case COMMAND_DF7: + window = (command - COMMAND_DF0); + handleDefineWindow(window); + break; + default: + Log.w(TAG, "Invalid C1 command: " + command); + } + } + + private void handleC2Command(int command) { + // C2 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes + if (command <= 0x0F) { + // Do nothing. + } else if (command <= 0x0F) { + serviceBlockPacket.skipBits(8); + } else if (command <= 0x17) { + serviceBlockPacket.skipBits(16); + } else if (command <= 0x1F) { + serviceBlockPacket.skipBits(24); + } + } + + private void handleC3Command(int command) { + // C3 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes + if (command <= 0x87) { + serviceBlockPacket.skipBits(32); + } else if (command <= 0x8F) { + serviceBlockPacket.skipBits(40); + } else if (command <= 0x9F) { + // 90-9F are variable length codes; the first byte defines the header with the first + // 2 bits specifying the type and the last 6 bits specifying the remaining length of the + // command in bytes + serviceBlockPacket.skipBits(2); + int length = serviceBlockPacket.readBits(6); + serviceBlockPacket.skipBits(8 * length); + } + } + + private void handleG0Character(int characterCode) { + if (characterCode == CHARACTER_MN) { + currentCueBuilder.append('\u266B'); + } else { + currentCueBuilder.append((char) (characterCode & 0xFF)); + } + } + + private void handleG1Character(int characterCode) { + currentCueBuilder.append((char) (characterCode & 0xFF)); + } + + private void handleG2Character(int characterCode) { + switch (characterCode) { + case CHARACTER_TSP: + currentCueBuilder.append('\u0020'); + break; + case CHARACTER_NBTSP: + currentCueBuilder.append('\u00A0'); + break; + case CHARACTER_ELLIPSIS: + currentCueBuilder.append('\u2026'); + break; + case CHARACTER_BIG_CARONS: + currentCueBuilder.append('\u0160'); + break; + case CHARACTER_BIG_OE: + currentCueBuilder.append('\u0152'); + break; + case CHARACTER_SOLID_BLOCK: + currentCueBuilder.append('\u2588'); + break; + case CHARACTER_OPEN_SINGLE_QUOTE: + currentCueBuilder.append('\u2018'); + break; + case CHARACTER_CLOSE_SINGLE_QUOTE: + currentCueBuilder.append('\u2019'); + break; + case CHARACTER_OPEN_DOUBLE_QUOTE: + currentCueBuilder.append('\u201C'); + break; + case CHARACTER_CLOSE_DOUBLE_QUOTE: + currentCueBuilder.append('\u201D'); + break; + case CHARACTER_BOLD_BULLET: + currentCueBuilder.append('\u2022'); + break; + case CHARACTER_TM: + currentCueBuilder.append('\u2122'); + break; + case CHARACTER_SMALL_CARONS: + currentCueBuilder.append('\u0161'); + break; + case CHARACTER_SMALL_OE: + currentCueBuilder.append('\u0153'); + break; + case CHARACTER_SM: + currentCueBuilder.append('\u2120'); + break; + case CHARACTER_DIAERESIS_Y: + currentCueBuilder.append('\u0178'); + break; + case CHARACTER_ONE_EIGHTH: + currentCueBuilder.append('\u215B'); + break; + case CHARACTER_THREE_EIGHTHS: + currentCueBuilder.append('\u215C'); + break; + case CHARACTER_FIVE_EIGHTHS: + currentCueBuilder.append('\u215D'); + break; + case CHARACTER_SEVEN_EIGHTHS: + currentCueBuilder.append('\u215E'); + break; + case CHARACTER_VERTICAL_BORDER: + currentCueBuilder.append('\u2502'); + break; + case CHARACTER_UPPER_RIGHT_BORDER: + currentCueBuilder.append('\u2510'); + break; + case CHARACTER_LOWER_LEFT_BORDER: + currentCueBuilder.append('\u2514'); + break; + case CHARACTER_HORIZONTAL_BORDER: + currentCueBuilder.append('\u2500'); + break; + case CHARACTER_LOWER_RIGHT_BORDER: + currentCueBuilder.append('\u2518'); + break; + case CHARACTER_UPPER_LEFT_BORDER: + currentCueBuilder.append('\u250C'); + break; + default: + Log.w(TAG, "Invalid G2 character: " + characterCode); + // The CEA-708 specification doesn't specify what to do in the case of an unexpected + // value in the G2 character range, so we ignore it. + } + } + + private void handleG3Character(int characterCode) { + if (characterCode == 0xA0) { + currentCueBuilder.append('\u33C4'); + } else { + Log.w(TAG, "Invalid G3 character: " + characterCode); + // Substitute any unsupported G3 character with an underscore as per CEA-708 specification. + currentCueBuilder.append('_'); + } + } + + private void handleSetPenAttributes() { + // the SetPenAttributes command contains 2 bytes of data + // first byte + int textTag = serviceBlockPacket.readBits(4); + int offset = serviceBlockPacket.readBits(2); + int penSize = serviceBlockPacket.readBits(2); + // second byte + boolean italicsToggle = serviceBlockPacket.readBit(); + boolean underlineToggle = serviceBlockPacket.readBit(); + int edgeType = serviceBlockPacket.readBits(3); + int fontStyle = serviceBlockPacket.readBits(3); + + currentCueBuilder.setPenAttributes(textTag, offset, penSize, italicsToggle, underlineToggle, + edgeType, fontStyle); + } + + private void handleSetPenColor() { + // the SetPenColor command contains 3 bytes of data + // first byte + int foregroundO = serviceBlockPacket.readBits(2); + int foregroundR = serviceBlockPacket.readBits(2); + int foregroundG = serviceBlockPacket.readBits(2); + int foregroundB = serviceBlockPacket.readBits(2); + int foregroundColor = CueBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB, + foregroundO); + // second byte + int backgroundO = serviceBlockPacket.readBits(2); + int backgroundR = serviceBlockPacket.readBits(2); + int backgroundG = serviceBlockPacket.readBits(2); + int backgroundB = serviceBlockPacket.readBits(2); + int backgroundColor = CueBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB, + backgroundO); + // third byte + serviceBlockPacket.skipBits(2); // null padding + int edgeR = serviceBlockPacket.readBits(2); + int edgeG = serviceBlockPacket.readBits(2); + int edgeB = serviceBlockPacket.readBits(2); + int edgeColor = CueBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB); + + currentCueBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor); + } + + private void handleSetPenLocation() { + // the SetPenLocation command contains 2 bytes of data + // first byte + serviceBlockPacket.skipBits(4); + int row = serviceBlockPacket.readBits(4); + // second byte + serviceBlockPacket.skipBits(2); + int column = serviceBlockPacket.readBits(6); + + currentCueBuilder.setPenLocation(row, column); + } + + private void handleSetWindowAttributes() { + // the SetWindowAttributes command contains 4 bytes of data + // first byte + int fillO = serviceBlockPacket.readBits(2); + int fillR = serviceBlockPacket.readBits(2); + int fillG = serviceBlockPacket.readBits(2); + int fillB = serviceBlockPacket.readBits(2); + int fillColor = CueBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO); + // second byte + int borderType = serviceBlockPacket.readBits(2); // only the lower 2 bits of borderType + int borderR = serviceBlockPacket.readBits(2); + int borderG = serviceBlockPacket.readBits(2); + int borderB = serviceBlockPacket.readBits(2); + int borderColor = CueBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB); + // third byte + if (serviceBlockPacket.readBit()) { + borderType |= 0x04; // set the top bit of the 3-bit borderType + } + boolean wordWrapToggle = serviceBlockPacket.readBit(); + int printDirection = serviceBlockPacket.readBits(2); + int scrollDirection = serviceBlockPacket.readBits(2); + int justification = serviceBlockPacket.readBits(2); + // fourth byte + // Note that we don't intend to support display effects + serviceBlockPacket.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2) + + currentCueBuilder.setWindowAttributes(fillColor, borderColor, wordWrapToggle, borderType, + printDirection, scrollDirection, justification); + } + + private void handleDefineWindow(int window) { + CueBuilder cueBuilder = cueBuilders[window]; + + // the DefineWindow command contains 6 bytes of data + // first byte + serviceBlockPacket.skipBits(2); // null padding + boolean visible = serviceBlockPacket.readBit(); + boolean rowLock = serviceBlockPacket.readBit(); + boolean columnLock = serviceBlockPacket.readBit(); + int priority = serviceBlockPacket.readBits(3); + // second byte + boolean relativePositioning = serviceBlockPacket.readBit(); + int verticalAnchor = serviceBlockPacket.readBits(7); + // third byte + int horizontalAnchor = serviceBlockPacket.readBits(8); + // fourth byte + int anchorId = serviceBlockPacket.readBits(4); + int rowCount = serviceBlockPacket.readBits(4); + // fifth byte + serviceBlockPacket.skipBits(2); // null padding + int columnCount = serviceBlockPacket.readBits(6); + // sixth byte + serviceBlockPacket.skipBits(2); // null padding + int windowStyle = serviceBlockPacket.readBits(3); + int penStyle = serviceBlockPacket.readBits(3); + + cueBuilder.defineWindow(visible, rowLock, columnLock, priority, relativePositioning, + verticalAnchor, horizontalAnchor, rowCount, columnCount, anchorId, windowStyle, penStyle); + } + + private List getDisplayCues() { + List displayCues = new ArrayList<>(); + for (int i = 0; i < NUM_WINDOWS; i++) { + if (!cueBuilders[i].isEmpty() && cueBuilders[i].isVisible()) { + displayCues.add(cueBuilders[i].build()); + } + } + Collections.sort(displayCues); + return Collections.unmodifiableList(displayCues); + } + + private void resetCueBuilders() { + for (int i = 0; i < NUM_WINDOWS; i++) { + cueBuilders[i].reset(); + } + } + + private static final class DtvCcPacket { + + public final int sequenceNumber; + public final int packetSize; + public final byte[] packetData; + + int currentIndex; + + public DtvCcPacket(int sequenceNumber, int packetSize) { + this.sequenceNumber = sequenceNumber; + this.packetSize = packetSize; + packetData = new byte[2 * packetSize - 1]; + currentIndex = 0; + } + + } + + // TODO: There is a lot of overlap between Cea708Decoder.CueBuilder and Cea608Decoder.CueBuilder + // which could be refactored into a separate class. + private static final class CueBuilder { + + private static final int RELATIVE_CUE_SIZE = 99; + private static final int VERTICAL_SIZE = 74; + private static final int HORIZONTAL_SIZE = 209; + + private static final int DEFAULT_PRIORITY = 4; + + private static final int MAXIMUM_ROW_COUNT = 15; + + private static final int JUSTIFICATION_LEFT = 0; + private static final int JUSTIFICATION_RIGHT = 1; + private static final int JUSTIFICATION_CENTER = 2; + private static final int JUSTIFICATION_FULL = 3; + + private static final int DIRECTION_LEFT_TO_RIGHT = 0; + private static final int DIRECTION_RIGHT_TO_LEFT = 1; + private static final int DIRECTION_TOP_TO_BOTTOM = 2; + private static final int DIRECTION_BOTTOM_TO_TOP = 3; + + // TODO: Add other border/edge types when utilized. + private static final int BORDER_AND_EDGE_TYPE_NONE = 0; + private static final int BORDER_AND_EDGE_TYPE_UNIFORM = 3; + + public static final int COLOR_SOLID_WHITE = getArgbColorFromCeaColor(2, 2, 2, 0); + public static final int COLOR_SOLID_BLACK = getArgbColorFromCeaColor(0, 0, 0, 0); + public static final int COLOR_TRANSPARENT = getArgbColorFromCeaColor(0, 0, 0, 3); + + // TODO: Add other sizes when utilized. + private static final int PEN_SIZE_STANDARD = 1; + + // TODO: Add other pen font styles when utilized. + private static final int PEN_FONT_STYLE_DEFAULT = 0; + private static final int PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS = 1; + private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS = 2; + private static final int PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS = 3; + private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS = 4; + + // TODO: Add other pen offsets when utilized. + private static final int PEN_OFFSET_NORMAL = 1; + + // The window style properties are specified in the CEA-708 specification. + private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[]{ + JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, + JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER, + JUSTIFICATION_LEFT + }; + private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[]{ + DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, + DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, + DIRECTION_TOP_TO_BOTTOM + }; + private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[]{ + DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, + DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, + DIRECTION_RIGHT_TO_LEFT + }; + private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[]{ + false, false, false, true, true, true, false + }; + private static final int[] WINDOW_STYLE_FILL = new int[]{ + COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, + COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK + }; + + // The pen style properties are specified in the CEA-708 specification. + private static final int[] PEN_STYLE_FONT_STYLE = new int[]{ + PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS, + PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, + PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS, + PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, + PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS + }; + private static final int[] PEN_STYLE_EDGE_TYPE = new int[]{ + BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, + BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM, + BORDER_AND_EDGE_TYPE_UNIFORM + }; + private static final int[] PEN_STYLE_BACKGROUND = new int[]{ + COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, + COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT}; + + private final List rolledUpCaptions; + private final SpannableStringBuilder captionStringBuilder; + + // Window/Cue properties + private boolean defined; + private boolean visible; + private int priority; + private boolean relativePositioning; + private int verticalAnchor; + private int horizontalAnchor; + private int anchorId; + private int rowCount; + private boolean rowLock; + private int justification; + private int windowStyleId; + private int penStyleId; + private int windowFillColor; + + // Pen/Text properties + private int italicsStartPosition; + private int underlineStartPosition; + private int foregroundColorStartPosition; + private int foregroundColor; + private int backgroundColorStartPosition; + private int backgroundColor; + + public CueBuilder() { + rolledUpCaptions = new LinkedList<>(); + captionStringBuilder = new SpannableStringBuilder(); + reset(); + } + + public boolean isEmpty() { + return !isDefined() || (rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0); + } + + public void reset() { + clear(); + + defined = false; + visible = false; + priority = DEFAULT_PRIORITY; + relativePositioning = false; + verticalAnchor = 0; + horizontalAnchor = 0; + anchorId = 0; + rowCount = MAXIMUM_ROW_COUNT; + rowLock = true; + justification = JUSTIFICATION_LEFT; + windowStyleId = 0; + penStyleId = 0; + windowFillColor = COLOR_SOLID_BLACK; + + foregroundColor = COLOR_SOLID_WHITE; + backgroundColor = COLOR_SOLID_BLACK; + } + + public void clear() { + rolledUpCaptions.clear(); + captionStringBuilder.clear(); + italicsStartPosition = C.POSITION_UNSET; + underlineStartPosition = C.POSITION_UNSET; + foregroundColorStartPosition = C.POSITION_UNSET; + backgroundColorStartPosition = C.POSITION_UNSET; + } + + public boolean isDefined() { + return defined; + } + + public void setVisibility(boolean visible) { + this.visible = visible; + } + + public boolean isVisible() { + return visible; + } + + public void defineWindow(boolean visible, boolean rowLock, boolean columnLock, int priority, + boolean relativePositioning, int verticalAnchor, int horizontalAnchor, int rowCount, + int columnCount, int anchorId, int windowStyleId, int penStyleId) { + this.defined = true; + this.visible = visible; + this.rowLock = rowLock; + this.priority = priority; + this.relativePositioning = relativePositioning; + this.verticalAnchor = verticalAnchor; + this.horizontalAnchor = horizontalAnchor; + this.anchorId = anchorId; + + // Decoders must add one to rowCount to get the desired number of rows. + if (this.rowCount != rowCount + 1) { + this.rowCount = rowCount + 1; + + // Trim any rolled up captions that are no longer valid, if applicable. + while ((rowLock && (rolledUpCaptions.size() >= this.rowCount)) + || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) { + rolledUpCaptions.remove(0); + } + } + + // TODO: Add support for column lock and count. + + if (windowStyleId != 0 && this.windowStyleId != windowStyleId) { + this.windowStyleId = windowStyleId; + // windowStyleId is 1-based. + int windowStyleIdIndex = windowStyleId - 1; + // Note that Border type and border color are the same for all window styles. + setWindowAttributes(WINDOW_STYLE_FILL[windowStyleIdIndex], COLOR_TRANSPARENT, + WINDOW_STYLE_WORD_WRAP[windowStyleIdIndex], BORDER_AND_EDGE_TYPE_NONE, + WINDOW_STYLE_PRINT_DIRECTION[windowStyleIdIndex], + WINDOW_STYLE_SCROLL_DIRECTION[windowStyleIdIndex], + WINDOW_STYLE_JUSTIFICATION[windowStyleIdIndex]); + } + + if (penStyleId != 0 && this.penStyleId != penStyleId) { + this.penStyleId = penStyleId; + // penStyleId is 1-based. + int penStyleIdIndex = penStyleId - 1; + // Note that pen size, offset, italics, underline, foreground color, and foreground + // opacity are the same for all pen styles. + setPenAttributes(0, PEN_OFFSET_NORMAL, PEN_SIZE_STANDARD, false, false, + PEN_STYLE_EDGE_TYPE[penStyleIdIndex], PEN_STYLE_FONT_STYLE[penStyleIdIndex]); + setPenColor(COLOR_SOLID_WHITE, PEN_STYLE_BACKGROUND[penStyleIdIndex], COLOR_SOLID_BLACK); + } + } + + + public void setWindowAttributes(int fillColor, int borderColor, boolean wordWrapToggle, + int borderType, int printDirection, int scrollDirection, int justification) { + this.windowFillColor = fillColor; + // TODO: Add support for border color and types. + // TODO: Add support for word wrap. + // TODO: Add support for other scroll directions. + // TODO: Add support for other print directions. + this.justification = justification; + + } + + public void setPenAttributes(int textTag, int offset, int penSize, boolean italicsToggle, + boolean underlineToggle, int edgeType, int fontStyle) { + // TODO: Add support for text tags. + // TODO: Add support for other offsets. + // TODO: Add support for other pen sizes. + + if (italicsStartPosition != C.POSITION_UNSET) { + if (!italicsToggle) { + captionStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition, + captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + italicsStartPosition = C.POSITION_UNSET; + } + } else if (italicsToggle) { + italicsStartPosition = captionStringBuilder.length(); + } + + if (underlineStartPosition != C.POSITION_UNSET) { + if (!underlineToggle) { + captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, + captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + underlineStartPosition = C.POSITION_UNSET; + } + } else if (underlineToggle) { + underlineStartPosition = captionStringBuilder.length(); + } + + // TODO: Add support for edge types. + // TODO: Add support for other font styles. + } + + public void setPenColor(int foregroundColor, int backgroundColor, int edgeColor) { + if (foregroundColorStartPosition != C.POSITION_UNSET) { + if (this.foregroundColor != foregroundColor) { + captionStringBuilder.setSpan(new ForegroundColorSpan(this.foregroundColor), + foregroundColorStartPosition, captionStringBuilder.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + if (foregroundColor != COLOR_SOLID_WHITE) { + foregroundColorStartPosition = captionStringBuilder.length(); + this.foregroundColor = foregroundColor; + } + + if (backgroundColorStartPosition != C.POSITION_UNSET) { + if (this.backgroundColor != backgroundColor) { + captionStringBuilder.setSpan(new BackgroundColorSpan(this.backgroundColor), + backgroundColorStartPosition, captionStringBuilder.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + if (backgroundColor != COLOR_SOLID_BLACK) { + backgroundColorStartPosition = captionStringBuilder.length(); + this.backgroundColor = backgroundColor; + } + + // TODO: Add support for edge color. + } + + public void setPenLocation(int row, int column) { + // TODO: Support moving the pen location with a window. + } + + public void backspace() { + int length = captionStringBuilder.length(); + if (length > 0) { + captionStringBuilder.delete(length - 1, length); + } + } + + public void append(char text) { + if (text == '\n') { + rolledUpCaptions.add(buildSpannableString()); + captionStringBuilder.clear(); + + if (italicsStartPosition != C.POSITION_UNSET) { + italicsStartPosition = 0; + } + if (underlineStartPosition != C.POSITION_UNSET) { + underlineStartPosition = 0; + } + if (foregroundColorStartPosition != C.POSITION_UNSET) { + foregroundColorStartPosition = 0; + } + if (backgroundColorStartPosition != C.POSITION_UNSET) { + backgroundColorStartPosition = 0; + } + + while ((rowLock && (rolledUpCaptions.size() >= rowCount)) + || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) { + rolledUpCaptions.remove(0); + } + } else { + captionStringBuilder.append(text); + } + } + + public SpannableString buildSpannableString() { + SpannableStringBuilder spannableStringBuilder = + new SpannableStringBuilder(captionStringBuilder); + int length = spannableStringBuilder.length(); + + if (length > 0) { + if (italicsStartPosition != C.POSITION_UNSET) { + spannableStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition, + length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (underlineStartPosition != C.POSITION_UNSET) { + spannableStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, + length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (foregroundColorStartPosition != C.POSITION_UNSET) { + spannableStringBuilder.setSpan(new ForegroundColorSpan(foregroundColor), + foregroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (backgroundColorStartPosition != C.POSITION_UNSET) { + spannableStringBuilder.setSpan(new BackgroundColorSpan(backgroundColor), + backgroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + return new SpannableString(spannableStringBuilder); + } + + public Cea708Cue build() { + if (isEmpty()) { + // The cue is empty. + return null; + } + + SpannableStringBuilder cueString = new SpannableStringBuilder(); + + // Add any rolled up captions, separated by new lines. + for (int i = 0; i < rolledUpCaptions.size(); i++) { + cueString.append(rolledUpCaptions.get(i)); + cueString.append('\n'); + } + // Add the current line. + cueString.append(buildSpannableString()); + + // TODO: Add support for right-to-left languages (i.e. where right would correspond to normal + // alignment). + Alignment alignment; + switch (justification) { + case JUSTIFICATION_FULL: + // TODO: Add support for full justification. + case JUSTIFICATION_LEFT: + alignment = Alignment.ALIGN_NORMAL; + break; + case JUSTIFICATION_RIGHT: + alignment = Alignment.ALIGN_OPPOSITE; + break; + case JUSTIFICATION_CENTER: + alignment = Alignment.ALIGN_CENTER; + break; + default: + throw new IllegalArgumentException("Unexpected justification value: " + justification); + } + + float position; + float line; + if (relativePositioning) { + position = (float) horizontalAnchor / RELATIVE_CUE_SIZE; + line = (float) verticalAnchor / RELATIVE_CUE_SIZE; + } else { + position = (float) horizontalAnchor / HORIZONTAL_SIZE; + line = (float) verticalAnchor / VERTICAL_SIZE; + } + // Apply screen-edge padding to the line and position. + position = (position * 0.9f) + 0.05f; + line = (line * 0.9f) + 0.05f; + + // anchorId specifies where the anchor should be placed on the caption cue/window. The 9 + // possible configurations are as follows: + // 0-----1-----2 + // | | + // 3 4 5 + // | | + // 6-----7-----8 + @AnchorType int verticalAnchorType; + if (anchorId % 3 == 0) { + verticalAnchorType = Cue.ANCHOR_TYPE_START; + } else if (anchorId % 3 == 1) { + verticalAnchorType = Cue.ANCHOR_TYPE_MIDDLE; + } else { + verticalAnchorType = Cue.ANCHOR_TYPE_END; + } + // TODO: Add support for right-to-left languages (i.e. where start is on the right). + @AnchorType int horizontalAnchorType; + if (anchorId / 3 == 0) { + horizontalAnchorType = Cue.ANCHOR_TYPE_START; + } else if (anchorId / 3 == 1) { + horizontalAnchorType = Cue.ANCHOR_TYPE_MIDDLE; + } else { + horizontalAnchorType = Cue.ANCHOR_TYPE_END; + } + + boolean windowColorSet = (windowFillColor != COLOR_SOLID_BLACK); + + return new Cea708Cue(cueString, alignment, line, Cue.LINE_TYPE_FRACTION, verticalAnchorType, + position, horizontalAnchorType, Cue.DIMEN_UNSET, windowColorSet, windowFillColor, + priority); + } + + public static int getArgbColorFromCeaColor(int red, int green, int blue) { + return getArgbColorFromCeaColor(red, green, blue, 0); + } + + public static int getArgbColorFromCeaColor(int red, int green, int blue, int opacity) { + Assertions.checkIndex(red, 0, 4); + Assertions.checkIndex(green, 0, 4); + Assertions.checkIndex(blue, 0, 4); + Assertions.checkIndex(opacity, 0, 4); + + int alpha; + switch (opacity) { + case 0: + case 1: + // Note the value of '1' is actually FLASH, but we don't support that. + alpha = 255; + break; + case 2: + alpha = 127; + break; + case 3: + alpha = 0; + break; + default: + alpha = 255; + } + + // TODO: Add support for the Alternative Minimum Color List or the full 64 RGB combinations. + + // Return values based on the Minimum Color List + return Color.argb(alpha, + (red > 1 ? 255 : 0), + (green > 1 ? 255 : 0), + (blue > 1 ? 255 : 0)); + } + + } + +} From 877c7f4e30ae23401d5856ff2e41a44ea6413b98 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 18 Jan 2017 02:51:14 -0800 Subject: [PATCH 097/142] Some misc file rearrangement. - Move .graffle files out of third_party - Add logo .ai file - Remove logo .svg files ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144812776 --- demo/assets/ic_launcher.svg | 660 ------------------------------------ 1 file changed, 660 deletions(-) delete mode 100644 demo/assets/ic_launcher.svg diff --git a/demo/assets/ic_launcher.svg b/demo/assets/ic_launcher.svg deleted file mode 100644 index 5486b27e29..0000000000 --- a/demo/assets/ic_launcher.svg +++ /dev/null @@ -1,660 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From bc4dc591f560f9c829eb1dd07a8ca62e8af452aa Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 18 Jan 2017 03:22:04 -0800 Subject: [PATCH 098/142] Fix some style nits in ID3 chapter support. Issue: #2316 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144815010 --- ...rameTest.java => ChapterTocFrameTest.java} | 12 ++-- .../exoplayer2/metadata/id3/ChapterFrame.java | 69 ++++++++++++------- ...pterTOCFrame.java => ChapterTocFrame.java} | 36 ++++++---- .../exoplayer2/metadata/id3/Id3Decoder.java | 19 +++-- .../exoplayer2/metadata/id3/UrlLinkFrame.java | 1 - 5 files changed, 88 insertions(+), 49 deletions(-) rename library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/{ChapterTOCFrameTest.java => ChapterTocFrameTest.java} (77%) rename library/src/main/java/com/google/android/exoplayer2/metadata/id3/{ChapterTOCFrame.java => ChapterTocFrame.java} (77%) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTOCFrameTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java similarity index 77% rename from library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTOCFrameTest.java rename to library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java index b0819ff427..9641de7669 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTOCFrameTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java @@ -19,9 +19,9 @@ import android.os.Parcel; import junit.framework.TestCase; /** - * Test for {@link ChapterTOCFrame}. + * Test for {@link ChapterTocFrame}. */ -public final class ChapterTOCFrameTest extends TestCase { +public final class ChapterTocFrameTest extends TestCase { public void testParcelable() { String[] children = new String[] {"child0", "child1"}; @@ -29,15 +29,15 @@ public final class ChapterTOCFrameTest extends TestCase { new TextInformationFrame("TIT2", null, "title"), new UrlLinkFrame("WXXX", "description", "url") }; - ChapterTOCFrame chapterTOCFrameToParcel = new ChapterTOCFrame("id", false, true, children, + ChapterTocFrame chapterTocFrameToParcel = new ChapterTocFrame("id", false, true, children, subFrames); Parcel parcel = Parcel.obtain(); - chapterTOCFrameToParcel.writeToParcel(parcel, 0); + chapterTocFrameToParcel.writeToParcel(parcel, 0); parcel.setDataPosition(0); - ChapterTOCFrame chapterTOCFrameFromParcel = ChapterTOCFrame.CREATOR.createFromParcel(parcel); - assertEquals(chapterTOCFrameToParcel, chapterTOCFrameFromParcel); + ChapterTocFrame chapterTocFrameFromParcel = ChapterTocFrame.CREATOR.createFromParcel(parcel); + assertEquals(chapterTocFrameToParcel, chapterTocFrameFromParcel); parcel.recycle(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java index 22fd0d5fe4..c82f982aa7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; @@ -27,18 +28,24 @@ public final class ChapterFrame extends Id3Frame { public static final String ID = "CHAP"; public final String chapterId; - public final int startTime; - public final int endTime; - public final int startOffset; - public final int endOffset; + public final int startTimeMs; + public final int endTimeMs; + /** + * The byte offset of the start of the chapter, or {@link C#POSITION_UNSET} if not set. + */ + public final long startOffset; + /** + * The byte offset of the end of the chapter, or {@link C#POSITION_UNSET} if not set. + */ + public final long endOffset; private final Id3Frame[] subFrames; - public ChapterFrame(String chapterId, int startTime, int endTime, int startOffset, int endOffset, - Id3Frame[] subFrames) { + public ChapterFrame(String chapterId, int startTimeMs, int endTimeMs, long startOffset, + long endOffset, Id3Frame[] subFrames) { super(ID); this.chapterId = chapterId; - this.startTime = startTime; - this.endTime = endTime; + this.startTimeMs = startTimeMs; + this.endTimeMs = endTimeMs; this.startOffset = startOffset; this.endOffset = endOffset; this.subFrames = subFrames; @@ -47,10 +54,10 @@ public final class ChapterFrame extends Id3Frame { /* package */ ChapterFrame(Parcel in) { super(ID); this.chapterId = in.readString(); - this.startTime = in.readInt(); - this.endTime = in.readInt(); - this.startOffset = in.readInt(); - this.endOffset = in.readInt(); + this.startTimeMs = in.readInt(); + this.endTimeMs = in.readInt(); + this.startOffset = in.readLong(); + this.endOffset = in.readLong(); int subFrameCount = in.readInt(); subFrames = new Id3Frame[subFrameCount]; for (int i = 0; i < subFrameCount; i++) { @@ -58,6 +65,20 @@ public final class ChapterFrame extends Id3Frame { } } + /** + * Returns the number of sub-frames. + */ + public int getSubFrameCount() { + return subFrames.length; + } + + /** + * Returns the sub-frame at {@code index}. + */ + public Id3Frame getSubFrame(int index) { + return subFrames[index]; + } + @Override public boolean equals(Object obj) { if (this == obj) { @@ -67,8 +88,8 @@ public final class ChapterFrame extends Id3Frame { return false; } ChapterFrame other = (ChapterFrame) obj; - return startTime == other.startTime - && endTime == other.endTime + return startTimeMs == other.startTimeMs + && endTimeMs == other.endTimeMs && startOffset == other.startOffset && endOffset == other.endOffset && Util.areEqual(chapterId, other.chapterId) @@ -78,10 +99,10 @@ public final class ChapterFrame extends Id3Frame { @Override public int hashCode() { int result = 17; - result = 31 * result + startTime; - result = 31 * result + endTime; - result = 31 * result + startOffset; - result = 31 * result + endOffset; + result = 31 * result + startTimeMs; + result = 31 * result + endTimeMs; + result = 31 * result + (int) startOffset; + result = 31 * result + (int) endOffset; result = 31 * result + (chapterId != null ? chapterId.hashCode() : 0); return result; } @@ -89,13 +110,13 @@ public final class ChapterFrame extends Id3Frame { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(chapterId); - dest.writeInt(startTime); - dest.writeInt(endTime); - dest.writeInt(startOffset); - dest.writeInt(endOffset); + dest.writeInt(startTimeMs); + dest.writeInt(endTimeMs); + dest.writeLong(startOffset); + dest.writeLong(endOffset); dest.writeInt(subFrames.length); - for (int i = 0; i < subFrames.length; i++) { - dest.writeParcelable(subFrames[i], 0); + for (Id3Frame subFrame : subFrames) { + dest.writeParcelable(subFrame, 0); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTOCFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java similarity index 77% rename from library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTOCFrame.java rename to library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java index 6dfcf9f104..d71d0863c7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTOCFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java @@ -16,15 +16,13 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; - import com.google.android.exoplayer2.util.Util; - import java.util.Arrays; /** * Chapter table of contents ID3 frame. */ -public final class ChapterTOCFrame extends Id3Frame { +public final class ChapterTocFrame extends Id3Frame { public static final String ID = "CTOC"; @@ -32,9 +30,9 @@ public final class ChapterTOCFrame extends Id3Frame { public final boolean isRoot; public final boolean isOrdered; public final String[] children; - public final Id3Frame[] subFrames; + private final Id3Frame[] subFrames; - public ChapterTOCFrame(String elementId, boolean isRoot, boolean isOrdered, String[] children, + public ChapterTocFrame(String elementId, boolean isRoot, boolean isOrdered, String[] children, Id3Frame[] subFrames) { super(ID); this.elementId = elementId; @@ -44,7 +42,7 @@ public final class ChapterTOCFrame extends Id3Frame { this.subFrames = subFrames; } - /* package */ ChapterTOCFrame(Parcel in) { + /* package */ ChapterTocFrame(Parcel in) { super(ID); this.elementId = in.readString(); this.isRoot = in.readByte() != 0; @@ -57,6 +55,20 @@ public final class ChapterTOCFrame extends Id3Frame { } } + /** + * Returns the number of sub-frames. + */ + public int getSubFrameCount() { + return subFrames.length; + } + + /** + * Returns the sub-frame at {@code index}. + */ + public Id3Frame getSubFrame(int index) { + return subFrames[index]; + } + @Override public boolean equals(Object obj) { if (this == obj) { @@ -65,7 +77,7 @@ public final class ChapterTOCFrame extends Id3Frame { if (obj == null || getClass() != obj.getClass()) { return false; } - ChapterTOCFrame other = (ChapterTOCFrame) obj; + ChapterTocFrame other = (ChapterTocFrame) obj; return isRoot == other.isRoot && isOrdered == other.isOrdered && Util.areEqual(elementId, other.elementId) @@ -94,16 +106,16 @@ public final class ChapterTOCFrame extends Id3Frame { } } - public static final Creator CREATOR = new Creator() { + public static final Creator CREATOR = new Creator() { @Override - public ChapterTOCFrame createFromParcel(Parcel in) { - return new ChapterTOCFrame(in); + public ChapterTocFrame createFromParcel(Parcel in) { + return new ChapterTocFrame(in); } @Override - public ChapterTOCFrame[] newArray(int size) { - return new ChapterTOCFrame[size]; + public ChapterTocFrame[] newArray(int size) { + return new ChapterTocFrame[size]; } }; diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 266aad6f70..9c3aa03271 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.metadata.id3; import android.util.Log; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; @@ -368,8 +369,8 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame(id, null, value); } - private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, - int frameSize) throws UnsupportedEncodingException { + private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { int encoding = id3Data.readUnsignedByte(); String charset = getCharsetName(encoding); @@ -523,8 +524,14 @@ public final class Id3Decoder implements MetadataDecoder { int startTime = id3Data.readInt(); int endTime = id3Data.readInt(); - int startOffset = id3Data.readInt(); - int endOffset = id3Data.readInt(); + long startOffset = id3Data.readUnsignedInt(); + if (startOffset == 0xFFFFFFFFL) { + startOffset = C.POSITION_UNSET; + } + long endOffset = id3Data.readUnsignedInt(); + if (endOffset == 0xFFFFFFFFL) { + endOffset = C.POSITION_UNSET; + } ArrayList subFrames = new ArrayList<>(); int limit = framePosition + frameSize; @@ -541,7 +548,7 @@ public final class Id3Decoder implements MetadataDecoder { return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, subFrameArray); } - private static ChapterTOCFrame decodeChapterTOCFrame(ParsableByteArray id3Data, int frameSize, + private static ChapterTocFrame decodeChapterTOCFrame(ParsableByteArray id3Data, int frameSize, int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize) throws UnsupportedEncodingException { int framePosition = id3Data.getPosition(); @@ -575,7 +582,7 @@ public final class Id3Decoder implements MetadataDecoder { Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()]; subFrames.toArray(subFrameArray); - return new ChapterTOCFrame(elementId, isRoot, isOrdered, children, subFrameArray); + return new ChapterTocFrame(elementId, isRoot, isOrdered, children, subFrameArray); } private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java index 7936d50b55..2148b921e1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.metadata.id3; import android.os.Parcel; import android.os.Parcelable; - import com.google.android.exoplayer2.util.Util; /** From 51f96374d46110438ec6ca2e0ba9bac4061299bd Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 18 Jan 2017 06:11:15 -0800 Subject: [PATCH 099/142] Make headers consisting across build.gradle files ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144826116 --- library/build.gradle | 4 ++-- testutils/build.gradle | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/library/build.gradle b/library/build.gradle index ae2eb2f3d9..0d4bbd0256 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -1,5 +1,3 @@ -import com.android.builder.core.BuilderConstants - // Copyright (C) 2016 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,6 +11,8 @@ import com.android.builder.core.BuilderConstants // 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. +import com.android.builder.core.BuilderConstants + apply plugin: 'com.android.library' apply plugin: 'bintray-release' diff --git a/testutils/build.gradle b/testutils/build.gradle index b935b30c69..83ff065f9a 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -1,3 +1,16 @@ +// 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. apply plugin: 'com.android.library' android { From 4b957cce47e0a7eab92f4f02635a31c5110984bd Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 18 Jan 2017 09:28:17 -0800 Subject: [PATCH 100/142] Fix streaming license renew error When the first streaming license request response provided to mediaDrm it might return an empty array instead of null. This was set to offlineLicenseKeySetId which made the work like there is a valid offline license. Simplified the code and made it to set offlineLicenseKeySetId only if there is sensible data in keySetId. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144843144 --- .../android/exoplayer2/drm/DefaultDrmSessionManager.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 6eb70428d5..9c959a38c5 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 @@ -564,9 +564,7 @@ public class DefaultDrmSessionManager implements DrmSe } } else { byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response); - if (offlineLicenseKeySetId != null && (keySetId == null || keySetId.length == 0)) { - // This means that the keySetId is unchanged. - } else { + if (keySetId != null && keySetId.length != 0) { offlineLicenseKeySetId = keySetId; } state = STATE_OPENED_WITH_KEYS; From d9be650b3be2a7d8c6c61af43f04fe2d91de60e3 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 19 Jan 2017 06:45:31 -0800 Subject: [PATCH 101/142] DASH: Fix propagation of language from manifest Issue: #2335 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144956177 --- .../dash/manifest/DashManifestParser.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) 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 688d68e893..8bbf8f6ccf 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 @@ -343,8 +343,7 @@ public class DashManifestParser extends DefaultHandler return C.TRACK_TYPE_VIDEO; } else if (MimeTypes.isAudio(sampleMimeType)) { return C.TRACK_TYPE_AUDIO; - } else if (mimeTypeIsRawText(sampleMimeType) - || MimeTypes.APPLICATION_RAWCC.equals(format.containerMimeType)) { + } else if (mimeTypeIsRawText(sampleMimeType)) { return C.TRACK_TYPE_TEXT; } return C.TRACK_TYPE_UNKNOWN; @@ -501,8 +500,7 @@ public class DashManifestParser extends DefaultHandler } else if (MimeTypes.isAudio(sampleMimeType)) { return Format.createAudioContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, audioChannels, audioSamplingRate, null, selectionFlags, language); - } else if (mimeTypeIsRawText(sampleMimeType) - || MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { + } else if (mimeTypeIsRawText(sampleMimeType)) { return Format.createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, selectionFlags, language, accessiblityChannel); } @@ -731,6 +729,14 @@ public class DashManifestParser extends DefaultHandler return MimeTypes.getAudioMediaMimeType(codecs); } else if (MimeTypes.isVideo(containerMimeType)) { return MimeTypes.getVideoMediaMimeType(codecs); + } else if (mimeTypeIsRawText(containerMimeType)) { + return containerMimeType; + } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) { + if ("stpp".equals(codecs)) { + return MimeTypes.APPLICATION_TTML; + } else if ("wvtt".equals(codecs)) { + return MimeTypes.APPLICATION_MP4VTT; + } } else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { if (codecs != null) { if (codecs.contains("cea708")) { @@ -740,14 +746,6 @@ public class DashManifestParser extends DefaultHandler } } return null; - } else if (mimeTypeIsRawText(containerMimeType)) { - return containerMimeType; - } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) { - if ("stpp".equals(codecs)) { - return MimeTypes.APPLICATION_TTML; - } else if ("wvtt".equals(codecs)) { - return MimeTypes.APPLICATION_MP4VTT; - } } return null; } @@ -759,7 +757,11 @@ public class DashManifestParser extends DefaultHandler * @return Whether the mimeType is a text sample mimeType. */ private static boolean mimeTypeIsRawText(String mimeType) { - return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType); + return MimeTypes.isText(mimeType) + || MimeTypes.APPLICATION_TTML.equals(mimeType) + || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) + || MimeTypes.APPLICATION_CEA708.equals(mimeType) + || MimeTypes.APPLICATION_CEA608.equals(mimeType); } /** From ae01c1a6fd01302941677d8233e2afde601389d2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 19 Jan 2017 08:59:42 -0800 Subject: [PATCH 102/142] Move inband event streams to Representation This is more consistent with our handling of DRM init data, and is more correct. It'll be up to whoever's using the manifest to look one layer deeper and figure out what event streams are defined on all representations, if they wish to do so. Issue #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144968183 --- .../drm/OfflineLicenseHelperTest.java | 11 ++-- .../source/dash/manifest/AdaptationSet.java | 11 +--- .../dash/manifest/DashManifestParser.java | 44 ++++----------- .../source/dash/manifest/Representation.java | 53 +++++++++++++++---- 4 files changed, 59 insertions(+), 60 deletions(-) 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 0342e37bd6..c7ebb22d9a 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 @@ -27,14 +27,12 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; 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.InbandEventStream; import com.google.android.exoplayer2.source.dash.manifest.Period; 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 java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import org.mockito.Mock; @@ -205,18 +203,17 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { private static DashManifest newDashManifestWithAllElements() { return newDashManifest(newPeriods(newAdaptationSets(newRepresentations(newDrmInitData())))); } - + private static DashManifest newDashManifest(Period... periods) { return new DashManifest(0, 0, 0, false, 0, 0, 0, null, null, Arrays.asList(periods)); } - + private static Period newPeriods(AdaptationSet... adaptationSets) { return new Period("", 0, Arrays.asList(adaptationSets)); } private static AdaptationSet newAdaptationSets(Representation... representations) { - return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), - Collections.emptyList()); + return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations)); } private static Representation newRepresentations(DrmInitData drmInitData) { @@ -225,7 +222,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { } private static DrmInitData newDrmInitData() { - return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", + return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", new byte[]{1, 4, 7, 0, 3, 6})); } 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 30649dcbe2..c4a4a4446b 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,26 +45,17 @@ public class AdaptationSet { */ public final List representations; - /** - * The {@link InbandEventStream}s contained by all {@link Representation}s in the adaptation set. - */ - public final List inbandEventStreams; - /** * @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 inbandEventStreams The {@link InbandEventStream}s contained by all - * {@link Representation}s in the adaptation set. */ - public AdaptationSet(int id, int type, List representations, - List inbandEventStreams) { + public AdaptationSet(int id, int type, List representations) { this.id = id; this.type = type; this.representations = Collections.unmodifiableList(representations); - this.inbandEventStreams = Collections.unmodifiableList(inbandEventStreams); } } 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 8bbf8f6ccf..a9dc0a8665 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 @@ -240,9 +240,8 @@ public class DashManifestParser extends DefaultHandler String language = xpp.getAttributeValue(null, "lang"); int accessibilityChannel = Format.NO_VALUE; ArrayList drmSchemeDatas = new ArrayList<>(); + ArrayList inbandEventStreams = new ArrayList<>(); List representationInfos = new ArrayList<>(); - List adaptationSetInbandEventStreams = new ArrayList<>(); - List commonRepresentationInbandEventStreams = null; @C.SelectionFlags int selectionFlags = 0; boolean seenFirstBaseUrl = false; @@ -274,22 +273,6 @@ public class DashManifestParser extends DefaultHandler contentType = checkContentTypeConsistency(contentType, getContentType(representationInfo.format)); representationInfos.add(representationInfo); - // Initialize or update InbandEventStream elements defined in all child Representations. - List inbandEventStreams = representationInfo.inbandEventStreams; - if (commonRepresentationInbandEventStreams == null) { - // Initialize with the elements defined in this representation. - commonRepresentationInbandEventStreams = new ArrayList<>(inbandEventStreams); - } else { - // Remove elements that are not also defined in this representation. - for (int i = commonRepresentationInbandEventStreams.size() - 1; i >= 0; i--) { - InbandEventStream inbandEventStream = commonRepresentationInbandEventStreams.get(i); - if (!inbandEventStreams.contains(inbandEventStream)) { - Log.w(TAG, "Ignoring InbandEventStream element not defined on all Representations: " - + inbandEventStream); - commonRepresentationInbandEventStreams.remove(i); - } - } - } } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { @@ -297,33 +280,25 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { - adaptationSetInbandEventStreams.add(parseInbandEventStream(xpp)); + inbandEventStreams.add(parseInbandEventStream(xpp)); } else if (XmlPullParserUtil.isStartTag(xpp)) { parseAdaptationSetChild(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "AdaptationSet")); - // Pull up InbandEventStream elements defined in all child Representations. - for (int i = 0; i < commonRepresentationInbandEventStreams.size(); i++) { - InbandEventStream inbandEventStream = commonRepresentationInbandEventStreams.get(i); - if (!adaptationSetInbandEventStreams.contains(inbandEventStream)) { - adaptationSetInbandEventStreams.add(inbandEventStream); - } - } - // Build the representations. List representations = new ArrayList<>(representationInfos.size()); for (int i = 0; i < representationInfos.size(); i++) { representations.add(buildRepresentation(representationInfos.get(i), contentId, - drmSchemeDatas)); + drmSchemeDatas, inbandEventStreams)); } - return buildAdaptationSet(id, contentType, representations, adaptationSetInbandEventStreams); + return buildAdaptationSet(id, contentType, representations); } protected AdaptationSet buildAdaptationSet(int id, int contentType, - List representations, List inbandEventStreams) { - return new AdaptationSet(id, contentType, representations, inbandEventStreams); + List representations) { + return new AdaptationSet(id, contentType, representations); } protected int parseContentType(XmlPullParser xpp) { @@ -510,15 +485,18 @@ public class DashManifestParser extends DefaultHandler } protected Representation buildRepresentation(RepresentationInfo representationInfo, - String contentId, ArrayList extraDrmSchemeDatas) { + String contentId, ArrayList extraDrmSchemeDatas, + 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; + inbandEventStremas.addAll(extraInbandEventStreams); return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format, - representationInfo.baseUrl, representationInfo.segmentBase); + representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStremas); } // SegmentBase, SegmentList and SegmentTemplate parsing. 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 f52727c1a8..cdf84f5f71 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 @@ -21,6 +21,8 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.dash.DashSegmentIndex; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.MultiSegmentBase; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; +import java.util.Collections; +import java.util.List; /** * A DASH representation. @@ -60,6 +62,10 @@ public abstract class Representation { * The offset of the presentation timestamps in the media stream relative to media time. */ public final long presentationTimeOffsetUs; + /** + * The {@link InbandEventStream}s in the representation. Never null, but may be empty. + */ + public final List inbandEventStreams; private final RangedUri initializationUri; @@ -78,6 +84,23 @@ public abstract class Representation { return newInstance(contentId, revisionId, format, baseUrl, segmentBase, null); } + /** + * Constructs a new instance. + * + * @param contentId Identifies the piece of content to which this representation belongs. + * @param revisionId Identifies the revision of the content. + * @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. + * @return The constructed instance. + */ + public static Representation newInstance(String contentId, long revisionId, Format format, + String baseUrl, SegmentBase segmentBase, List inbandEventStreams) { + return newInstance(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams, + null); + } + /** * Constructs a new instance. * @@ -86,18 +109,20 @@ 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 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, String customCacheKey) { + String baseUrl, SegmentBase segmentBase, List inbandEventStreams, + String customCacheKey) { if (segmentBase instanceof SingleSegmentBase) { return new SingleSegmentRepresentation(contentId, revisionId, format, baseUrl, - (SingleSegmentBase) segmentBase, customCacheKey, C.LENGTH_UNSET); + (SingleSegmentBase) segmentBase, inbandEventStreams, customCacheKey, C.LENGTH_UNSET); } else if (segmentBase instanceof MultiSegmentBase) { return new MultiSegmentRepresentation(contentId, revisionId, format, baseUrl, - (MultiSegmentBase) segmentBase); + (MultiSegmentBase) segmentBase, inbandEventStreams); } else { throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or " + "MultiSegmentBase"); @@ -105,11 +130,14 @@ public abstract class Representation { } private Representation(String contentId, long revisionId, Format format, String baseUrl, - SegmentBase segmentBase) { + SegmentBase segmentBase, List inbandEventStreams) { this.contentId = contentId; this.revisionId = revisionId; this.format = format; this.baseUrl = baseUrl; + this.inbandEventStreams = inbandEventStreams == null + ? Collections.emptyList() + : Collections.unmodifiableList(inbandEventStreams); initializationUri = segmentBase.getInitialization(this); presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs(); } @@ -167,18 +195,20 @@ 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 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, String customCacheKey, long contentLength) { + long indexStart, long indexEnd, List inbandEventStreams, + String customCacheKey, long contentLength) { RangedUri rangedUri = new RangedUri(null, initializationStart, initializationEnd - initializationStart + 1); SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, indexStart, indexEnd - indexStart + 1); return new SingleSegmentRepresentation(contentId, revisionId, - format, uri, segmentBase, customCacheKey, contentLength); + format, uri, segmentBase, inbandEventStreams, customCacheKey, contentLength); } /** @@ -187,12 +217,14 @@ 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 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, String customCacheKey, long contentLength) { - super(contentId, revisionId, format, baseUrl, segmentBase); + String baseUrl, SingleSegmentBase segmentBase, List inbandEventStreams, + String customCacheKey, long contentLength) { + super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.uri = Uri.parse(baseUrl); this.indexUri = segmentBase.getIndex(); this.cacheKey = customCacheKey != null ? customCacheKey @@ -235,10 +267,11 @@ 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. */ public MultiSegmentRepresentation(String contentId, long revisionId, Format format, - String baseUrl, MultiSegmentBase segmentBase) { - super(contentId, revisionId, format, baseUrl, segmentBase); + String baseUrl, MultiSegmentBase segmentBase, List inbandEventStreams) { + super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.segmentBase = segmentBase; } From 430d8e8a7a376545649e0de5f4aff4d2cc8c1d88 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 19 Jan 2017 09:14:50 -0800 Subject: [PATCH 103/142] Rename SingleTrackMetadataOutput ahead of real metadata support Issue #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144969838 --- .../source/chunk/ChunkExtractorWrapper.java | 22 +++++++++---------- .../source/chunk/ContainerMediaChunk.java | 6 ++--- .../source/chunk/InitializationChunk.java | 6 ++--- 3 files changed, 17 insertions(+), 17 deletions(-) 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 ed76a505ea..9e3e5fb8c0 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 @@ -30,15 +30,15 @@ import java.io.IOException; /** * An {@link Extractor} wrapper for loading chunks containing a single track. *

    - * The wrapper allows switching of the {@link SingleTrackMetadataOutput} and {@link TrackOutput} - * which receive parsed data. + * The wrapper allows switching of the {@link SeekMapOutput} and {@link TrackOutput} that receive + * parsed data. */ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput { /** - * Receives metadata associated with the track as extracted by the wrapped {@link Extractor}. + * Receives {@link SeekMap}s extracted by the wrapped {@link Extractor}. */ - public interface SingleTrackMetadataOutput { + public interface SeekMapOutput { /** * @see ExtractorOutput#seekMap(SeekMap) @@ -53,7 +53,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput private final boolean resendFormatOnInit; private boolean extractorInitialized; - private SingleTrackMetadataOutput metadataOutput; + private SeekMapOutput seekMapOutput; private TrackOutput trackOutput; private Format sentFormat; @@ -68,7 +68,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput * @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(SingleTrackMetadataOutput, TrackOutput)}. + * it is initialized via {@link #init(SeekMapOutput, TrackOutput)}. */ public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, boolean preferManifestDrmInitData, boolean resendFormatOnInit) { @@ -79,14 +79,14 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput } /** - * Initializes the extractor to output to the provided {@link SingleTrackMetadataOutput} and + * Initializes the extractor to output to the provided {@link SeekMapOutput} and * {@link TrackOutput} instances, and configures it to receive data from a new chunk. * - * @param metadataOutput The {@link SingleTrackMetadataOutput} that will receive metadata. + * @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(SingleTrackMetadataOutput metadataOutput, TrackOutput trackOutput) { - this.metadataOutput = metadataOutput; + public void init(SeekMapOutput seekMapOutput, TrackOutput trackOutput) { + this.seekMapOutput = seekMapOutput; this.trackOutput = trackOutput; if (!extractorInitialized) { extractor.init(this); @@ -130,7 +130,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput @Override public void seekMap(SeekMap seekMap) { - metadataOutput.seekMap(seekMap); + seekMapOutput.seekMap(seekMap); } // TrackOutput implementation. 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 410990b2c1..5f2b843510 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 @@ -21,7 +21,7 @@ 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.SingleTrackMetadataOutput; +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.Util; @@ -30,7 +30,7 @@ import java.io.IOException; /** * A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data. */ -public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMetadataOutput { +public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput { private final int chunkCount; private final long sampleOffsetUs; @@ -85,7 +85,7 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe return bytesLoaded; } - // SingleTrackMetadataOutput implementation. + // SeekMapOutput implementation. @Override public final void seekMap(SeekMap seekMap) { 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 dd62a2b49b..c7ac2d66a9 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 @@ -22,7 +22,7 @@ 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.SingleTrackMetadataOutput; +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.ParsableByteArray; @@ -32,7 +32,7 @@ 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 SingleTrackMetadataOutput, +public final class InitializationChunk extends Chunk implements SeekMapOutput, TrackOutput { private final ChunkExtractorWrapper extractorWrapper; @@ -85,7 +85,7 @@ public final class InitializationChunk extends Chunk implements SingleTrackMetad return seekMap; } - // SingleTrackMetadataOutput implementation. + // SeekMapOutput implementation. @Override public void seekMap(SeekMap seekMap) { From e3b3c8b69ca641ac4d8827ca8b65eb594692a158 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 19 Jan 2017 13:05:07 -0800 Subject: [PATCH 104/142] Display EMSG metadata events in EventLogger Issue #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144998826 --- .../java/com/google/android/exoplayer2/demo/EventLogger.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index d3e4b1ae3e..edc268ddb9 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.GeobFrame; @@ -384,6 +385,10 @@ import java.util.Locale; } else if (entry instanceof Id3Frame) { Id3Frame id3Frame = (Id3Frame) entry; Log.d(TAG, prefix + String.format("%s", id3Frame.id)); + } else if (entry instanceof EventMessage) { + EventMessage eventMessage = (EventMessage) entry; + Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s", + eventMessage.schemeIdUri, eventMessage.id, eventMessage.value)); } } } From 641597d7086a16f9ebcb7e69094c04990c1108bc Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 19 Jan 2017 13:09:05 -0800 Subject: [PATCH 105/142] Add a flag to enable EMSG output from FMP4 extractor Issue #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144999302 --- .../extractor/mp4/FragmentedMp4Extractor.java | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) 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 d1f47d981f..603aec4b22 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 @@ -20,6 +20,7 @@ import android.util.Log; import android.util.Pair; import android.util.SparseArray; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; @@ -74,7 +75,7 @@ public final class FragmentedMp4Extractor implements Extractor { */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, - FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_SIDELOADED}) + FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_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. @@ -88,11 +89,16 @@ public final class FragmentedMp4Extractor implements Extractor { * Flag to ignore any tfdt boxes in the stream. */ public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2; + /** + * Flag to indicate that the extractor should output an event message metadata track. Any event + * 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 {@link Track} was sideloaded, instead of being declared by the MP4 * container. */ - private static final int FLAG_SIDELOADED = 4; + private static final int FLAG_SIDELOADED = 8; 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}; @@ -143,7 +149,7 @@ public final class FragmentedMp4Extractor implements Extractor { // Extractor output. private ExtractorOutput extractorOutput; - private TrackOutput metadataTrackOutput; + private TrackOutput eventMessageTrackOutput; // Whether extractorOutput.seekMap has been called. private boolean haveOutputSeekMap; @@ -196,6 +202,7 @@ public final class FragmentedMp4Extractor implements Extractor { TrackBundle bundle = new TrackBundle(output.track(0)); bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); trackBundles.put(0, bundle); + maybeInitEventMessageTrack(); extractorOutput.endTracks(); } } @@ -406,6 +413,7 @@ public final class FragmentedMp4Extractor implements Extractor { trackBundles.put(track.id, new TrackBundle(extractorOutput.track(i))); durationUs = Math.max(durationUs, track.durationUs); } + maybeInitEventMessageTrack(); extractorOutput.endTracks(); } else { Assertions.checkState(trackBundles.size() == trackCount); @@ -429,12 +437,20 @@ public final class FragmentedMp4Extractor implements Extractor { } } + private void maybeInitEventMessageTrack() { + if ((flags & FLAG_ENABLE_EMSG_TRACK) == 0) { + return; + } + eventMessageTrackOutput = extractorOutput.track(trackBundles.size()); + eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, + Format.OFFSET_SAMPLE_RELATIVE)); + } + /** * Handles an emsg atom (defined in 23009-1). */ private void onEmsgLeafAtomRead(ParsableByteArray atom) { - // TODO: Enable metadata output. - if (metadataTrackOutput == null) { + if (eventMessageTrackOutput == null) { return; } // Parse the event's presentation time delta. @@ -447,11 +463,11 @@ public final class FragmentedMp4Extractor implements Extractor { // Output the sample data. atom.setPosition(Atom.FULL_HEADER_SIZE); int sampleSize = atom.bytesLeft(); - metadataTrackOutput.sampleData(atom, sampleSize); + eventMessageTrackOutput.sampleData(atom, sampleSize); // Output the sample metadata. if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { // We can output the sample metadata immediately. - metadataTrackOutput.sampleMetadata( + eventMessageTrackOutput.sampleMetadata( segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null); } else { @@ -673,7 +689,7 @@ public final class FragmentedMp4Extractor implements Extractor { DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues; int defaultSampleDescriptionIndex = ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0) - ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex; + ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex; int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration; int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0) @@ -1081,7 +1097,7 @@ public final class FragmentedMp4Extractor implements Extractor { while (!pendingMetadataSampleInfos.isEmpty()) { MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); pendingMetadataSampleBytes -= sampleInfo.size; - metadataTrackOutput.sampleMetadata( + eventMessageTrackOutput.sampleMetadata( sampleTimeUs + sampleInfo.presentationTimeDeltaUs, C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null); } From 9617986538dc9ec637efcdaaf14af5369ce3b2c6 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 19 Jan 2017 13:11:44 -0800 Subject: [PATCH 106/142] Remove redundant MetadataDecoder.canDecode method This is no longer needed as MetadataDecoderFactory figures out which decoder should be used. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144999613 --- .../android/exoplayer2/metadata/MetadataDecoder.java | 8 -------- .../exoplayer2/metadata/emsg/EventMessageDecoder.java | 6 ------ .../android/exoplayer2/metadata/id3/Id3Decoder.java | 6 ------ .../exoplayer2/metadata/scte35/SpliceInfoDecoder.java | 7 ------- 4 files changed, 27 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java index 5c04bdaa2a..9137bad4fd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoder.java @@ -20,14 +20,6 @@ package com.google.android.exoplayer2.metadata; */ public interface MetadataDecoder { - /** - * Checks whether the decoder supports a given mime type. - * - * @param mimeType A metadata mime type. - * @return Whether the mime type is supported. - */ - boolean canDecode(String mimeType); - /** * Decodes a {@link Metadata} element from the provided input buffer. * diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index b1cd5d2cf1..fd6996aa80 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.metadata.emsg; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.nio.ByteBuffer; import java.util.Arrays; @@ -31,11 +30,6 @@ import java.util.Arrays; */ public final class EventMessageDecoder implements MetadataDecoder { - @Override - public boolean canDecode(String mimeType) { - return MimeTypes.APPLICATION_EMSG.equals(mimeType); - } - @Override public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index 9c3aa03271..16059ccfbf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -20,7 +20,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.UnsupportedEncodingException; @@ -51,11 +50,6 @@ public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; - @Override - public boolean canDecode(String mimeType) { - return mimeType.equals(MimeTypes.APPLICATION_ID3); - } - @Override public Metadata decode(MetadataInputBuffer inputBuffer) { ByteBuffer buffer = inputBuffer.data; 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 dad8525d34..6e373a45e7 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 @@ -15,12 +15,10 @@ */ package com.google.android.exoplayer2.metadata.scte35; -import android.text.TextUtils; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.nio.ByteBuffer; @@ -44,11 +42,6 @@ public final class SpliceInfoDecoder implements MetadataDecoder { sectionHeader = new ParsableBitArray(); } - @Override - public boolean canDecode(String mimeType) { - return TextUtils.equals(mimeType, MimeTypes.APPLICATION_SCTE35); - } - @Override public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException { ByteBuffer buffer = inputBuffer.data; From 7abc34c6ae29e1a3f349563a2a00bd30ea3e5fa4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 19 Jan 2017 13:14:03 -0800 Subject: [PATCH 107/142] Respect decode-only flag in MetadataRenderer Issue #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144999973 --- .../google/android/exoplayer2/metadata/MetadataRenderer.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index 4869611aeb..550a13771f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -113,6 +113,10 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { inputStreamEnded = true; + } else if (buffer.isDecodeOnly()) { + // Do nothing. Note this assumes that all metadata buffers can be decoded independently. + // If we ever need to support a metadata format where this is not the case, we'll need to + // pass the buffer to the decoder and discard the output. } else { pendingMetadataTimestamp = buffer.timeUs; buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; From 6e18c97c209f6635813787f028a6335a04811671 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 20 Jan 2017 05:55:41 -0800 Subject: [PATCH 108/142] Pull assertion and layer of indirection out from ChunkExtractorWrapper It should be possible to remove ChunkExtractorWrapper from the track output side as well (currently all extractor output is funneled via ChunkExtractorWrapper just so it can adjust the format, which is kind of unnecessary). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145076891 --- .../source/chunk/ChunkExtractorWrapper.java | 17 ++--------------- .../source/chunk/ContainerMediaChunk.java | 5 ++++- .../source/chunk/InitializationChunk.java | 5 ++++- 3 files changed, 10 insertions(+), 17 deletions(-) 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 9e3e5fb8c0..2623d31cef 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 @@ -47,7 +47,8 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput } - private final Extractor extractor; + public final Extractor extractor; + private final Format manifestFormat; private final boolean preferManifestDrmInitData; private final boolean resendFormatOnInit; @@ -99,20 +100,6 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput } } - /** - * Reads from the provided {@link ExtractorInput}. - * - * @param input The {@link ExtractorInput} from which to read. - * @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}. - * @throws IOException If an error occurred reading from the source. - * @throws InterruptedException If the thread was interrupted. - */ - public int read(ExtractorInput input) throws IOException, InterruptedException { - int result = extractor.read(input, null); - Assertions.checkState(result != Extractor.RESULT_SEEK); - return result; - } - // ExtractorOutput implementation. @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 5f2b843510..060e6130cf 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 @@ -24,6 +24,7 @@ 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; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -120,10 +121,12 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput } // Load and decode the sample data. try { + Extractor extractor = extractorWrapper.extractor; int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractorWrapper.read(input); + result = extractor.read(input, null); } + Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } 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 c7ac2d66a9..c8c3389830 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 @@ -25,6 +25,7 @@ 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; @@ -142,10 +143,12 @@ public final class InitializationChunk extends Chunk implements SeekMapOutput, } // Load and decode the initialization data. try { + Extractor extractor = extractorWrapper.extractor; int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractorWrapper.read(input); + result = extractor.read(input, null); } + Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } From 5407c98526f6ce96c9725577cc56f8c827349717 Mon Sep 17 00:00:00 2001 From: tap-prod Date: Fri, 20 Jan 2017 06:10:32 -0800 Subject: [PATCH 109/142] Automated rollback *** Original change description *** Pull assertion and layer of indirection out from ChunkExtractorWrapper It should be possible to remove ChunkExtractorWrapper from the track output side as well (currently all extractor output is funneled via ChunkExtractorWrapper just so it can adjust the format, which is kind of unnecessary). *** ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145078094 --- .../source/chunk/ChunkExtractorWrapper.java | 17 +++++++++++++++-- .../source/chunk/ContainerMediaChunk.java | 5 +---- .../source/chunk/InitializationChunk.java | 5 +---- 3 files changed, 17 insertions(+), 10 deletions(-) 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..9e3e5fb8c0 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 @@ -47,8 +47,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput } - public final Extractor extractor; - + private final Extractor extractor; private final Format manifestFormat; private final boolean preferManifestDrmInitData; private final boolean resendFormatOnInit; @@ -100,6 +99,20 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput } } + /** + * Reads from the provided {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}. + * @throws IOException If an error occurred reading from the source. + * @throws InterruptedException If the thread was interrupted. + */ + public int read(ExtractorInput input) throws IOException, InterruptedException { + int result = extractor.read(input, null); + Assertions.checkState(result != Extractor.RESULT_SEEK); + return result; + } + // ExtractorOutput implementation. @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..5f2b843510 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 @@ -24,7 +24,6 @@ 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; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -121,12 +120,10 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput } // Load and decode the sample data. try { - Extractor extractor = extractorWrapper.extractor; int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractor.read(input, null); + result = extractorWrapper.read(input); } - Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } 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..c7ac2d66a9 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 @@ -25,7 +25,6 @@ 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; @@ -143,12 +142,10 @@ public final class InitializationChunk extends Chunk implements SeekMapOutput, } // Load and decode the initialization data. try { - Extractor extractor = extractorWrapper.extractor; int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractor.read(input, null); + result = extractorWrapper.read(input); } - Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } From 26b303a4496b89a96aeea7e758174b7c80aa0f9e Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 20 Jan 2017 07:31:51 -0800 Subject: [PATCH 110/142] Pull assertion and layer of indirection out from ChunkExtractorWrapper It should be possible to remove ChunkExtractorWrapper from the track output side as well (currently all extractor output is funneled via ChunkExtractorWrapper just so it can adjust the format, which is kind of unnecessary). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145083620 --- .../source/chunk/ChunkExtractorWrapper.java | 17 ++--------------- .../source/chunk/ContainerMediaChunk.java | 5 ++++- .../source/chunk/InitializationChunk.java | 5 ++++- 3 files changed, 10 insertions(+), 17 deletions(-) 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 9e3e5fb8c0..2623d31cef 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 @@ -47,7 +47,8 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput } - private final Extractor extractor; + public final Extractor extractor; + private final Format manifestFormat; private final boolean preferManifestDrmInitData; private final boolean resendFormatOnInit; @@ -99,20 +100,6 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput } } - /** - * Reads from the provided {@link ExtractorInput}. - * - * @param input The {@link ExtractorInput} from which to read. - * @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}. - * @throws IOException If an error occurred reading from the source. - * @throws InterruptedException If the thread was interrupted. - */ - public int read(ExtractorInput input) throws IOException, InterruptedException { - int result = extractor.read(input, null); - Assertions.checkState(result != Extractor.RESULT_SEEK); - return result; - } - // ExtractorOutput implementation. @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 5f2b843510..060e6130cf 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 @@ -24,6 +24,7 @@ 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; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -120,10 +121,12 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput } // Load and decode the sample data. try { + Extractor extractor = extractorWrapper.extractor; int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractorWrapper.read(input); + result = extractor.read(input, null); } + Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } 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 c7ac2d66a9..c8c3389830 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 @@ -25,6 +25,7 @@ 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; @@ -142,10 +143,12 @@ public final class InitializationChunk extends Chunk implements SeekMapOutput, } // Load and decode the initialization data. try { + Extractor extractor = extractorWrapper.extractor; int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractorWrapper.read(input); + result = extractor.read(input, null); } + Assertions.checkState(result != Extractor.RESULT_SEEK); } finally { bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); } From 63604493b41d232602eca3f658dacd4d6690ec9e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 20 Jan 2017 08:40:37 -0800 Subject: [PATCH 111/142] Fix memory leak in HlsMediaChunk's Issue:#2319 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145089668 --- .../exoplayer2/source/hls/HlsMediaChunk.java | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) 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 f9dba14e0e..0c411854d5 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 @@ -79,8 +79,10 @@ import java.util.concurrent.atomic.AtomicInteger; private final boolean isEncrypted; private final boolean isMasterTimestampSource; private final TimestampAdjuster timestampAdjuster; - private final HlsMediaChunk previousChunk; private final String lastPathSegment; + private final Extractor previousExtractor; + private final boolean shouldSpliceIn; + private final boolean needNewExtractor; private final boolean isPackedAudio; private final Id3Decoder id3Decoder; @@ -123,7 +125,6 @@ import java.util.concurrent.atomic.AtomicInteger; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; this.discontinuitySequenceNumber = discontinuitySequenceNumber; - this.previousChunk = previousChunk; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; lastPathSegment = dataSpec.uri.getLastPathSegment(); @@ -131,13 +132,19 @@ import java.util.concurrent.atomic.AtomicInteger; || lastPathSegment.endsWith(AC3_FILE_EXTENSION) || lastPathSegment.endsWith(EC3_FILE_EXTENSION) || lastPathSegment.endsWith(MP3_FILE_EXTENSION); - if (isPackedAudio) { - id3Decoder = previousChunk != null ? previousChunk.id3Decoder : new Id3Decoder(); - id3Data = previousChunk != null ? previousChunk.id3Data - : new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); + if (previousChunk != null) { + id3Decoder = previousChunk.id3Decoder; + id3Data = previousChunk.id3Data; + previousExtractor = previousChunk.extractor; + shouldSpliceIn = previousChunk.hlsUrl != hlsUrl; + needNewExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber + || shouldSpliceIn; } else { - id3Decoder = null; - id3Data = null; + id3Decoder = isPackedAudio ? new Id3Decoder() : null; + id3Data = isPackedAudio ? new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH) : null; + previousExtractor = null; + shouldSpliceIn = false; + needNewExtractor = true; } initDataSource = dataSource; uid = UID_SOURCE.getAndIncrement(); @@ -151,7 +158,7 @@ import java.util.concurrent.atomic.AtomicInteger; */ public void init(HlsSampleStreamWrapper output) { extractorOutput = output; - output.init(uid, previousChunk != null && previousChunk.hlsUrl != hlsUrl); + output.init(uid, shouldSpliceIn); } @Override @@ -191,8 +198,8 @@ import java.util.concurrent.atomic.AtomicInteger; // Internal loading methods. private void maybeLoadInitData() throws IOException, InterruptedException { - if ((previousChunk != null && previousChunk.extractor == extractor) || initLoadCompleted - || initDataSpec == null) { + if (previousExtractor == extractor || initLoadCompleted || initDataSpec == null) { + // According to spec, for packed audio, initDataSpec is expected to be null. return; } DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded); @@ -325,9 +332,6 @@ import java.util.concurrent.atomic.AtomicInteger; private Extractor buildExtractorByExtension() { // Set the extractor that will read the chunk. Extractor extractor; - boolean needNewExtractor = previousChunk == null - || previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber - || trackFormat != previousChunk.trackFormat; boolean usingNewExtractor = true; if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { @@ -335,7 +339,7 @@ import java.util.concurrent.atomic.AtomicInteger; } else if (!needNewExtractor) { // Only reuse TS and fMP4 extractors. usingNewExtractor = false; - extractor = previousChunk.extractor; + extractor = previousExtractor; } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) { extractor = new FragmentedMp4Extractor(0, timestampAdjuster); } else { From 52d47aa244f677bf32ba8c3b7c284f61e44b66db Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 20 Jan 2017 11:17:37 -0800 Subject: [PATCH 112/142] Fix possible track selection NPE If no tracks are selected at the start of playback, TrackSelectorResult isEquivalent(null) returned true, meaning we were keeping the old result (i.e. null), which we then tried to de-reference. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145105702 --- .../trackselection/TrackSelectorResult.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index 390b77391c..5cdb157570 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -61,10 +61,14 @@ public final class TrackSelectorResult { /** * Returns whether this result is equivalent to {@code other} for all renderers. * - * @param other The other {@link TrackSelectorResult}. May be null. + * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} + * will be returned in all cases. * @return Whether this result is equivalent to {@code other} for all renderers. */ public boolean isEquivalent(TrackSelectorResult other) { + if (other == null) { + return false; + } for (int i = 0; i < selections.length; i++) { if (!isEquivalent(other, i)) { return false; @@ -78,13 +82,14 @@ public final class TrackSelectorResult { * The results are equivalent if they have equal track selections and configurations for the * renderer. * - * @param other The other {@link TrackSelectorResult}. May be null. + * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} + * will be returned in all cases. * @param index The renderer index to check for equivalence. * @return Whether this result is equivalent to {@code other} for all renderers. */ public boolean isEquivalent(TrackSelectorResult other, int index) { if (other == null) { - return selections.get(index) == null && rendererConfigurations[index] == null; + return false; } return Util.areEqual(selections.get(index), other.selections.get(index)) && Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index]); From 55ca323cee1906e499e65acfdbb32a027f1e1376 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 20 Jan 2017 20:50:02 +0000 Subject: [PATCH 113/142] Add upstream.crypto package (and friends). --- .../upstream/cache/CacheDataSourceTest.java | 4 +- .../upstream/cache/CacheDataSourceTest2.java | 181 ++++++++++++++++ .../cache/CachedRegionTrackerTest.java | 126 +++++++++++ .../crypto/AesFlushingCipherTest.java | 186 ++++++++++++++++ .../upstream/cache/CachedRegionTracker.java | 205 ++++++++++++++++++ .../upstream/crypto/AesCipherDataSink.java | 95 ++++++++ .../upstream/crypto/AesCipherDataSource.java | 73 +++++++ .../upstream/crypto/AesFlushingCipher.java | 120 ++++++++++ .../upstream/crypto/CryptoUtil.java | 44 ++++ 9 files changed, 1033 insertions(+), 1 deletion(-) create mode 100644 library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java create mode 100644 library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java create mode 100644 library/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java 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 18e39be93c..c9eaa33204 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 @@ -27,7 +27,9 @@ import java.io.File; import java.io.IOException; import java.util.Arrays; -/** Unit tests for {@link CacheDataSource}. */ +/** + * Unit tests for {@link CacheDataSource}. + */ public class CacheDataSourceTest extends InstrumentationTestCase { private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java new file mode 100644 index 0000000000..70a7d797c1 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java @@ -0,0 +1,181 @@ +/* + * 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.upstream.cache; + +import android.content.Context; +import android.net.Uri; +import android.test.AndroidTestCase; +import android.test.MoreAsserts; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DataSink; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.FileDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; +import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSink; +import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSource; +import com.google.android.exoplayer2.util.Util; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Random; + +/** + * Additional tests for {@link CacheDataSource}. + */ +public class CacheDataSourceTest2 extends AndroidTestCase { + + private static final String EXO_CACHE_DIR = "exo"; + private static final int EXO_CACHE_MAX_FILESIZE = 128; + + private static final Uri URI = Uri.parse("http://test.com/content"); + private static final String KEY = "key"; + private static final byte[] DATA = TestUtil.buildTestData(8 * EXO_CACHE_MAX_FILESIZE + 1); + + // A DataSpec that covers the full file. + private static final DataSpec FULL = new DataSpec(URI, 0, DATA.length, KEY); + + private static final int OFFSET_ON_BOUNDARY = EXO_CACHE_MAX_FILESIZE; + // A DataSpec that starts at 0 and extends to a cache file boundary. + private static final DataSpec END_ON_BOUNDARY = new DataSpec(URI, 0, OFFSET_ON_BOUNDARY, KEY); + // A DataSpec that starts on the same boundary and extends to the end of the file. + private static final DataSpec START_ON_BOUNDARY = new DataSpec(URI, OFFSET_ON_BOUNDARY, + DATA.length - OFFSET_ON_BOUNDARY, KEY); + + private static final int OFFSET_OFF_BOUNDARY = EXO_CACHE_MAX_FILESIZE * 2 + 1; + // A DataSpec that starts at 0 and extends to just past a cache file boundary. + private static final DataSpec END_OFF_BOUNDARY = new DataSpec(URI, 0, OFFSET_OFF_BOUNDARY, KEY); + // A DataSpec that starts on the same boundary and extends to the end of the file. + private static final DataSpec START_OFF_BOUNDARY = new DataSpec(URI, OFFSET_OFF_BOUNDARY, + DATA.length - OFFSET_OFF_BOUNDARY, KEY); + + public void testWithoutEncryption() throws IOException { + testReads(false); + } + + public void testWithEncryption() throws IOException { + testReads(true); + } + + private void testReads(boolean useEncryption) throws IOException { + FakeDataSource upstreamSource = buildFakeUpstreamSource(); + CacheDataSource source = buildCacheDataSource(getContext(), upstreamSource, useEncryption); + // First read, should arrive from upstream. + testRead(END_ON_BOUNDARY, source); + assertSingleOpen(upstreamSource, 0, OFFSET_ON_BOUNDARY); + // Second read, should arrive from upstream. + testRead(START_OFF_BOUNDARY, source); + assertSingleOpen(upstreamSource, OFFSET_OFF_BOUNDARY, DATA.length); + // Second read, should arrive part from cache and part from upstream. + testRead(END_OFF_BOUNDARY, source); + assertSingleOpen(upstreamSource, OFFSET_ON_BOUNDARY, OFFSET_OFF_BOUNDARY); + // Third read, should arrive from cache. + testRead(FULL, source); + assertNoOpen(upstreamSource); + // Various reads, should all arrive from cache. + testRead(FULL, source); + assertNoOpen(upstreamSource); + testRead(START_ON_BOUNDARY, source); + assertNoOpen(upstreamSource); + testRead(END_ON_BOUNDARY, source); + assertNoOpen(upstreamSource); + testRead(START_OFF_BOUNDARY, source); + assertNoOpen(upstreamSource); + testRead(END_OFF_BOUNDARY, source); + assertNoOpen(upstreamSource); + } + + private void testRead(DataSpec dataSpec, CacheDataSource source) throws IOException { + byte[] scratch = new byte[4096]; + Random random = new Random(0); + source.open(dataSpec); + int position = (int) dataSpec.absoluteStreamPosition; + int bytesRead = 0; + while (bytesRead != C.RESULT_END_OF_INPUT) { + int maxBytesToRead = random.nextInt(scratch.length) + 1; + bytesRead = source.read(scratch, 0, maxBytesToRead); + if (bytesRead != C.RESULT_END_OF_INPUT) { + MoreAsserts.assertEquals(Arrays.copyOfRange(DATA, position, position + bytesRead), + Arrays.copyOf(scratch, bytesRead)); + position += bytesRead; + } + } + source.close(); + } + + /** + * Asserts that a single {@link DataSource#open(DataSpec)} call has been made to the upstream + * source, with the specified start (inclusive) and end (exclusive) positions. + */ + private void assertSingleOpen(FakeDataSource upstreamSource, int start, int end) { + DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs(); + assertEquals(1, openedDataSpecs.length); + assertEquals(start, openedDataSpecs[0].position); + assertEquals(start, openedDataSpecs[0].absoluteStreamPosition); + assertEquals(end - start, openedDataSpecs[0].length); + } + + /** + * Asserts that the upstream source was not opened. + */ + private void assertNoOpen(FakeDataSource upstreamSource) { + DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs(); + assertEquals(0, openedDataSpecs.length); + } + + private static FakeDataSource buildFakeUpstreamSource() { + return new FakeDataSource.Builder().appendReadData(DATA).build(); + } + + private static CacheDataSource buildCacheDataSource(Context context, DataSource upstreamSource, + boolean useAesEncryption) throws CacheException { + File cacheDir = context.getExternalCacheDir(); + Cache cache = new SimpleCache(new File(cacheDir, EXO_CACHE_DIR), new NoOpCacheEvictor()); + emptyCache(cache); + + // Source and cipher + final String secretKey = "testKey:12345678"; + DataSource file = new FileDataSource(); + DataSource cacheReadDataSource = useAesEncryption + ? new AesCipherDataSource(Util.getUtf8Bytes(secretKey), file) : file; + + // Sink and cipher + CacheDataSink cacheSink = new CacheDataSink(cache, EXO_CACHE_MAX_FILESIZE); + byte[] scratch = new byte[3897]; + DataSink cacheWriteDataSink = useAesEncryption + ? new AesCipherDataSink(Util.getUtf8Bytes(secretKey), cacheSink, scratch) : cacheSink; + + return new CacheDataSource(cache, + upstreamSource, + cacheReadDataSource, + cacheWriteDataSink, + CacheDataSource.FLAG_BLOCK_ON_CACHE, + null); // eventListener + } + + private static void emptyCache(Cache cache) throws CacheException { + for (String key : cache.getKeys()) { + for (CacheSpan span : cache.getCachedSpans(key)) { + cache.removeSpan(span); + } + } + // Sanity check that the cache really is empty now. + assertTrue(cache.getKeys().isEmpty()); + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java new file mode 100644 index 0000000000..799027f4b5 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -0,0 +1,126 @@ +/* + * 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.upstream.cache; + +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.extractor.ChunkIndex; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.File; +import java.io.IOException; +import org.mockito.Mock; + +/** + * Tests for {@link CachedRegionTracker}. + */ +public final class CachedRegionTrackerTest extends InstrumentationTestCase { + + private static final String CACHE_KEY = "abc"; + private static final long MS_IN_US = 1000; + + // 5 chunks, each 20 bytes long and 100 ms long. + private static final ChunkIndex CHUNK_INDEX = new ChunkIndex( + new int[] {20, 20, 20, 20, 20}, + new long[] {100, 120, 140, 160, 180}, + new long[] {100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US}, + new long[] {0, 100 * MS_IN_US, 200 * MS_IN_US, 300 * MS_IN_US, 400 * MS_IN_US}); + + @Mock private Cache cache; + private CachedRegionTracker tracker; + + private CachedContentIndex index; + private File cacheDir; + + @Override + protected void setUp() throws Exception { + TestUtil.setUpMockito(this); + + tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); + + cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); + index = new CachedContentIndex(cacheDir); + } + + @Override + protected void tearDown() throws Exception { + TestUtil.recursiveDelete(cacheDir); + } + + public void testGetRegion_noSpansInCache() { + assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(100)); + assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(150)); + } + + public void testGetRegion_fullyCached() throws Exception { + tracker.onSpanAdded( + cache, + newCacheSpan(100, 100)); + + assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(101)); + assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(121)); + } + + public void testGetRegion_partiallyCached() throws Exception { + tracker.onSpanAdded( + cache, + newCacheSpan(100, 40)); + + assertEquals(200, tracker.getRegionEndTimeMs(101)); + assertEquals(200, tracker.getRegionEndTimeMs(121)); + } + + public void testGetRegion_multipleSpanAddsJoinedCorrectly() throws Exception { + tracker.onSpanAdded( + cache, + newCacheSpan(100, 20)); + tracker.onSpanAdded( + cache, + newCacheSpan(120, 20)); + + assertEquals(200, tracker.getRegionEndTimeMs(101)); + assertEquals(200, tracker.getRegionEndTimeMs(121)); + } + + public void testGetRegion_fullyCachedThenPartiallyRemoved() throws Exception { + // Start with the full stream in cache. + tracker.onSpanAdded( + cache, + newCacheSpan(100, 100)); + + // Remove the middle bit. + tracker.onSpanRemoved( + cache, + newCacheSpan(140, 40)); + + assertEquals(200, tracker.getRegionEndTimeMs(101)); + assertEquals(200, tracker.getRegionEndTimeMs(121)); + + assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(181)); + } + + public void testGetRegion_subchunkEstimation() throws Exception { + tracker.onSpanAdded( + cache, + newCacheSpan(100, 10)); + + assertEquals(50, tracker.getRegionEndTimeMs(101)); + assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(111)); + } + + private CacheSpan newCacheSpan(int position, int length) throws IOException { + return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0); + } + +} diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java new file mode 100644 index 0000000000..b4e7e6e7f6 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java @@ -0,0 +1,186 @@ +/* + * 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.upstream.crypto; + +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.Util; +import java.util.Random; +import javax.crypto.Cipher; +import junit.framework.TestCase; + +/** + * Unit tests for {@link AesFlushingCipher}. + */ +public class AesFlushingCipherTest extends TestCase { + + private static final int DATA_LENGTH = 65536; + private static final byte[] KEY = Util.getUtf8Bytes("testKey:12345678"); + private static final long NONCE = 0; + private static final long START_OFFSET = 11; + private static final long RANDOM_SEED = 0x12345678; + + private AesFlushingCipher encryptCipher; + private AesFlushingCipher decryptCipher; + + @Override + protected void setUp() { + encryptCipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, KEY, NONCE, START_OFFSET); + decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, START_OFFSET); + } + + @Override + protected void tearDown() { + encryptCipher = null; + decryptCipher = null; + } + + private long getMaxUnchangedBytesAllowedPostEncryption(long length) { + // Assuming that not more than 10% of the resultant bytes should be identical. + // The value of 10% is arbitrary, ciphers standards do not name a value. + return length / 10; + } + + // Count the number of bytes that do not match. + private int getDifferingByteCount(byte[] data1, byte[] data2, int startOffset) { + int count = 0; + for (int i = startOffset; i < data1.length; i++) { + if (data1[i] != data2[i]) { + count++; + } + } + return count; + } + + // Count the number of bytes that do not match. + private int getDifferingByteCount(byte[] data1, byte[] data2) { + return getDifferingByteCount(data1, data2, 0); + } + + // Test a single encrypt and decrypt call + public void testSingle() { + byte[] reference = TestUtil.buildTestData(DATA_LENGTH); + byte[] data = reference.clone(); + + encryptCipher.updateInPlace(data, 0, data.length); + int unchangedByteCount = data.length - getDifferingByteCount(reference, data); + assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)); + + decryptCipher.updateInPlace(data, 0, data.length); + int differingByteCount = getDifferingByteCount(reference, data); + assertEquals(0, differingByteCount); + } + + // Test several encrypt and decrypt calls, each aligned on a 16 byte block size + public void testAligned() { + byte[] reference = TestUtil.buildTestData(DATA_LENGTH); + byte[] data = reference.clone(); + Random random = new Random(RANDOM_SEED); + + int offset = 0; + while (offset < data.length) { + int bytes = (1 + random.nextInt(50)) * 16; + bytes = Math.min(bytes, data.length - offset); + assertEquals(0, bytes % 16); + encryptCipher.updateInPlace(data, offset, bytes); + offset += bytes; + } + + int unchangedByteCount = data.length - getDifferingByteCount(reference, data); + assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)); + + offset = 0; + while (offset < data.length) { + int bytes = (1 + random.nextInt(50)) * 16; + bytes = Math.min(bytes, data.length - offset); + assertEquals(0, bytes % 16); + decryptCipher.updateInPlace(data, offset, bytes); + offset += bytes; + } + + int differingByteCount = getDifferingByteCount(reference, data); + assertEquals(0, differingByteCount); + } + + // Test several encrypt and decrypt calls, not aligned on block boundary + public void testUnAligned() { + byte[] reference = TestUtil.buildTestData(DATA_LENGTH); + byte[] data = reference.clone(); + Random random = new Random(RANDOM_SEED); + + // Encrypt + int offset = 0; + while (offset < data.length) { + int bytes = 1 + random.nextInt(4095); + bytes = Math.min(bytes, data.length - offset); + encryptCipher.updateInPlace(data, offset, bytes); + offset += bytes; + } + + int unchangedByteCount = data.length - getDifferingByteCount(reference, data); + assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)); + + offset = 0; + while (offset < data.length) { + int bytes = 1 + random.nextInt(4095); + bytes = Math.min(bytes, data.length - offset); + decryptCipher.updateInPlace(data, offset, bytes); + offset += bytes; + } + + int differingByteCount = getDifferingByteCount(reference, data); + assertEquals(0, differingByteCount); + } + + // Test decryption starting from the middle of an encrypted block + public void testMidJoin() { + byte[] reference = TestUtil.buildTestData(DATA_LENGTH); + byte[] data = reference.clone(); + Random random = new Random(RANDOM_SEED); + + // Encrypt + int offset = 0; + while (offset < data.length) { + int bytes = 1 + random.nextInt(4095); + bytes = Math.min(bytes, data.length - offset); + encryptCipher.updateInPlace(data, offset, bytes); + offset += bytes; + } + + // Verify + int unchangedByteCount = data.length - getDifferingByteCount(reference, data); + assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)); + + // Setup decryption from random location + offset = random.nextInt(4096); + decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, offset + START_OFFSET); + int remainingLength = data.length - offset; + int originalOffset = offset; + + // Decrypt + while (remainingLength > 0) { + int bytes = 1 + random.nextInt(4095); + bytes = Math.min(bytes, remainingLength); + decryptCipher.updateInPlace(data, offset, bytes); + offset += bytes; + remainingLength -= bytes; + } + + // Verify + int differingByteCount = getDifferingByteCount(reference, data, originalOffset); + assertEquals(0, differingByteCount); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java new file mode 100644 index 0000000000..0f08ca40f2 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java @@ -0,0 +1,205 @@ +/* + * 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.upstream.cache; + +import android.util.Log; +import com.google.android.exoplayer2.extractor.ChunkIndex; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NavigableSet; +import java.util.TreeSet; + +/** + * Utility class for efficiently tracking regions of data that are stored in a {@link Cache} + * for a given cache key. + */ +public final class CachedRegionTracker implements Cache.Listener { + + private static final String TAG = "CachedRegionTracker"; + + public static final int NOT_CACHED = -1; + public static final int CACHED_TO_END = -2; + + private final Cache cache; + private final String cacheKey; + private final ChunkIndex chunkIndex; + + private final TreeSet regions; + private final Region lookupRegion; + + public CachedRegionTracker(Cache cache, String cacheKey, ChunkIndex chunkIndex) { + this.cache = cache; + this.cacheKey = cacheKey; + this.chunkIndex = chunkIndex; + this.regions = new TreeSet<>(); + this.lookupRegion = new Region(0, 0); + + synchronized (this) { + NavigableSet cacheSpans = cache.addListener(cacheKey, this); + if (cacheSpans != null) { + // Merge the spans into regions. mergeSpan is more efficient when merging from high to low, + // which is why a descending iterator is used here. + Iterator spanIterator = cacheSpans.descendingIterator(); + while (spanIterator.hasNext()) { + CacheSpan span = spanIterator.next(); + mergeSpan(span); + } + } + } + } + + public void release() { + cache.removeListener(cacheKey, this); + } + + /** + * When provided with a byte offset, this method locates the cached region within which the + * offset falls, and returns the approximate end position in milliseconds of that region. If the + * byte offset does not fall within a cached region then {@link #NOT_CACHED} is returned. + * If the cached region extends to the end of the stream, {@link #CACHED_TO_END} is returned. + * + * @param byteOffset The byte offset in the underlying stream. + * @return The end position of the corresponding cache region, {@link #NOT_CACHED}, or + * {@link #CACHED_TO_END}. + */ + public synchronized int getRegionEndTimeMs(long byteOffset) { + lookupRegion.startOffset = byteOffset; + Region floorRegion = regions.floor(lookupRegion); + if (floorRegion == null || byteOffset > floorRegion.endOffset + || floorRegion.endOffsetIndex == -1) { + return NOT_CACHED; + } + int index = floorRegion.endOffsetIndex; + if (index == chunkIndex.length - 1 + && floorRegion.endOffset == (chunkIndex.offsets[index] + chunkIndex.sizes[index])) { + return CACHED_TO_END; + } + long segmentFractionUs = (chunkIndex.durationsUs[index] + * (floorRegion.endOffset - chunkIndex.offsets[index])) / chunkIndex.sizes[index]; + return (int) ((chunkIndex.timesUs[index] + segmentFractionUs) / 1000); + } + + @Override + public synchronized void onSpanAdded(Cache cache, CacheSpan span) { + mergeSpan(span); + } + + @Override + public synchronized void onSpanRemoved(Cache cache, CacheSpan span) { + Region removedRegion = new Region(span.position, span.position + span.length); + + // Look up a region this span falls into. + Region floorRegion = regions.floor(removedRegion); + if (floorRegion == null) { + Log.e(TAG, "Removed a span we were not aware of"); + return; + } + + // Remove it. + regions.remove(floorRegion); + + // Add new floor and ceiling regions, if necessary. + if (floorRegion.startOffset < removedRegion.startOffset) { + Region newFloorRegion = new Region(floorRegion.startOffset, removedRegion.startOffset); + + int index = Arrays.binarySearch(chunkIndex.offsets, newFloorRegion.endOffset); + newFloorRegion.endOffsetIndex = index < 0 ? -index - 2 : index; + regions.add(newFloorRegion); + } + + if (floorRegion.endOffset > removedRegion.endOffset) { + Region newCeilingRegion = new Region(removedRegion.endOffset + 1, floorRegion.endOffset); + newCeilingRegion.endOffsetIndex = floorRegion.endOffsetIndex; + regions.add(newCeilingRegion); + } + } + + @Override + public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) { + // Do nothing. + } + + private void mergeSpan(CacheSpan span) { + Region newRegion = new Region(span.position, span.position + span.length); + Region floorRegion = regions.floor(newRegion); + Region ceilingRegion = regions.ceiling(newRegion); + boolean floorConnects = regionsConnect(floorRegion, newRegion); + boolean ceilingConnects = regionsConnect(newRegion, ceilingRegion); + + if (ceilingConnects) { + if (floorConnects) { + // Extend floorRegion to cover both newRegion and ceilingRegion. + floorRegion.endOffset = ceilingRegion.endOffset; + floorRegion.endOffsetIndex = ceilingRegion.endOffsetIndex; + } else { + // Extend newRegion to cover ceilingRegion. Add it. + newRegion.endOffset = ceilingRegion.endOffset; + newRegion.endOffsetIndex = ceilingRegion.endOffsetIndex; + regions.add(newRegion); + } + regions.remove(ceilingRegion); + } else if (floorConnects) { + // Extend floorRegion to the right to cover newRegion. + floorRegion.endOffset = newRegion.endOffset; + int index = floorRegion.endOffsetIndex; + while (index < chunkIndex.length - 1 + && (chunkIndex.offsets[index + 1] <= floorRegion.endOffset)) { + index++; + } + floorRegion.endOffsetIndex = index; + } else { + // This is a new region. + int index = Arrays.binarySearch(chunkIndex.offsets, newRegion.endOffset); + newRegion.endOffsetIndex = index < 0 ? -index - 2 : index; + regions.add(newRegion); + } + } + + private boolean regionsConnect(Region lower, Region upper) { + return lower != null && upper != null && lower.endOffset == upper.startOffset; + } + + private static class Region implements Comparable { + + /** + * The first byte of the region (inclusive). + */ + public long startOffset; + /** + * End offset of the region (exclusive). + */ + public long endOffset; + /** + * The index in chunkIndex that contains the end offset. May be -1 if the end offset comes + * before the start of the first media chunk (i.e. if the end offset is within the stream + * header). + */ + public int endOffsetIndex; + + public Region(long position, long endOffset) { + this.startOffset = position; + this.endOffset = endOffset; + } + + @Override + public int compareTo(Region another) { + return startOffset < another.startOffset ? -1 + : startOffset == another.startOffset ? 0 : 1; + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java b/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java new file mode 100644 index 0000000000..ccf9a5b3f5 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java @@ -0,0 +1,95 @@ +/* + * 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.upstream.crypto; + +import com.google.android.exoplayer2.upstream.DataSink; +import com.google.android.exoplayer2.upstream.DataSpec; +import java.io.IOException; +import javax.crypto.Cipher; + +/** + * A wrapping {@link DataSink} that encrypts the data being consumed. + */ +public final class AesCipherDataSink implements DataSink { + + private final DataSink wrappedDataSink; + private final byte[] secretKey; + private final byte[] scratch; + + private AesFlushingCipher cipher; + + /** + * Create an instance whose {@code write} methods have the side effect of overwriting the input + * {@code data}. Use this constructor for maximum efficiency in the case that there is no + * requirement for the input data arrays to remain unchanged. + * + * @param secretKey The key data. + * @param wrappedDataSink The wrapped {@link DataSink}. + */ + public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink) { + this(secretKey, wrappedDataSink, null); + } + + /** + * Create an instance whose {@code write} methods are free of side effects. Use this constructor + * when the input data arrays are required to remain unchanged. + * + * @param secretKey The key data. + * @param wrappedDataSink The wrapped {@link DataSink}. + * @param scratch Scratch space. Data is decrypted into this array before being written to the + * wrapped {@link DataSink}. It should be of appropriate size for the expected writes. If a + * write is larger than the size of this array the write will still succeed, but multiple + * cipher calls will be required to complete the operation. + */ + public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, byte[] scratch) { + this.wrappedDataSink = wrappedDataSink; + this.secretKey = secretKey; + this.scratch = scratch; + } + + @Override + public void open(DataSpec dataSpec) throws IOException { + wrappedDataSink.open(dataSpec); + long nonce = CryptoUtil.getFNV64Hash(dataSpec.key); + cipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, secretKey, nonce, + dataSpec.absoluteStreamPosition); + } + + @Override + public void write(byte[] data, int offset, int length) throws IOException { + if (scratch == null) { + // In-place mode. Writes over the input data. + cipher.updateInPlace(data, offset, length); + wrappedDataSink.write(data, offset, length); + } else { + // Use scratch space. The original data remains intact. + int bytesProcessed = 0; + while (bytesProcessed < length) { + int bytesToProcess = Math.min(length - bytesProcessed, scratch.length); + cipher.update(data, offset + bytesProcessed, bytesToProcess, scratch, 0); + wrappedDataSink.write(scratch, 0, bytesToProcess); + bytesProcessed += bytesToProcess; + } + } + } + + @Override + public void close() throws IOException { + cipher = null; + wrappedDataSink.close(); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java new file mode 100644 index 0000000000..26ac3b38fa --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java @@ -0,0 +1,73 @@ +/* + * 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.upstream.crypto; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import java.io.IOException; +import javax.crypto.Cipher; + +/** + * A {@link DataSource} that decrypts the data read from an upstream source. + */ +public final class AesCipherDataSource implements DataSource { + + private final DataSource upstream; + private final byte[] secretKey; + + private AesFlushingCipher cipher; + + public AesCipherDataSource(byte[] secretKey, DataSource upstream) { + this.upstream = upstream; + this.secretKey = secretKey; + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + long dataLength = upstream.open(dataSpec); + long nonce = CryptoUtil.getFNV64Hash(dataSpec.key); + cipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, secretKey, nonce, + dataSpec.absoluteStreamPosition); + return dataLength; + } + + @Override + public int read(byte[] data, int offset, int readLength) throws IOException { + if (readLength == 0) { + return 0; + } + int read = upstream.read(data, offset, readLength); + if (read == C.RESULT_END_OF_INPUT) { + return C.RESULT_END_OF_INPUT; + } + cipher.updateInPlace(data, offset, read); + return read; + } + + @Override + public void close() throws IOException { + cipher = null; + upstream.close(); + } + + @Override + public Uri getUri() { + return upstream.getUri(); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java b/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java new file mode 100644 index 0000000000..e093eb3064 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java @@ -0,0 +1,120 @@ +/* + * 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.upstream.crypto; + +import com.google.android.exoplayer2.util.Assertions; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * A flushing variant of a AES/CTR/NoPadding {@link Cipher}. + * + * Unlike a regular {@link Cipher}, the update methods of this class are guaranteed to process all + * of the bytes input (and hence output the same number of bytes). + */ +public final class AesFlushingCipher { + + private final Cipher cipher; + private final int blockSize; + private final byte[] zerosBlock; + private final byte[] flushedBlock; + + private int pendingXorBytes; + + public AesFlushingCipher(int mode, byte[] secretKey, long nonce, long offset) { + try { + cipher = Cipher.getInstance("AES/CTR/NoPadding"); + blockSize = cipher.getBlockSize(); + zerosBlock = new byte[blockSize]; + flushedBlock = new byte[blockSize]; + long counter = offset / blockSize; + int startPadding = (int) (offset % blockSize); + cipher.init(mode, new SecretKeySpec(secretKey, cipher.getAlgorithm().split("/")[0]), + new IvParameterSpec(getInitializationVector(nonce, counter))); + if (startPadding != 0) { + updateInPlace(new byte[startPadding], 0, startPadding); + } + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException e) { + // Should never happen. + throw new RuntimeException(e); + } + } + + public void updateInPlace(byte[] data, int offset, int length) { + update(data, offset, length, data, offset); + } + + public void update(byte[] in, int inOffset, int length, byte[] out, int outOffset) { + // If we previously flushed the cipher by inputting zeros up to a block boundary, then we need + // to manually transform the data that actually ended the block. See the comment below for more + // details. + while (pendingXorBytes > 0) { + out[outOffset] = (byte) (in[inOffset] ^ flushedBlock[blockSize - pendingXorBytes]); + outOffset++; + inOffset++; + pendingXorBytes--; + length--; + if (length == 0) { + return; + } + } + + // Do the bulk of the update. + int written = nonFlushingUpdate(in, inOffset, length, out, outOffset); + if (length == written) { + return; + } + + // We need to finish the block to flush out the remaining bytes. We do so by inputting zeros, + // so that the corresponding bytes output by the cipher are those that would have been XORed + // against the real end-of-block data to transform it. We store these bytes so that we can + // perform the transformation manually in the case of a subsequent call to this method with + // the real data. + int bytesToFlush = length - written; + Assertions.checkState(bytesToFlush < blockSize); + outOffset += written; + pendingXorBytes = blockSize - bytesToFlush; + written = nonFlushingUpdate(zerosBlock, 0, pendingXorBytes, flushedBlock, 0); + Assertions.checkState(written == blockSize); + // The first part of xorBytes contains the flushed data, which we copy out. The remainder + // contains the bytes that will be needed for manual transformation in a subsequent call. + for (int i = 0; i < bytesToFlush; i++) { + out[outOffset++] = flushedBlock[i]; + } + } + + private int nonFlushingUpdate(byte[] in, int inOffset, int length, byte[] out, int outOffset) { + try { + return cipher.update(in, inOffset, length, out, outOffset); + } catch (ShortBufferException e) { + // Should never happen. + throw new RuntimeException(e); + } + } + + private byte[] getInitializationVector(long nonce, long counter) { + return ByteBuffer.allocate(16).putLong(nonce).putLong(counter).array(); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java b/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java new file mode 100644 index 0000000000..ff8841fa9c --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java @@ -0,0 +1,44 @@ +/* + * 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.upstream.crypto; + +/** + * Utility functions for the crypto package. + */ +/* package */ final class CryptoUtil { + + private CryptoUtil() {} + + /** + * Returns the hash value of the input as a long using the 64 bit FNV-1a hash function. The hash + * values produced by this function are less likely to collide than those produced by + * {@link #hashCode()}. + */ + public static long getFNV64Hash(String input) { + if (input == null) { + return 0; + } + + long hash = 0; + for (int i = 0; i < input.length(); i++) { + hash ^= input.charAt(i); + // This is equivalent to hash *= 0x100000001b3 (the FNV magic prime number). + hash += (hash << 1) + (hash << 4) + (hash << 5) + (hash << 7) + (hash << 8) + (hash << 40); + } + return hash; + } + +} From 24724158b1622955c3af2cfcefeb19248deced1e Mon Sep 17 00:00:00 2001 From: Devin Tuchsen Date: Sun, 22 Jan 2017 13:05:00 -0600 Subject: [PATCH 114/142] Use ParseableByteArray to get ALAC sample rate --- .../google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 92240a50c1..27f329fbbf 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,7 +89,9 @@ import java.util.List; if (!hasOutputFormat) { channelCount = ffmpegGetChannelCount(nativeContext); if ("alac".equals(codecName)) { - sampleRate = ByteBuffer.wrap(extraData, extraData.length - 4, 4).getInt(); + ParsableByteArray parsableExtraData = new ParsableByteArray(extraData); + parsableExtraData.setPosition(extraData.length - 4); + sampleRate = parsableExtraData.readUnsignedIntToInt(); } else { sampleRate = ffmpegGetSampleRate(nativeContext); } From d303db975ed770528e5b51a243c5023a54c8ec6e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 23 Jan 2017 03:13:35 -0800 Subject: [PATCH 115/142] Re-initialize the DemoApp player on BLWE This CL shows a de facto way to solve BLWEs until an in-player solution is implemented. Issue:#1782 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145265895 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index e61a9ed130..9add658d30 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -461,11 +461,12 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay playerNeedsSource = true; if (isBehindLiveWindow(e)) { clearResumePosition(); + initializePlayer(); } else { updateResumePosition(); + updateButtonVisibilities(); + showControls(); } - updateButtonVisibilities(); - showControls(); } @Override From 9ac0add4be89c91b46acce28e579a08249ce67d5 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 23 Jan 2017 03:38:33 -0800 Subject: [PATCH 116/142] Add a BUILD file with mobile harness target to playbacktests/src/androidTest folder ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145267137 --- .../src/androidTest/AndroidManifest.xml | 42 - .../playbacktests/gts/DashTest.java | 896 ------------------ 2 files changed, 938 deletions(-) delete mode 100644 playbacktests/src/androidTest/AndroidManifest.xml delete mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java diff --git a/playbacktests/src/androidTest/AndroidManifest.xml b/playbacktests/src/androidTest/AndroidManifest.xml deleted file mode 100644 index 2f7bbe6d7c..0000000000 --- a/playbacktests/src/androidTest/AndroidManifest.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - 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 deleted file mode 100644 index 6b561bc81c..0000000000 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ /dev/null @@ -1,896 +0,0 @@ -/* - * 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.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}. - */ -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) - .delay(10000).seek(30000).seek(31000).seek(32000).seek(33000).seek(34000) - .delay(1000).pause().delay(1000).play() - .delay(1000).pause().seek(120000).delay(1000).play() - .build(); - private static final ActionSchedule RENDERER_DISABLING_SCHEDULE = new ActionSchedule.Builder(TAG) - // Wait 10 seconds, disable the video renderer, wait another 10 seconds and enable it again. - .delay(10000).disableRenderer(VIDEO_RENDERER_INDEX) - .delay(10000).enableRenderer(VIDEO_RENDERER_INDEX) - // Ditto for the audio renderer. - .delay(10000).disableRenderer(AUDIO_RENDERER_INDEX) - .delay(10000).enableRenderer(AUDIO_RENDERER_INDEX) - // Wait 10 seconds, then disable and enable the video renderer 5 times in quick succession. - .delay(10000).disableRenderer(VIDEO_RENDERER_INDEX) - .enableRenderer(VIDEO_RENDERER_INDEX) - .disableRenderer(VIDEO_RENDERER_INDEX) - .enableRenderer(VIDEO_RENDERER_INDEX) - .disableRenderer(VIDEO_RENDERER_INDEX) - .enableRenderer(VIDEO_RENDERER_INDEX) - .disableRenderer(VIDEO_RENDERER_INDEX) - .enableRenderer(VIDEO_RENDERER_INDEX) - .disableRenderer(VIDEO_RENDERER_INDEX) - .enableRenderer(VIDEO_RENDERER_INDEX) - // Ditto for the audio renderer. - .delay(10000).disableRenderer(AUDIO_RENDERER_INDEX) - .enableRenderer(AUDIO_RENDERER_INDEX) - .disableRenderer(AUDIO_RENDERER_INDEX) - .enableRenderer(AUDIO_RENDERER_INDEX) - .disableRenderer(AUDIO_RENDERER_INDEX) - .enableRenderer(AUDIO_RENDERER_INDEX) - .disableRenderer(AUDIO_RENDERER_INDEX) - .enableRenderer(AUDIO_RENDERER_INDEX) - .disableRenderer(AUDIO_RENDERER_INDEX) - .enableRenderer(AUDIO_RENDERER_INDEX) - .delay(10000).seek(120000) - .build(); - - public DashTest() { - super(HostActivity.class); - } - - // H264 CDD. - - public void testH264Fixed() { - if (Util.SDK_INT < 16) { - // Pass. - return; - } - String streamName = "test_h264_fixed"; - testDashPlayback(getActivity(), streamName, H264_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, - MimeTypes.VIDEO_H264, false, H264_CDD_FIXED); - } - - public void testH264Adaptive() throws DecoderQueryException { - if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { - // Pass. - return; - } - String streamName = "test_h264_adaptive"; - testDashPlayback(getActivity(), streamName, H264_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, - MimeTypes.VIDEO_H264, ALLOW_ADDITIONAL_VIDEO_FORMATS, H264_CDD_ADAPTIVE); - } - - public void testH264AdaptiveWithSeeking() throws DecoderQueryException { - if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { - // Pass. - return; - } - String streamName = "test_h264_adaptive_with_seeking"; - testDashPlayback(getActivity(), streamName, SEEKING_SCHEDULE, false, H264_MANIFEST, - AAC_AUDIO_REPRESENTATION_ID, false, MimeTypes.VIDEO_H264, ALLOW_ADDITIONAL_VIDEO_FORMATS, - H264_CDD_ADAPTIVE); - } - - public void testH264AdaptiveWithRendererDisabling() throws DecoderQueryException { - if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { - // Pass. - return; - } - String streamName = "test_h264_adaptive_with_renderer_disabling"; - testDashPlayback(getActivity(), streamName, RENDERER_DISABLING_SCHEDULE, false, H264_MANIFEST, - AAC_AUDIO_REPRESENTATION_ID, false, MimeTypes.VIDEO_H264, ALLOW_ADDITIONAL_VIDEO_FORMATS, - H264_CDD_ADAPTIVE); - } - - // H265 CDD. - - public void testH265Fixed() { - if (Util.SDK_INT < 23) { - // Pass. - return; - } - String streamName = "test_h265_fixed"; - testDashPlayback(getActivity(), streamName, H265_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, - MimeTypes.VIDEO_H265, false, H265_CDD_FIXED); - } - - public void testH265Adaptive() throws DecoderQueryException { - if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { - // Pass. - return; - } - String streamName = "test_h265_adaptive"; - testDashPlayback(getActivity(), streamName, H265_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, - MimeTypes.VIDEO_H265, ALLOW_ADDITIONAL_VIDEO_FORMATS, H265_CDD_ADAPTIVE); - } - - public void testH265AdaptiveWithSeeking() throws DecoderQueryException { - if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { - // Pass. - return; - } - String streamName = "test_h265_adaptive_with_seeking"; - testDashPlayback(getActivity(), streamName, SEEKING_SCHEDULE, false, H265_MANIFEST, - AAC_AUDIO_REPRESENTATION_ID, false, MimeTypes.VIDEO_H265, ALLOW_ADDITIONAL_VIDEO_FORMATS, - H265_CDD_ADAPTIVE); - } - - public void testH265AdaptiveWithRendererDisabling() throws DecoderQueryException { - if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { - // Pass. - return; - } - String streamName = "test_h265_adaptive_with_renderer_disabling"; - testDashPlayback(getActivity(), streamName, RENDERER_DISABLING_SCHEDULE, false, - H265_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, MimeTypes.VIDEO_H265, - ALLOW_ADDITIONAL_VIDEO_FORMATS, H265_CDD_ADAPTIVE); - } - - // VP9 (CDD). - - public void testVp9Fixed360p() { - if (Util.SDK_INT < 23) { - // Pass. - return; - } - String streamName = "test_vp9_fixed_360p"; - testDashPlayback(getActivity(), streamName, VP9_MANIFEST, VORBIS_AUDIO_REPRESENTATION_ID, false, - MimeTypes.VIDEO_VP9, false, VP9_CDD_FIXED); - } - - public void testVp9Adaptive() throws DecoderQueryException { - if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { - // Pass. - return; - } - String streamName = "test_vp9_adaptive"; - testDashPlayback(getActivity(), streamName, VP9_MANIFEST, VORBIS_AUDIO_REPRESENTATION_ID, false, - MimeTypes.VIDEO_VP9, ALLOW_ADDITIONAL_VIDEO_FORMATS, VP9_CDD_ADAPTIVE); - } - - public void testVp9AdaptiveWithSeeking() throws DecoderQueryException { - if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { - // Pass. - return; - } - String streamName = "test_vp9_adaptive_with_seeking"; - testDashPlayback(getActivity(), streamName, SEEKING_SCHEDULE, false, VP9_MANIFEST, - VORBIS_AUDIO_REPRESENTATION_ID, false, MimeTypes.VIDEO_VP9, ALLOW_ADDITIONAL_VIDEO_FORMATS, - VP9_CDD_ADAPTIVE); - } - - public void testVp9AdaptiveWithRendererDisabling() throws DecoderQueryException { - if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { - // Pass. - return; - } - String streamName = "test_vp9_adaptive_with_renderer_disabling"; - testDashPlayback(getActivity(), streamName, RENDERER_DISABLING_SCHEDULE, false, - VP9_MANIFEST, VORBIS_AUDIO_REPRESENTATION_ID, false, MimeTypes.VIDEO_VP9, - ALLOW_ADDITIONAL_VIDEO_FORMATS, VP9_CDD_ADAPTIVE); - } - - // H264: Other frame-rates for output buffer count assertions. - - // 23.976 fps. - public void test23FpsH264Fixed() { - if (Util.SDK_INT < 23) { - // Pass. - return; - } - String streamName = "test_23fps_h264_fixed"; - testDashPlayback(getActivity(), streamName, H264_23_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, - false, MimeTypes.VIDEO_H264, false, H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID); - } - - // 24 fps. - public void test24FpsH264Fixed() { - if (Util.SDK_INT < 23) { - // Pass. - return; - } - String streamName = "test_24fps_h264_fixed"; - testDashPlayback(getActivity(), streamName, H264_24_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, - false, MimeTypes.VIDEO_H264, false, H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID); - } - - // 29.97 fps. - public void test29FpsH264Fixed() { - if (Util.SDK_INT < 23) { - // Pass. - return; - } - String streamName = "test_29fps_h264_fixed"; - testDashPlayback(getActivity(), streamName, H264_29_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, - false, MimeTypes.VIDEO_H264, false, H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID); - } - - // Widevine encrypted media tests. - // H264 CDD. - - public void testWidevineH264Fixed() throws DecoderQueryException { - if (Util.SDK_INT < 18) { - // Pass. - return; - } - String streamName = "test_widevine_h264_fixed"; - testDashPlayback(getActivity(), streamName, WIDEVINE_H264_MANIFEST_PREFIX, - WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H264, false, - WIDEVINE_H264_CDD_FIXED); - } - - public void testWidevineH264Adaptive() throws DecoderQueryException { - if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { - // Pass. - return; - } - String streamName = "test_widevine_h264_adaptive"; - testDashPlayback(getActivity(), streamName, WIDEVINE_H264_MANIFEST_PREFIX, - WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H264, - ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_H264_CDD_ADAPTIVE); - } - - public void testWidevineH264AdaptiveWithSeeking() throws DecoderQueryException { - if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { - // Pass. - return; - } - String streamName = "test_widevine_h264_adaptive_with_seeking"; - testDashPlayback(getActivity(), streamName, SEEKING_SCHEDULE, false, - WIDEVINE_H264_MANIFEST_PREFIX, WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, - MimeTypes.VIDEO_H264, ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_H264_CDD_ADAPTIVE); - } - - public void testWidevineH264AdaptiveWithRendererDisabling() throws DecoderQueryException { - if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { - // Pass. - return; - } - String streamName = "test_widevine_h264_adaptive_with_renderer_disabling"; - testDashPlayback(getActivity(), streamName, RENDERER_DISABLING_SCHEDULE, false, - WIDEVINE_H264_MANIFEST_PREFIX, WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, - MimeTypes.VIDEO_H264, ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_H264_CDD_ADAPTIVE); - } - - // H265 CDD. - - public void testWidevineH265Fixed() throws DecoderQueryException { - if (Util.SDK_INT < 23) { - // Pass. - return; - } - String streamName = "test_widevine_h265_fixed"; - testDashPlayback(getActivity(), streamName, WIDEVINE_H265_MANIFEST_PREFIX, - WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H265, false, - WIDEVINE_H265_CDD_FIXED); - } - - public void testWidevineH265Adaptive() throws DecoderQueryException { - if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { - // Pass. - return; - } - String streamName = "test_widevine_h265_adaptive"; - testDashPlayback(getActivity(), streamName, WIDEVINE_H265_MANIFEST_PREFIX, - WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H265, - ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_H265_CDD_ADAPTIVE); - } - - public void testWidevineH265AdaptiveWithSeeking() throws DecoderQueryException { - if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { - // Pass. - return; - } - String streamName = "test_widevine_h265_adaptive_with_seeking"; - testDashPlayback(getActivity(), streamName, SEEKING_SCHEDULE, false, - WIDEVINE_H265_MANIFEST_PREFIX, WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, - MimeTypes.VIDEO_H265, ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_H265_CDD_ADAPTIVE); - } - - public void testWidevineH265AdaptiveWithRendererDisabling() throws DecoderQueryException { - if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { - // Pass. - return; - } - String streamName = "test_widevine_h265_adaptive_with_renderer_disabling"; - testDashPlayback(getActivity(), streamName, RENDERER_DISABLING_SCHEDULE, false, - WIDEVINE_H265_MANIFEST_PREFIX, WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, - MimeTypes.VIDEO_H265, ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_H265_CDD_ADAPTIVE); - } - - // VP9 (CDD). - - public void testWidevineVp9Fixed360p() throws DecoderQueryException { - if (Util.SDK_INT < 23) { - // Pass. - return; - } - String streamName = "test_widevine_vp9_fixed_360p"; - testDashPlayback(getActivity(), streamName, WIDEVINE_VP9_MANIFEST_PREFIX, - WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_VP9, false, - WIDEVINE_VP9_CDD_FIXED); - } - - public void testWidevineVp9Adaptive() throws DecoderQueryException { - if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { - // Pass. - return; - } - String streamName = "test_widevine_vp9_adaptive"; - testDashPlayback(getActivity(), streamName, WIDEVINE_VP9_MANIFEST_PREFIX, - WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_VP9, - ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_VP9_CDD_ADAPTIVE); - } - - public void testWidevineVp9AdaptiveWithSeeking() throws DecoderQueryException { - if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { - // Pass. - return; - } - String streamName = "test_widevine_vp9_adaptive_with_seeking"; - testDashPlayback(getActivity(), streamName, SEEKING_SCHEDULE, false, - WIDEVINE_VP9_MANIFEST_PREFIX, WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID, true, - MimeTypes.VIDEO_VP9, ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_VP9_CDD_ADAPTIVE); - } - - public void testWidevineVp9AdaptiveWithRendererDisabling() throws DecoderQueryException { - if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { - // Pass. - return; - } - String streamName = "test_widevine_vp9_adaptive_with_renderer_disabling"; - testDashPlayback(getActivity(), streamName, RENDERER_DISABLING_SCHEDULE, false, - WIDEVINE_VP9_MANIFEST_PREFIX, WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID, true, - MimeTypes.VIDEO_VP9, ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_VP9_CDD_ADAPTIVE); - } - - // H264: Other frame-rates for output buffer count assertions. - - // 23.976 fps. - public void testWidevine23FpsH264Fixed() throws DecoderQueryException { - if (Util.SDK_INT < 23) { - // Pass. - return; - } - String streamName = "test_widevine_23fps_h264_fixed"; - testDashPlayback(getActivity(), streamName, WIDEVINE_H264_23_MANIFEST_PREFIX, - WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H264, false, - WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID); - } - - // 24 fps. - public void testWidevine24FpsH264Fixed() throws DecoderQueryException { - if (Util.SDK_INT < 23) { - // Pass. - return; - } - String streamName = "test_widevine_24fps_h264_fixed"; - testDashPlayback(getActivity(), streamName, WIDEVINE_H264_24_MANIFEST_PREFIX, - WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H264, false, - WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID); - } - - // 29.97 fps. - public void testWidevine29FpsH264Fixed() throws DecoderQueryException { - if (Util.SDK_INT < 23) { - // Pass. - return; - } - String streamName = "test_widevine_29fps_h264_fixed"; - testDashPlayback(getActivity(), streamName, WIDEVINE_H264_29_MANIFEST_PREFIX, - WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H264, false, - WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID); - } - - // Internal. - - private void testDashPlayback(HostActivity activity, String streamName, String manifestFileName, - String audioFormat, boolean isWidevineEncrypted, String videoMimeType, - boolean canIncludeAdditionalVideoFormats, String... videoFormats) { - testDashPlayback(activity, streamName, null, true, manifestFileName, audioFormat, - isWidevineEncrypted, videoMimeType, canIncludeAdditionalVideoFormats, videoFormats); - } - - private void testDashPlayback(HostActivity activity, String streamName, - ActionSchedule actionSchedule, boolean fullPlaybackNoSeeking, String manifestFileName, - String audioFormat, boolean isWidevineEncrypted, String videoMimeType, - boolean canIncludeAdditionalVideoFormats, String... videoFormats) { - MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(getInstrumentation(), TAG, - REPORT_NAME, REPORT_OBJECT_NAME); - String manifestPath = MANIFEST_URL_PREFIX + manifestFileName; - DashHostedTest test = new DashHostedTest(streamName, manifestPath, metricsLogger, - fullPlaybackNoSeeking, audioFormat, isWidevineEncrypted, videoMimeType, - canIncludeAdditionalVideoFormats, false, actionSchedule, videoFormats); - 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) { - metricsLogger = MetricsLogger.Factory.createDefault(getInstrumentation(), TAG, REPORT_NAME, - REPORT_OBJECT_NAME); - test = new DashHostedTest(streamName, manifestPath, metricsLogger, fullPlaybackNoSeeking, - audioFormat, isWidevineEncrypted, videoMimeType, false, true, actionSchedule, - videoFormats); - activity.runTest(test, TEST_TIMEOUT_MS); - } - } - - private static boolean shouldSkipAdaptiveTest(String mimeType) throws DecoderQueryException { - MediaCodecInfo decoderInfo = MediaCodecUtil.getDecoderInfo(mimeType, false); - assertNotNull(decoderInfo); - if (decoderInfo.adaptive) { - return false; - } - assertTrue(Util.SDK_INT < 21); - return true; - } - - @TargetApi(16) - private static class DashHostedTest extends ExoHostedTest { - - private final String streamName; - private final String videoMimeType; - private final String manifestPath; - private final MetricsLogger metricsLogger; - private final boolean fullPlaybackNoSeeking; - private final boolean isCddLimitedRetry; - private final boolean isWidevineEncrypted; - private final DashTestTrackSelector trackSelector; - - private boolean needsCddLimitedRetry; - private boolean needsSecureVideoDecoder; - - /** - * @param streamName The name of the test stream for metric logging. - * @param manifestPath The manifest path. - * @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 isWidevineEncrypted Whether the video is Widevine encrypted. - * @param videoMimeType The video mime type. - * @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 videoFormats The video formats. - */ - public DashHostedTest(String streamName, String manifestPath, MetricsLogger metricsLogger, - boolean fullPlaybackNoSeeking, String audioFormat, boolean isWidevineEncrypted, - String videoMimeType, boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry, - ActionSchedule actionSchedule, String... videoFormats) { - super(TAG, fullPlaybackNoSeeking); - Assertions.checkArgument(!(isCddLimitedRetry && canIncludeAdditionalVideoFormats)); - this.streamName = streamName; - this.manifestPath = manifestPath; - this.metricsLogger = metricsLogger; - this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; - this.isWidevineEncrypted = isWidevineEncrypted; - this.videoMimeType = videoMimeType; - this.isCddLimitedRetry = isCddLimitedRetry; - trackSelector = new DashTestTrackSelector(audioFormat, videoFormats, - canIncludeAdditionalVideoFormats); - if (actionSchedule != null) { - setSchedule(actionSchedule); - } - } - - @Override - protected MappingTrackSelector buildTrackSelector(HostActivity host, - BandwidthMeter bandwidthMeter) { - return trackSelector; - } - - @Override - @TargetApi(18) - @SuppressWarnings("ResourceType") - protected final DefaultDrmSessionManager 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; - } - - } - -} From 5debf5a14a95016f955f47658d4bc6390a6c2381 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 23 Jan 2017 04:19:28 -0800 Subject: [PATCH 117/142] Use bitrate as fixed track selection tie breaker If we don't have resolutions (and therefore cannot determine pixel counts) then use bitrate as a tie breaker instead. Also use pixel count as a tie breaker if pixel counts are known but equal. Streams with known pixel counts will always be preferred over streams without. Issue: #2343 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145269968 --- .../trackselection/DefaultTrackSelector.java | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) 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, From 18d7cdf39f4d13270a23d06a4a51b24b6ec05fdb Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 23 Jan 2017 07:01:26 -0800 Subject: [PATCH 118/142] Add pts adjustment in SpliceInfoDecoder This allows the user to interpret PTSs in the playback timebase. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145280921 --- .../scte35/SpliceInfoDecoderTest.java | 173 ++++++++++++++++++ .../extractor/TimestampAdjuster.java | 6 + .../metadata/scte35/PrivateCommand.java | 1 - .../metadata/scte35/SpliceInfoDecoder.java | 15 +- .../metadata/scte35/SpliceInsertCommand.java | 28 ++- .../metadata/scte35/TimeSignalCommand.java | 14 +- 6 files changed, 222 insertions(+), 15 deletions(-) create mode 100644 library/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java 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..4c493fd8ad --- /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.extractor.TimestampAdjuster; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataDecoderException; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +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/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java b/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java index a4da5d8e66..1fc0e1813e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/TimestampAdjuster.java @@ -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/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..dc85788a8b 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.scte35; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoderException; @@ -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..07a84bf5d1 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.metadata.scte35; import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Collections; @@ -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..e21eafbeeb 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.metadata.scte35; import android.os.Parcel; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.util.ParsableByteArray; /** @@ -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 From 497651c7b9b70ea4d84568506b88a9c682845fe6 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 23 Jan 2017 08:30:39 -0800 Subject: [PATCH 119/142] Ignore file extension for HLS Subtitle Renditions According to the spec, subtitle renditions must be Webvtt media segments. Issue:#2025 Issue:#2355 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145289266 --- .../android/exoplayer2/source/hls/HlsMediaChunk.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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..7ef6b7ace0 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 @@ -187,7 +187,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,11 +329,12 @@ 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) { From b1ec5e3a2505140f3189d130993398cdc95ea557 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 23 Jan 2017 09:30:54 -0800 Subject: [PATCH 120/142] Move TimestampAdjuster from extractor to util ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145295850 --- .../android/exoplayer2/extractor/ts/SectionReaderTest.java | 2 +- .../android/exoplayer2/extractor/ts/TsExtractorTest.java | 2 +- .../exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java | 2 +- .../exoplayer2/extractor/mp4/FragmentedMp4Extractor.java | 2 +- .../com/google/android/exoplayer2/extractor/ts/PesReader.java | 3 +-- .../google/android/exoplayer2/extractor/ts/PsExtractor.java | 2 +- .../android/exoplayer2/extractor/ts/SectionPayloadReader.java | 2 +- .../google/android/exoplayer2/extractor/ts/SectionReader.java | 2 +- .../exoplayer2/extractor/ts/SpliceInfoSectionReader.java | 2 +- .../google/android/exoplayer2/extractor/ts/TsExtractor.java | 2 +- .../android/exoplayer2/extractor/ts/TsPayloadReader.java | 2 +- .../android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java | 2 +- .../exoplayer2/metadata/scte35/SpliceInsertCommand.java | 2 +- .../android/exoplayer2/metadata/scte35/TimeSignalCommand.java | 2 +- .../google/android/exoplayer2/source/hls/HlsChunkSource.java | 2 +- .../google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 +- .../exoplayer2/source/hls/TimestampAdjusterProvider.java | 2 +- .../google/android/exoplayer2/source/hls/WebvttExtractor.java | 2 +- .../exoplayer2/{extractor => util}/TimestampAdjuster.java | 2 +- 19 files changed, 19 insertions(+), 20 deletions(-) rename library/src/main/java/com/google/android/exoplayer2/{extractor => util}/TimestampAdjuster.java (99%) 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..2dce742158 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 @@ -21,7 +21,6 @@ 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 +29,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; 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 index 4c493fd8ad..c50ff06699 100644 --- 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 @@ -16,10 +16,10 @@ package com.google.android.exoplayer2.metadata.scte35; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; 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; 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..45cb788a2b 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,7 +31,6 @@ 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; @@ -39,6 +38,7 @@ 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; 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/SpliceInfoSectionReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java index 121a622362..057fa636ce 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. 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..61d66afbc2 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,13 +25,13 @@ 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.Arrays; 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..5785c50a7b 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. 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 dc85788a8b..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 @@ -15,13 +15,13 @@ */ package com.google.android.exoplayer2.metadata.scte35; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; 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; /** 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 07a84bf5d1..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 @@ -18,8 +18,8 @@ package com.google.android.exoplayer2.metadata.scte35; import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; 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; 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 e21eafbeeb..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 @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.metadata.scte35; import android.os.Parcel; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; 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. 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..c2a345ace6 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; 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 7ef6b7ace0..924d3d3ece 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; 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..c8928ce65d 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; 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 99% 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 1fc0e1813e..19c500202b 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; From 4efdd14c659c151d5fddadb0280c26f1e69c2800 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 24 Jan 2017 04:24:29 -0800 Subject: [PATCH 121/142] Allow FMP4 extractor to output CEA-608 Issue: #2362 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145401668 --- .../extractor/mp4/FragmentedMp4Extractor.java | 60 +++++++--- .../exoplayer2/extractor/ts/SeiReader.java | 38 +------ .../exoplayer2/text/cea/Cea608Decoder.java | 31 ----- .../android/exoplayer2/text/cea/CeaUtil.java | 106 ++++++++++++++++++ 4 files changed, 155 insertions(+), 80 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java 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 45cb788a2b..7d687cc709 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 @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.extractor.SeekMap; 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; @@ -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,20 @@ 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 int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information 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}; @@ -121,6 +128,7 @@ public final class FragmentedMp4Extractor implements Extractor { // Temporary arrays. private final ParsableByteArray nalStartCode; private final ParsableByteArray nalLength; + private final ParsableByteArray nalPayload; private final ParsableByteArray encryptionSignalByte; // Adjusts sample timestamps. @@ -150,6 +158,7 @@ public final class FragmentedMp4Extractor implements Extractor { // Extractor output. private ExtractorOutput extractorOutput; private TrackOutput eventMessageTrackOutput; + private TrackOutput cea608TrackOutput; // Whether extractorOutput.seekMap has been called. private boolean haveOutputSeekMap; @@ -180,6 +189,7 @@ public final class FragmentedMp4Extractor implements Extractor { atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); + nalPayload = new ParsableByteArray(1); encryptionSignalByte = new ParsableByteArray(1); extendedTypeScratch = new byte[16]; containerAtoms = new Stack<>(); @@ -202,7 +212,7 @@ public final class FragmentedMp4Extractor implements Extractor { TrackBundle bundle = new TrackBundle(output.track(0)); bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); trackBundles.put(0, bundle); - maybeInitEventMessageTrack(); + maybeInitExtraTracks(); extractorOutput.endTracks(); } } @@ -413,7 +423,7 @@ public final class FragmentedMp4Extractor implements Extractor { trackBundles.put(track.id, new TrackBundle(extractorOutput.track(i))); durationUs = Math.max(durationUs, track.durationUs); } - maybeInitEventMessageTrack(); + maybeInitExtraTracks(); extractorOutput.endTracks(); } else { Assertions.checkState(trackBundles.size() == trackCount); @@ -437,13 +447,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()); + 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()); + 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)); } /** @@ -1065,6 +1079,26 @@ public final class FragmentedMp4Extractor implements Extractor { output.sampleData(nalStartCode, 4); sampleBytesWritten += 4; sampleSize += nalUnitLengthFieldLengthDiff; + if (cea608TrackOutput != null) { + byte[] nalPayloadData = nalPayload.data; + // Peek the NAL unit type byte. + input.peekFully(nalPayloadData, 0, 1); + if ((nalPayloadData[0] & 0x1F) == NAL_UNIT_TYPE_SEI) { + // Read the whole SEI NAL unit into nalWrapper, including the NAL unit type byte. + nalPayload.reset(sampleCurrentNalBytesRemaining); + input.readFully(nalPayloadData, 0, sampleCurrentNalBytesRemaining); + // Write the SEI unit straight to the output. + output.sampleData(nalPayload, sampleCurrentNalBytesRemaining); + sampleBytesWritten += sampleCurrentNalBytesRemaining; + sampleCurrentNalBytesRemaining = 0; + // Unescape and process the SEI unit. + int unescapedLength = NalUnitUtil.unescapeStream(nalPayloadData, nalPayload.limit()); + nalPayload.setPosition(1); // Skip the NAL unit type byte. + nalPayload.setLimit(unescapedLength); + CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalPayload, + cea608TrackOutput); + } + } } else { // Write the payload of the NAL unit. int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); 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..6e2e42d8e2 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 @@ -15,10 +15,9 @@ */ 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.TrackOutput; -import com.google.android.exoplayer2.text.cea.Cea608Decoder; +import com.google.android.exoplayer2.text.cea.CeaUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -36,40 +35,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; } 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/text/cea/Cea608Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 3ae8ded9ba..7324c94288 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; @@ -573,31 +567,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; 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..3053debfcf --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java @@ -0,0 +1,106 @@ +/* + * 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 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 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) { + 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 (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); + } + } + } + + /** + * 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() {} + +} From c01c2c34f731cf96baf1022e6cdbb68a9f0aca40 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 24 Jan 2017 04:41:43 -0800 Subject: [PATCH 122/142] Store full accessibility descriptors in parsed DASH manifest Issue: #2362 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145402640 --- .../drm/OfflineLicenseHelperTest.java | 2 +- .../dash/manifest/DashManifestParserTest.java | 71 ++++++--- .../source/dash/manifest/AdaptationSet.java | 12 +- .../dash/manifest/DashManifestParser.java | 149 ++++++++++-------- .../source/dash/manifest/Representation.java | 28 ++-- ...dEventStream.java => SchemeValuePair.java} | 8 +- 6 files changed, 164 insertions(+), 106 deletions(-) rename library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/{InbandEventStream.java => SchemeValuePair.java} (87%) 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..9eed8dfd3a 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 @@ -213,7 +213,7 @@ 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) { 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/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..1917399282 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) { @@ -367,16 +367,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 +423,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 +437,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 +466,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 +475,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 +486,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 +504,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 +803,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 +920,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..4146037e1c 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; } 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); } From 0e992370752e8570c330f70d83d5dd34a09e4228 Mon Sep 17 00:00:00 2001 From: zhihuichen Date: Tue, 24 Jan 2017 13:14:46 -0800 Subject: [PATCH 123/142] Allow duplicate tracks in WebM/MKV extractor ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145457836 --- .../android/exoplayer2/extractor/mkv/MatroskaExtractor.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 ccf78e6bc6..970335e9d2 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 @@ -529,11 +529,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; From 8970e80b25ad0e91c44d7089d46185829da896b6 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 25 Jan 2017 04:39:12 -0800 Subject: [PATCH 124/142] Don't use the returned key set id if the request wasn't for an offline license key ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145533961 --- .../android/exoplayer2/drm/DefaultDrmSessionManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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..1cd8d8464d 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 @@ -530,9 +530,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 +563,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; From 953c6855ec8cae75d962fadd56ea7812d4c80754 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 25 Jan 2017 05:54:30 -0800 Subject: [PATCH 125/142] FMP4 EMSG/CEA608 output bug fix + tweaks - Fix to use different track ids for EMSG + CEA608, so they can both be enabled at once. - Tweaked extractor to output formats prior to endTracks() when parsing the initial moov box. This makes it easier to handle multiple tracks through the chunk package. It may or may not be made a requirement (it's already true for the MKV extractor). Issue: #2362 Issue: #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145538757 --- .../extractor/mp4/FragmentedMp4Extractor.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 7d687cc709..f7cc42c48f 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 @@ -420,19 +420,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)); + trackBundle.init(track, defaultSampleValuesArray.get(track.id)); + trackBundles.put(track.id, trackBundle); durationUs = Math.max(durationUs, track.durationUs); } 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)); + } } } @@ -454,7 +454,7 @@ public final class FragmentedMp4Extractor implements Extractor { Format.OFFSET_SAMPLE_RELATIVE)); } if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutput == null) { - cea608TrackOutput = extractorOutput.track(trackBundles.size()); + cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1); cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); } From 0a8dc41632fa4c95ff78bd33ccb06b586b09bbdb Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 25 Jan 2017 06:56:26 -0800 Subject: [PATCH 126/142] Set max resolution from codec capabilities for ABR where resolutions are unknown Issue: #2096 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145542983 --- .../audio/MediaCodecAudioRenderer.java | 3 +- .../exoplayer2/mediacodec/MediaCodecInfo.java | 80 +++++++------ .../mediacodec/MediaCodecRenderer.java | 7 +- .../video/MediaCodecVideoRenderer.java | 109 +++++++++++++++--- 4 files changed, 144 insertions(+), 55 deletions(-) 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..f8501c3858 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 @@ -183,7 +183,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(); 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..70445466a6 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 @@ -276,11 +276,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 { @@ -345,7 +348,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(); 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: From e9ab71a28060e152e2d900956b155a69dfa619be Mon Sep 17 00:00:00 2001 From: cdrolle Date: Wed, 25 Jan 2017 08:36:36 -0800 Subject: [PATCH 127/142] Modified CeaDecoder and CeaSubtitle so that it's correctly setting the subsampleOffset and making proper use of it. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145552166 --- .../google/android/exoplayer2/text/cea/CeaDecoder.java | 3 ++- .../google/android/exoplayer2/text/cea/CeaSubtitle.java | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) 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..f479050d57 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; @@ -109,7 +110,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(); } } From 98db14e7e5e28ef409e23b0f8977727d2263eee6 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 25 Jan 2017 09:00:50 -0800 Subject: [PATCH 128/142] Fix some documentation nits in AudioTrack. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145554504 --- .../android/exoplayer2/audio/AudioTrack.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) 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..71049c9de8 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 @@ -54,7 +54,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 { @@ -120,13 +122,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 +216,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; @@ -386,7 +390,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; @@ -627,6 +631,7 @@ public final class AudioTrack { return result; } + @SuppressWarnings("ReferenceEquality") private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { boolean isNewSourceBuffer = currentSourceBuffer == null; Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer); From 6f5c7b38a7077d81fe0fd9ff216ce8b5dd34ad09 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 25 Jan 2017 10:11:58 -0800 Subject: [PATCH 129/142] Wait for first sync frame in MediaCodecRenderer For the video renderer, it's not true that the source always provides from a sync frame specifically in the case where the surface has been replaced on the renderer. In this case the source doesn't know that it should be providing from a sync frame. Issue: #2093 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145562222 --- .../exoplayer2/mediacodec/MediaCodecRenderer.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 70445466a6..9be1c59baf 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 @@ -201,6 +201,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; + private boolean waitingForFirstSyncFrame; protected DecoderCounters decoderCounters; @@ -366,6 +367,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++; } @@ -504,6 +506,7 @@ 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(); @@ -633,6 +636,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) { From 63e6bf5cf2d2be41eff2725ce241f9f523beaee6 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 25 Jan 2017 11:06:56 -0800 Subject: [PATCH 130/142] Consolidate UnrecognizedInputFormatException ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145570097 --- .../source/ExtractorMediaPeriod.java | 13 +++--- .../source/ExtractorMediaSource.java | 14 ------- .../UnrecognizedInputFormatException.java | 40 +++++++++++++++++++ .../hls/playlist/HlsPlaylistParser.java | 18 ++------- 4 files changed, 50 insertions(+), 35 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java 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..5226043593 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 @@ -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/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index c349bbee05..0cd861c369 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,20 +40,6 @@ 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"; @@ -116,7 +103,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Wed, 25 Jan 2017 11:09:10 -0800 Subject: [PATCH 131/142] Update logo assets + shorten name ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145570392 --- demo/src/main/AndroidManifest.xml | 2 +- .../main/res/drawable-hdpi/ic_launcher.png | Bin 2929 -> 0 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 1798 -> 0 bytes .../src/main/res/drawable-xhdpi/ic_banner.png | Bin 8662 -> 6884 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 4177 -> 0 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 6846 -> 0 bytes .../main/res/drawable-xxxhdpi/ic_launcher.png | Bin 9809 -> 0 bytes demo/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3441 bytes demo/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2186 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4951 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7732 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 11241 bytes demo/src/main/res/values/strings.xml | 3 +-- 13 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 demo/src/main/res/drawable-hdpi/ic_launcher.png delete mode 100644 demo/src/main/res/drawable-mdpi/ic_launcher.png delete mode 100644 demo/src/main/res/drawable-xhdpi/ic_launcher.png delete mode 100644 demo/src/main/res/drawable-xxhdpi/ic_launcher.png delete mode 100644 demo/src/main/res/drawable-xxxhdpi/ic_launcher.png create mode 100644 demo/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 demo/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 demo/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 demo/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 4c6d832211..1a7848eb5c 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ 99J2~|IeA-y_d zfntAen$65iA!RvWLJb+FaqczZn~(hBwnih`LlH3muw;@>v8EDI%A(dp_6n&lCKQ#G zaIR&OZ*dG{8w_?18+AyQAYFz)B;#)9kH6dhqa$;3a}t1r237*(feGFHVa=oy0O)~- zesH4IY~BDczy#>FGaErcH=Qj421EaYTSxzML4gC;hVwL^-Sdq|V8i<9f9G-c_V0ad z-%Gk;(z=)fM*yIkzkPVqbvt)`sUuoD73CTZ4dh z_X4ha{W%B`87y_6P2I4XlWHO$%Ox)SZ{eDC&E}5*>;{knAU_5P0AlxdAHS-#<70n2 zm*!JB1lZh3BO-fe%MRTCT_cUI1OsTcXK;0=gZA_^NC>P|C?W!e<=g3Hw?M^a2EamP zK(0GQ*|u9Beyhh*8M|<_@v!&3-y{Pv&yjSykoht)8qr1*9}|E`Z{!XXP0WZEIJ(n7 zvC#?$x|TlN_q|l4Qv`z{WsY9l0lU$&bbUF8)n2m-WFJ8bMtJHG0HTCNn^`AxHW!Y2 z$`({Al)dk6Vg|EB5_iEYZLloYdU-7zczH1M(lMd8uz~>c*+8;wTYuaM#GX*G41k^f zoh73e$5u&xJNuMa(VK;@_4D#oi9u$2Zs8oTDl%OlvyK~%VvNg42TW<>qJQ!gXG}_` z>y?aN5&kS)CkQT@#w8@vP#hMaYsv4D)PY?x2l4pKC?5dXFzx1y*P$+ zV$#s+;jqI58m;p!Yr4)m-EQ8hs4nQb$6Upfj~Czj&goC2gElm{EJ@I4wLn_fOI42_ z)9T%;x}4x#%(znUT!rt4!1%{)y;+YYH!PwiXdNm8cJ6}2n<01w` zKO8sM=UIxYHf;>TG1vAt(hIx;6;l(Ts{r|plc?|2LQwj8J)%}V9q>WNSYVuVy5Q+n~SY$C3cVKvM2mlj-0PNav1HSr& z&*Ht;zH6CqhIg$hKz1AyYJ8tdApkLy&N&ZeJy7H_7<;Y0vpP7LrH+j=>#*;hJ8{>? zZ^cxzxlB0q;~yVTX@SgbxWD;Is)84W#=083JR&ncdK9! zm(pM;2PYVCg%a#Egvi2iA*E>wX2#}C8?f)by}19=x1l{XBW1Kh>V-`!yyM)mSt!(8XzL<-nkvy zw{F3K!$Gz?OZ~h_EZC$in~VcvlVl)^q5e)j}Q9)L0}(tp(0#e>8I5-L9sAVdTqL;=sZk>ce%y-jrelW%ra%cH zAOVQd{EBQgux@1(RS(C7WZ|!%89-{_3OzuBPz|C8?db@;B!{Ne%&nI*;zz;dIpg&YaM`9n{g{}@TDCBRKxl2 z{6*|P^bF=+`}0oMl z8bWwI7nE{}SXQMV!m$%C;8#cffH>(5Th)=x0FqD->m`7k2MZ}X5YRc%(t?H3g26ks}7Rd+( zhUyS{iGt*guZWc5o_YQ+c;eV`^s)?V2?yv0q+$@OiVbU{cq8fQOfi@wn@MVwWIHeX z@Zcewzwow6JwizIK{C%%dwV_g5w~5^IwmzR)L|#u3_M~r}BL5AcaQKGXBZ;A8IblCsD-@y_ae- z&p|?{ZAoy7Yh+&fiTg^{u{232tti6G>W@ElAx#!`wC=k9?&z9p?_ff6q*BaGSG~@I z*)(~d0GhMi&N7rkc4wZJ6i+f%ZZMgpy_ez4uSq`%AlM&~qY0p;w+$6igZ3^GwIEZxE#zb^FRqB1<-@KxtaefSNlGk z-i!){g=5SIBv6KJ)H+Uc*YOOX9D31EZ@eb2WI}=Q-E_RO(8AosLU-v|0+4I+QZEWv z=A2Q{xUIAri~^Qbco5J^nfERl0{hFsII!1Gnk!xlpIBj&|GT5cCV*;e0;t9&fNE?4 bsK)*eyQy>SBxZs*00000NkvXXu0mjf)E8>! diff --git a/demo/src/main/res/drawable-mdpi/ic_launcher.png b/demo/src/main/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index a5d2a53b131511f9734eba6be2b6475177dcb5ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1798 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 diff --git a/demo/src/main/res/drawable-xhdpi/ic_launcher.png b/demo/src/main/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index 1d00268635ec1211328ac13028a9c9cb03741eac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4177 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 N+`FT%S*~sy`9C%i_>2Gm diff --git a/demo/src/main/res/drawable-xxhdpi/ic_launcher.png b/demo/src/main/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index ef2f312fd4bff190a270ba2366d3c2159bc63493..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6846 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 diff --git a/demo/src/main/res/mipmap-hdpi/ic_launcher.png b/demo/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..6e8b5499deb00bdfb2256fa23eade5567db7a762 GIT binary patch literal 3441 zcmV-%4UY1OP)N#yUI*#e?=1lAkf_yai-a#nb)Xz&b>6FEz8(WzW84LQ2cT?2Y1M(E=Gev# z)okn45j8u<39j!I0;9UwJpy#hZ_t3#8o_ZO%1LUy0177{0X7OxmD7Pm_YBn6CRaOP zcXsTH^0^R1Fp2f}%F~>bB1na6ARaucCA_FuhIyV+fSyp13KfP5RpER5-Z5 z9ie0hG)Cn_D0+o&1M_>C)JCU562O^&QbP@4jZ(WBj*g$=LDls1^gQYmNW9}ysZ_zF z#sWAn5Jk{7MQ3N{?;YyAdHQ(MC^B+^6NZ5-d>e5G?d|O%hiDRCA!^REX2a=j1+G3v zBkJxpmqQ|L$M!>0Q`2m_LgG~t?U;Oy!W=l<^>09#6$8r5(3_Ga01OZGhAs=fp@r*vwZM1ze?CX-*fbm<{s zpDhA}TC>m6SjS>OyxHZxtT9h#ni>uLjg5_mn5hl2$h1&k=L(Z&oRqk^J<1v z{rP~e{xt@pqU@RGz2bQy!f<9U&O1J zLZSGNHQHkgkaW-UkTXpL2S>+hNp7$RWa(i>K}j@!=ga?;YLt3HacWPYi(_9 z*}Z%B0|tXfo}5%FUCgw{s_6@TfXdfK8id13)%pmnGBxi+!13SwcwQVD2`v%f&^lKH zU7zg*PTpl#NH{0~4*4$|)FNhkZfIUdtTit~fbzm(OajUay_vh+NSy6{}83L3$Q)Chl$qD~hI5bB8IbA@! zbo_Y@D9g+3VrH1>OOlh5y@gD)MlbX~LHxY3)xU-wKo=H7n+3?27y_h`$T=WO`3O|a z%{IkEG@9k)UlYgnwI}W43tlggvw_lq4=37xcqdK*>JS{(}wk+tgg0buBfJ_ zCLdW0?&HEcqrkwxTT#U-*u`pl0-_j)B4<-(mVxn3-RuafU>KC>Na+0J-@vuEn*yY9 zL=6oMmE*^cAJ*rn`Jiamu3h)wMG{*^Fnxf;A4EXbljfOGRdA$z;RyZH!WW?D(j{|7 zvZmFmSN|9=@_>#XKRyNlb=V6~Yg#y*ogHJgkNIkX$YF5$AT5Xk4o4~z5w&#l=FQ{# z0m{tG9EX79)+DGx>8{F$!bQ<=$k#F_{Oyx54hKY{C@3(RxFR4q0-D$l(9xqu$J+x? zL1invwPG8jk6&aFpl|%01E{C4&?KNun>Ibx573@Hd;C-?mDE-f`?jV7qPLeq+{gcg z527~0fiZK<0(A0W$D3H&k{2;;II zHWsSwJd$a$|Mbs{X^z$~UteEe<>lpdmlhxf=@%GD=h+%48UjSJ-TxJnT6D-K7S28% z10@(BmQ9O-i&LWELO?V$TMCErvrQj@N+Pi+P%JMm{}x$zX|2&_VfAeiiDZwX042SU zVq2>5ocRG{`>F)!DJ`)o)hH}1JU}e8raV|;HW>4eb+#s}`T)KBb`~7;aeT7cws;YA zpR{@oo1;mytgNiRu~eg9vg&c-#EB4Vmgt%Sv@zoEj-BohMMLM#9l*(C)=rp_`+<&* z4%o6~OAN8lo$er8bDuYFo)1!Nuw^9E2PoCdQ7mn0fBv6UPw$3PO9CTe> zUE}Q8v&XS)Ui*~?M(p;+#>Ovg@k9Cm{c~2LNjRHB-qu{ZI5`hK(sR8op!Qeeq32?u zU7u`RxNspG8Qh_zJvx49XnubFE4B>v`T(tYZLdi{tKK}QRVIFYCTyGhTK`FV%%|iT z8K73$?^!;&6*bK!rl+T`AqEJ_Q}3HNxbqZ=M8A^DMDb9wgj$) zm~AEf%EhD=!(aZi3-&&|1g?e0KxgV!lhYQ~nwMBCZk{=FCM~S;H26l*{{8z099IsA1wLDIiuIUe)uX}s+T(Q*}Nt}ZE0zN zb?eqeQQ!6YAK?}sA5ZUS-B(*%d&(LuG6l#SS-iIlj+HiplH;7Rc%h)6;2ieZoAurJ z#t05eNlA%TC=?cJku88?cYY6P=Nq6yu5`xwpk9`}!-fqTUZ6gwrly*=@^#UoMYrPa z{?gLYqcr?kvVh83KyRO@ftnV%Qb=&B%prL>d+r7DRnbOiZ+Fogx+>qW2v>d^nL-VId^Wo`4qYz5+)sH-SRQ zIUA0qFYOpEY~Q~9J?i7il`C(vW}&OwQ%^lbk3am1ii)z7Hv==1rgm8@GYTeVM=5-K zRtznjJ+4>+OmH-&b>_^OQ`on$tdG{cNzK&m&XFTWK2}{_U1`oMPu-{y%l7@C7tXrO zDyV4cayl5{!cB4)#l^){-rnAmsBg=bEwk;F1gu~v?(SWC1J1?oYf#&(s;Xp|Ej&+sVtuoHn?HZP7w#TNN=jOMkJ<^i40-*VlKjOn5*-;vF*i95gHG+O=y*bNm7V0%$_s zVK4f+PoF-WzI@;t7Z(?H<;s;R7Hz9-c@pA?IEUIq>wPX?zFZX@9X*HI%-Zhox1imF zgM;Z;@bU8Udg9!4@UsYGUAU?9D~JC52$ZFB`3;pXA*?@zWp z8s!lT+h5Gf%R56O9r-7=^?gyo7R{ovtPX;r<~4@yMfb#eFQGP2TUeV0%*ReN+T`o& zOJ8I0Lz%dI`SQ49$BylIR0>o# z-DBy}r8HeZOIG}-4Ff@I3~hrZPoC_>%#J`kf?H5ZO3Ghzb91vQDk{V@dq)%GEVlzw z8x7NRZC6(pElq)|SFegOk2*nhQr%2r{OF!^Z)(F&;WI*mC<&Y}VFGE?C=^<;^(g+j zze`9+cxlIu9UHT=vkw*)7UpB9P>xQu;o7xpEodqoBy@~UzoD$Gtembv-*#~4&Yc^n zEY-oZh3Xtl_h1R2Ok_l0VXp)TKhK^<(D-2d3t(2j`9R`ddie3&rX{* zZ59a~)Bo_?Z|NGkmX)Eh4^UlHC)MrDq1)lG&?E-auT9bH#EBE{88KqSeKZ%$z>$;p zVKSQi&zr8HYpD#Cr8<62P>ecu>L?D$9_Um(XU&>5gq9+c&@ugw&M_D+bGZKlo>mpZ T;jhwa00000NkvXXu0mjf6D6Td literal 0 HcmV?d00001 diff --git a/demo/src/main/res/mipmap-mdpi/ic_launcher.png b/demo/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..26fe2f0782aabc52a1a4a98e0a91eaecfd84bc23 GIT binary patch literal 2186 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 M07*qoM6N<$f{Z{Cv;Y7A literal 0 HcmV?d00001 diff --git a/demo/src/main/res/mipmap-xhdpi/ic_launcher.png b/demo/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d3251491ceab2edd0085cc3babc6ebd1b46f1782 GIT binary patch literal 4951 zcmV-d6R7NoP)KE;0(O~`)f^ydw=_RzVCU5Zx2sB z^<;UnbhH3~6`D0On+}YxXY~X*?ExnMJ2A7-VLLXW?N~oSw*4MXVA-|@IR0jb)(fpa znlsvPX3p$8`pvEbLTuUVlMy27p;j%v?~FDK%>!*M+HSO1w3}!JXq9NSX!Xo$*>`u@ zIb&T^J=%ISi3oTu7sdd+Ju)6i6(d5z+G^j{u~W(fsNhE!B;30atj8& znek(x_Wb9tQHczE zxrIxPT45`Z(Ae0RR8UaR( z;*?h4*W3enY9PqYF9lirQhdJ^zbmvzzfePjkOjdN4Ut52&k&6+AoTZtPS4#F&tYfa=7YizPtfsvG2A9Uu@?V zgW_&1DDl87gajozWhg2t@;B%x;NBj=e@hhSYZUD$ij&(1Uu5!2t3gqbW)aa5^9PWd zn>)iGkr8(=wY9aqac6h6jq??EqSYqgq0In!$|g|Om72RhP!J|4MFok8iT(8Ilv#0& z^vw$Q6RJzZqquWILjstc@&rZZK~T!1W?j0#g`_9~v9cE7rk~!Aaj%!Cw$o%(0=UJ! z4~oKzxGl5v4?@wXuC5L;GBOtFWfX9SQdwEq1BX}^lOC;0$vP9j{ln$YKv9)#wo+74 zQIQiB74;{boih8g754srFzL~$j;}8Q1dE>aFn^F%niUC1fTE$H0kX5Rza#9lCV>A@ zB9RQjjaIVWuQVG1xSjC4{be$;U1!E9Ue`E&yzG4oRGzR{hAQ)VE-(L za#0OFx9HK{PC4Kn^fq(}O9zbkt>_U~{DA)hW%F53~(~V8WBB0^88^llZG7AE@Jy^FE zWI5UTm?sdj;^gGy^=AeiTDsQKwfP*hgxRH7zFeB;KA1BkJ0t9(F|B6r3J=!~fdpyJ!P z$(oIVlTO}{{^lGgcW(|H0pIdFU95890;oTJ3^1h8sya%iub_X8M~v-Sl_l5*D^E|) z?hOqMzlm!Z#v(wncNY9QYpzD%pC0B7#WOwh2C_OPS@sb$BqwWC7s5#B*2IYusV-#I zEE3|?&+Xf{_r?%3*O&y<9i9QHOXdi1y(ZvC4)lUTH{*gYw(4&;Vm4i)HUZ>_3knMI zmoH!bQmg7HC*bJOqmH<4u^~dxmIRR8WUreICr4_h`Bx@-nhg9$By8NEQ7BqeR8;!S zH{ZCZBEb6W*|WoNm3ZLY#n@ zn3xgxhiXF+P;))zVb!z0?%`$0EDy6FpgtyAlVq$037DXY0Gp_&sF8*w;B0CUeEnV! zggY&IYzUB^KBW->fq{XqveI;G0wN+Jh8t3XPW@gEp1acE-7N|5FSqqNjfC9kW*|Yc z-9eV0uaN`=1O$v#Nx-&k+lI*H@?t|1!0n@do`xNhR%=B-!nnC6C!pqsK#iR6{rmSz z{rvn!sw7~|nl=3}54dBfFtjBBWOGAq!QWT>3Z9!R5zbYC%#2X6#Y+Mo^ zeDOKRva+-5{_X4mpaw6 zl~cZtNm}VFcfA2$wd4b6xNt$+3aKy%Zrr#*r3lJ|t^NJ|o0TH0@CzCe-O*9(G8F*} z{+$f_M=a6GCy*qadD$D{TybPLKm6{5lef+!RQh;BU05h6>g%G`(FOB}70~RaLNS*RBr;W7Pl7s%FGG zZ`rbC42Gf=h6qJf5ioDtbqF7_*yIFMz2^(k*mIzi%k@4bdGFr6s+B8OP9cm{O-Yg_ z(GSm8S63$+lYnJk$C!3f-pdne_wG@_irIWMgE|Q0Lh9^pfM%IDCdV( zR8&B4aPY^3E&72jDhXi8LD%{7=f7NEUw>EMGNg)t1wq%~kmCZ4dcW6>)MndQy5Ssb zdu>g}rzS352=ymV80Cyv|asgAwzaAzWAaeYFC;*>p~r6!tZVV6?RTssS^BcQ`f+v z9}+t(XK?#^-!-UCylSfZP6(K#rlw}R@WKmpv7TLr^23)8Km3qRo#=Jt%9V|56zJt7 zEp=)^UOR7s-|B-xgtL3>$3l+}{NI1Vr+$Y8iBSjm_e0a+%!k2yiHzCu!U2++lR_WTaJ6N{i)Dl-Z2Z8+c388c=~B`lXM zTlTaj7fK4}2;r;4wQJXcl zV8Md_#-UWAk5OO<0#+Q!f@^oHO%8m^-k*_?QHH($4Z_gZ*SCu{7t32snly=S?cVRg zg$tYL!ed=Wfj$ZFJ&+DjH_M<=t}rL?h2Bqvx6shg9|=2#p>}so!J$LGpeIH|E(HYz z>AL0x`XIn-cNzp=Dul{9nb`o}a^eMLJF9>H{*-w2R4evxZxrA#A_n{J*sjps^HDY_19L z3-{fIi0h>wm6`7i{5Tf$nv#-|=j!S@k+6FG_1C-WqxbX0dh+DS^gx4`Hg4SLi|Mso zo2cmK{Tj7^pNl#l7+(OTHH~Hqd`s!c&CQkj`S~p&j2KpWzk8zXh!G=t;p^ZdM~-|) zo?cUbK$+?4$n18(|12sOZWmj|y+cJ8Vnaws$Y#QZVPuf|Nn4?=b?w%z8=coOG%hYK zjFJuFfKqRD8(o0O($etER=|IFFazQ;D?y=9S_=3=xq)i^5fKrG2@}GGU5aY3Cv8{{ z9z6J2HnrxOkdSbOX8XwjX+lDcR1U{(ltI9`eE2T*t`N`HHOeglcn&@tGhO+3>eQ(? z#9{X@q_`xqLU$|r!5k=Qyz#mIzB%B zEO*S*P=#gO`&sQ!Q*<*vIC=6U$qt=nK2YRzjsAos+F@rG7Z-ZM5Iu!%WOQ_Ncu7eK zkB;Pz0|`9NXV-X9eV-gI-Ev?MJNC||0KXXt=zN}MBf)j|?%m&GvL>S&6|s!jB1Z%7 z^~|@?9S~(fK|vI1y3(<-V>T)H&E%+W-@gCVqel-SVfg2tfBrsZg}J1amN**3y`GLC zDJkjBs#U8f$r?__NXKd_*=d&qdwcsH18~9j!-fnQGWo=b6Oo05g}_FGP%1QMB3Q3i zu(r~5y>R&O;p0wDPBg1EjE?O&T+9u8F$olgb#rudd=X!rC_Y}le*LPeSFfg*mzVP= zm@1i*ZZfCPlF5zWXsNB(>offQ{Xb(;Geij>&X>6%#m+$27NYdp1q+9Eu~QzD^rIEzCy#-|riX=v?Ls1osF%@PGkY3`oXJy@!m#&n>Bu9Bm!Bz5W2r_b~K`}ZdY?My>@!h{Lau$OJYVV*`l zf@RX23?+-4_-hjbPE(+P$=suDaf#vg`KPdw+OTuy&cJcw#=XVl#+lkmZDzgR#uCAA zl{u4s;&ASb&Up|c!WGl_@oU$vT^<=3c{DXOHJ<>XNewXQTfTvYayQac7A=u94Mz}0Iy*bR zv2NYERha1PzkK=fH9D)HxVTuD;HKV6*I2QcCs4JpISL`_qB~TPrk9qMDzmb(N)r+i zuFAG9ypC+(ZsKy6_<-;K4=`l-iw;Y8R`JdM^5V{1MtTnHGnanq(vbHjXM`Hmes zHXS>5?BID^l9Q9KqgH39r>7U@gx=w5ma@hsw2q-{yuf= z*s=ff=9_Q2;WCqq-qUyZ+Z0-dtviZox(nOZVA?kAgXw#Bwr`uqO=`3u!kTF=0n!b7 zr#)TJOtCk~0~;ZZOtPFApk%{DHkAEF>p0SX=)Y_meQ29ZVxDIEvi_q3uMQzpYw1eB zQI<_-8aOyO{E7LHK4PQq=r>x2)@5LKW!rcHxQ>KyQmmO|*f3DV#=o}~fo=AH|3B#l VxK7YlkA?sM002ovPDHLkV1knZn^*t< literal 0 HcmV?d00001 diff --git a/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b5a12d35f337790ea7c6eb6cd43ac4cf5761efb1 GIT binary patch literal 7732 zcmV-49?Rj0P)W_Vw2BsjWj*oad2B z0R`$%6;wb#P=O+&h|I`5Lr6~8Z?Cuy?~n#a$VqZelJkA*OT^^loV|Zp>%Z0>^2Qtb zLx1QG{h>efhyLK}fe<&~#`C82tC%*v4aU2Wn~VcKBLdrD^4V|=hI*K|2aY5>N&O8%%SYJuVtY(HhAdFT_z)4Jh|x!58%7`W z`{?tCTL7z`T+3c^ZDvx}Y$Oes{!pX0Asz5v*u1c%%K(OAfIfveDxJ1x?hb=)08aQZRR0)eBP)UFy*T%fu1ac2H(q9sNkE;%x$=J?g ztC8|FBJ%A=95*q6i5co@$URNggTyX4%uw}rY`3Mn4eeQ&R@bCgQH5)CI)(MBtz>ko=+k*6KxGu%7(TlQemxwuES%@ zc)o?JSG%f2Rx~ZVe>|~WXw0;PJeoj%8 zEBHA&Vl5D2b5fBw9qR;K#2(5tMhG$+$4^dUetgam$tGdhi|sHo_PpC_^o4<$rY zUKkQR-Hk+H>#(gyqS(ENDu~4YC(^^ifC#cPoVX;1hKBa$MT$=svJw*$AytoLUGr4s zU|`lF;q{p^4Pc^567}#9qrbT2<>k?P_wF_2bg_|9N;kk4?#ntn)pUt2P(rySXkI#PKatL-Sa?I z1EZq&gGvMRS|82A=kh}0`ZApjWkR74<>lo?;6Cf1aXmz83kF!EZDwEhFaV1Eg&lxs zyX}Ff^s7iL66hR6&_^{jH4w^vB6`S3qoTR4u5N-VbdO2_qV4`lL={9yM7pl&q0-V) z6cZCO75ebry?aKQ3SEZSEk>z&PLIj}k{C|=A8kV7$}F7=Wx2VzPjDX^t38y_p$lKc zT2+QF^#DZdrV|o9JAuT4N}UO1RaI3eF){HwwT3cM$u!8%&mVwEE+e5!%zJ=nyE7kA znKvavCOR-tAH90@syrwtXb^$OP+i)`kfNxQ>Y9$KR)FZVKlI;-DvH%=eFS}nH7rU= zNx1}ls`9j+7S5HGmE-9hywowPP!j+(4U0V9AsI%|WRtuRa;w&;^X7l&B*Aw z4;ES0>Z<#&0Z_p~Ck%=^@*B{)g-mrv#mAhK_ede1Au2zd_jRGHtgMQYCr>&+pZoau zbYQFMY*QNM+lyB!Ef7`aj#2;#0zW~i8|I?ijXxq`S{m;F z!rB(Pm%>RBij0ih1bzPa@nd5)#=O9cL?Y2+O@uE+0ebP{9OP#sKRmSg+qt9M-OlZT zCvUGax;y3ddD5r5pi-APsQQo>5|xzjE{wtS*OZi$KXIRTBGqXFrL|8{QPC87H&jh5 zN0t|$iVL&RwZR?=!$aHejvlDSXO`UHl%JiA!e_ZP16Bf%^K6NgE$~Ej*RCR*;_ymL zzk2lwUB7;PI`q9#Vw$+H4s~^PyEVDhoV)-%@^Ec8Jha8HaB1^%>ilM*xaBUA&?o(V zCP1{6ulfQB9!2q@egt6*roP6+#2kgbZ+94@!`Gohhq~euCu%}GD*{lR|15NEkcZL$ zT_5d%1VJrlb?N_^gJ09~`(*({TjiEbNSKw$c`*%o==t;Ksh@uOsd>A@7#+R}3JRt% z!WZHyKsi4D%LR@Wv#v_4w02-V%Zfy6=~C&e_( z6^e+6@B#)D28fY;nFxi#a6SN(u*#J+fF$*y58P1XTvrq@M6s|aFF>?atY3>7;$k_a zj{qQ;vH0xSvlz^Psls859`}lkjU9wfR;j7%OCb-mo?U?2Yl;FSu{lntYS&IA%FE+a zHkO&0S&cRD7+^u6q9UDr;dIWO=pT)4LnjYFFl517fatL1=7MT3oJUkmH5ZDC;-aD= zbo1uT<-h_4$V5^7V~peAUP)eu53U1L>pM$kTFobbn$rGdOHqADFcOPI8VzHR&Odzk z@C2|Rw+@0>=i=f5?y93eAPD9QKwvOvsto`&rTrVeK@CY7+igUmgRqQw2s#LukV^;A z`e*m<-4>XeY@&a}d;qBYymFOjt^p*mxz4D1-(DmtD$>Z{YFb)azNe>WKQcmZC~x@c z5FZ~8KX)zEw18H0EDQ>L?HLQ>EI_nAn&*ycfAc|9eZ9JLkVHu5=H`k{pFaIQFmdeI zF=KfJv{6=8)?CI$&WJAnNk-$po~tp4GH1+H2|!KyXvM#fAUsUvX+IItjUc4`{QN!z zCgc**1R#@=l9CNX|A_ep5LoEPZmh!*IM4%yj&(;dlUz{jdv20P_;;X#y9C(qzqz6k z6~fa@AN}w@NSL0kQh>mBR8&-;J9qBv04C%Dh`gv{Wo6|dE(qxM0`$n8JwbhAh&xJ} zIu{i?ImztIVrM7xYL>HFz-eYMZRE&dM3t7Rbe3XRSlD@BLN0(dZQ2AH$P~XZfUf`* z9d$whL)j7253yG%Y5^RsSlPn)s4n0d5;ruko{2T!b4Eu;!({u;gpJmZsZ*!Ii#pfW z*FORQ&8H9sb9tUDc0vC3?7$N>L46^K4XarF1ro%>u%?fo?;(`M;R7>aM%XZD&HF%_ zX5`@D&`l^5Ci4X#K>!xc-^@ipBiKvfLmb^ui4%X|X*MjfeH$yYA))VqfoIR2rCV89 z!B)XWgpn5YPY3cME0IW)!-X19s{sA}dK$-_N$iSce=LY|K6d6GJj`tE^A$)`T z2xLrZYHI$11q8GP!*D@=*NVYAzHJr2(q>Z5DdE#06coS6#(9aq3*H00>N&nyPd$F*7r> z=7%4Ccw26O@Z%^9Pd%3a5(Lgfso%1zb6PUKhcBkpUcRI>K-t;Zf-PINOawMs1&EZY zA>SW^;c4IsKn4C@==MbQRFeH|JDa>V12Z1}G;dM}z^I3~XS4Oxgs<+uM6A zmjF8TM;hAgvK#r@d+GsF6rlb4*ziC?xd0;WspGkG=NvISbzA}HP-rf4Ig^N1>i}Vq z)EPhaC@3g6%vXTGcDe0iEu00|d!WaYTu@#MAi>{3GZi~&U>;Cl01~TU`xr7KQL?n> z#*G`W?OhMTMyo3`4A!k%2TNJIr>3TU$CW9qiUI_o?AX)|>=zkr+|eJy+$GakBPYCm zgn#>!`PacXdn-{#$W!r5MCO!M_*I`PSFZdFOeg|Sx3I7<4|VUx$_7BLrxPSqYfY`n z$e7{6>$%X?;!bS>MDqMDlO|2F!2;?f7cwz<0b02u82Q?HY860bo*t;y z=Zf0agGs0GadC0Ej*gB{1M2ej+ix4U1rQt^EG;d&;77~y^71iheU;i8r{7fXJD|j`OUt4`*x;(XSJ=v(8a~2D}JNwzQ8(-dU@4|&$ zIhX=EH#Zj@IB;MMut1oQ3m}6TGiHF0?!JEg`mrLBs8rKqV)6jAb@>V20#xSiifS)j zU}NlxwF)#4l$e-UF@OI2cYy^`M3ifnG6N7x=H}+z@N;YI#vW)|NXrAz_N7Nzg8&9* zfB9rP+Og;my7<;g)d9r4oynR20{wIE-aXjjZUC?#uYhh6(vv4oHp7orNl8gN`2f(S z&(E+1(6%Kr;)fNb2+@;&$eI&EZs1lo_ilCO=2QUH}L zTFBYmS(){p^z?M<`|rQ^1O{;Ul3PGGg)s*Q2hc%1u-W1;7OzP!Bo9FIPQ)RfVT+Xp z=)&JVN3Lg{wfH%!w%taj-u+T>fav|9D>?tO;$qmDEG#T61@}GtnsX1rfHy4qvIMsY0BAyFuT#NB=bGAeez>W@gq4!dPKpp~fbd zDKZxJ?;o!>>ri>^%FxAV#m*3gr;UIu+Z%?wW^BUx$wLAV4}YbH-VaEUeFX;x!)_b{ zN#A!=RR1u;m@#o-)*&Gw8)0^uCiaY0bUMenAA-=8A@b`X@LFGBZPOljniVs*-avUr z&LWWtr&*|aZ)hkU9zJ|{C-nLF@#6s?;?!JOuk%gd3zU#o&1Z0$#=eS+i=p;&1^U|2(Xo#*;j39T1{+S{$G#gjY?xkM zUEQGW@iAopQk5 zOZ;e`oSYn>ZZE_ofaaY~LT952P-UHv_uxp6ykEU~6(-NylfE`pHheV`(^w0EnC`cA z>(<#-RaGK&ru19^Xvdu_lwDS@72rt!GEYK60=0VeYN$N-8#;8T(&AYfARw``jg1W~ zE^~;DjrDJMy(1{43jnS4e~zAHS0PHfz-jV6Fmc|;$LALGE$L%=@)#=s(PG-9U%!6f zg$ArzwQ5F5NlC4`ypW~rB1tkkr-{;PqgUk!(N)GU& zDqYwT&oWbbLg;{T_{h%A&Vvh4b#+>u0R4FLC3;a@tF_=rFIx@|4}Udg%ox}_YCzw< zeJ$8X-CIrRflxx~KyPpF??AniUj*khK&yPy(8G)ht#Gjo+?Js3PKUD1n>R1PslAwOJ(OP|kZ6|n zOtbhoBpc;b)axWT((An9;^M^X)~)*-`U>kF@IKwuA)MO^WiaU0t6#r<4&W9qUApu$ zYzabsYn8Ps^^q{e!q`^<=_oF@O4q@mN7@@xQc_@*=|Sit(pSA01EvOVXy|APWiSD$ zTeog-VIyzcxZzXydQ$+_LK%dg?|#o{dm`E86m%;|JEmE%6}mDrGtt?zX9J;cNFVjl zWaw%sl$mwz+_^7V3Ni}E+Iuj&A8vrH2B~WhNd&dWxg?p>EY3vcqpEtL&V$2*2cm(G zk54%66WHf@kg2ID3`Ln~GIYuELWZOS-UA;t5M!~8dHC?*6DU|g;fi8$WdhW0ZY7nV z(-@dDQ3aBTT_x2GdT^wQg^+PwyLRnQ+!yduO+!fE^k58E7`(xegQNDk=?rI+|^W^wv5&Wo2csIpzy{d;2NS7o<-xl*yPERr(uXP zXiuc?Wc9#v>(;HTapT7Q9r^(G1;`2u(lvaqm#8yb#pa12f>e}lOwWTh*m zhp7D_3#Jt(B_*LVXU<&3Z95j)n6$N(RJzU^cv^zUk`%Fq;Ts)&^ytw&uo9rArbe%e z+6fQL!-|QCq4wb#fp^x540(6TYDmyyK<>e$fhRhLej|rR#sL3(I}`>`1|_@ z!yFY@Dx^>KTTjv9F*!LImLT7^wY7a8+OU8B{v)AHN!xbeqvny<6B(0Q1PtW$#}EyN z@fpxX2M!$g>G9*oHBdbTQ3&5Oy*Fwul%Wg~9v)uv(@#I`g*Jn>BVWY+q)j_(BRsN- zjZh7R-Ou`(nVAjl*|X;;aKlbcPO}060>V>MQxU{>A`*2HBpMuO1Mtk?b#U?#@$ttW zJ40JRn?c(_8_8Zy{OMDAHE8r0u$CVn!iXAfm-JxZi&G8#Sa$mo5@-gyZ_V zXV0F!5FH&|RZvhs@1P~p*3icAI3kGQkXS@TMO7U;cI;BWe*NBuHi5RmZ8Q|xinJLm zmURjqS-QxSj1pT(d808iX3Us<>C&aZr%#^>AoCy~Q8Y+=hKX_gl28TUfChZ>4 z!L5TX0Ab0;@Wu>Nkdi}~&dbY#nojM-ix+QtczDbw;5b4XKwA*6V?o-eqpo@#dA(70 z65j`a%f^ieMY7S*s30Owo;-Qs!Gj06P|dEYssf56%T>s-B2L0YyiFtVHj+8YP(+0x z2)LKy$B$q9$3Olthk!F0?wN!u8`2it^0=!LZY-}xD`?hV{w)gJcdkXDw;2z;# z2{=}y4Z7&E)@iRdG9s?JE13ud%ZF@4171BwZivKoqc4suShG%(;D!P5XVfByMrJzOJPtFyE7Jh*mJ&yj>G;&E)ry_%DI z@5o5c_3*F&lEfo{F2KTa6wXW_v`NAnj6~0yH*dlI{reAHy?XUwL_|a()RbUeFaQNw zgK^LuO)76#KeU5O3*=XjT|}h!}aUeuQ_t$ z$Z=m^Uzq2gj>BCgbS#8C$anxO2rFdYN7U8<9tB8a^ z2`FsueDJ{sv)8U&yL#u&ox4t*I(6>y<;&Lt0|O(2gM$-8LqlIiMn>l1G`a}GP>MrN zc|t-$d3=0)DF(0T(W6JXVPRnzAt51&K|w*0zyA8`bvVbaUAuO|IpN$!Mn)6KXN@JF zE%7uAXaTjOqnth44%W^;Qt69;~`u!-rocZ5yFn-Ge^QTz_pNTVnRcQ zDPwXkdT=xVlC)Q1!qkhTzkSI!9)KD|vN;J<7>F^DE;GCp)5b_yv;QYvWi&a51Qs|q zdKj{fAFR^&6x$!D68YtTc(QF!blqy;Gjq`O;Ex{*d2l#V5(q@-KvP#Ttg_y6^s z>)P2*Gjq+E=iK-2xzCBw(Nf02q{IXO063~D3VO(Y_x_@WVieL9pA$J^KMDIuzn4el&fw@J9mO zK;MdV;C=KJ^qR6kcVL}SBw_i@rE}f%N)7F!egvM~&$3q{8Q1yg>8 zswb68L2J^EL%+vGwk2X`jcAnKb2qFEjHU+1puCAl53a@KuYaW+E#T|yc+e&C^&qSM zU7>*TZ|8lLcKnVn;LF@p0?tiGMf88PsY$=ly1Y1aZQV;)QK=PE5^=G4&B=!E;cEGmy_8d#FH86*=?ZoR})B#M$1 z4-i_JOoUqJLWMmoEX^q+G}EI+rY8S_>9XExqI_27NV5Vi1k4|O3KT-;W@q;aQ)R?a zqdiZ`8|lFk+kESt0@TjkA^Fx^QeR#Do;F3n11!z$xH#?`@JNMoDyho!nfu?re>mhA zvw)z$z(A^Vd`Xg`7ueQ!mI4W21zexOp`qrtOgg{^Ys9zTX#$)5*1t6+Qx#0gUzwQ& zigI(;utH_IKfkeFq?QYOjyeeFk0zY3n{Sc`MVvLGK$6&b#rM`;m+WT?7qv|fpMtZ;> zicUa*p%z2k*ze{14GNrlK!3ucK*T44nVFeO@#v&-kX2An zKtV!s)vlW)!!S`$_$unP9%zQ`b&|<9l#w>Pv=wGZ(qMb?B&lGM!w5eMA-SuY+cO3h z){)JW4~SMG6`h#7K(^G{k#9x@D0oNrZyT=d zN2A;GAL*xx8`AlCbM);2afQIU}eQ;vi#3D}lyoUU)ZJP3|5){V-2K*KiE z;H32@Ajf$MxeF?KVtPh~yo1AGfS1>eh)J^{-5B+u?m?6O>lmKO6FEzCI7Ra7ndmMZ z-Rv=Vl_eoDDXE;CTpv&^*w=RlZ|W!6Apy&Zz3I&bhRYU~B?dfeQX`2uKMXS~9B3H! z1W1;{CnXih`COPRrCQ*-w6sLJ&7#-3Zcc`$=Lp{HD--PjEp6fN(%IP&C#f)Eq_UGa&49Ag}E&BKDT*o$8kBUFB1+S}U? zBB!Y{!+dC17*)*3S&cG{IWNBo-85lv)2D0tPxtC(ML*#+#!kU;ATo@3O4nOpgWdC8 z$xQ(x+K<<~=sHH8l4REQ-Q&_ay$peu`vhPxcwBD$;Dx+~y1@~F=6veq)05TV<5eiY zWKqZhb=Y-_V%U)%(_+w0Lqp^AGqbw!jINBA&99eHbG+Q#dC=x{^B{CMXH{#DsgzAh zNY~KLA_EkP0zyIQDZ_1u(eitCT&Vp{Pv)t;l9rFA35E5=M%4Qqy$CARkB7#OP>5nfkLW)RM^el$~-0_c6X_tz$$%NuB{~) zS-!qWFSLYrb!F1?@^D(5H>9F7iC6J9zXf@Fdy}~WN50*Ns(?~`1HplGVinl_idavJ zxT6ye`2Zhf7P-Pa$pMW_RK+OV&}#Y(Hu<8~d>!OhUe1^F^Q9N@I$2*+u>%RwPt*xI z=WprDLuJH()z91QVFgY2tZ(9hJ+oU-^yfa(PG2diw4x!H%=LCm(vc8EEr3Tp;-!N- z!}R5)du!n1oz*~@#RMNtK#?%x#EB2TM2`ov%xsG%B}&gK7Y+SEd8G$P>a)Pdzb|3W zYOlnG2!fkBnK$HoykQ?xZIpJfk0}~UI%l_1t8vH8artCCnJlYrDugtd- z#lb2o;);ByZruQuUk>e&sW|#O3YCBP9pwRSnpxo6jXHqp`|93Z@Sm+MEIbb)0&Txep z|D}AL5g3c=DkUZ5h+jp?nY6WySC%t2HZ~b8d2LPxTFw8*1_Syl5t;^`o7=Wd1u(8Y z4&Z9Nsq%74!+_sH1k2@P${}ZXYS=97k<*39gVRJv1u77oBJK5WwSkdUjo%uC(9UCl z0jGF64*_2;-ZKo0;7(lCQ60=O%h)VlR`C{c@yVR@mqn|ZsPb@da0HB{b3Y2Pgu!m* zBQq)=6s2&I0U_7R48bflwcyQ%u+Z;SvY#_6ZjNGDSXsl$%gc%4(XwWRZzF`CVW3QW zUcavHaOpW_c$fCyxD-I{l4e9vfS*iyMpesi9TUZbiuAl_qXDNp4u!TpQ7g^S(NX_P z=7&liAZ>NF!|Hzz5NrCf{ji~4d_$>Ad2&_>T;9)l zdL0G0bxPTE_fjrql_?E6jmJujirkM63SJzjc2Q7JxVyVLg3x;&Z9}GbMf=t|4~9Go z@qSOXTZcL)*h1lMPrZ>>^Z^JR^S&=I<-$LRvn||cQvp!LU&w4Rm~Uia;>lp+vZo!a zvL-;G<>6)x)=5=Nz}4~J=EIL?aG(e$A3U7ud4BSvvC6P&%{M-Ko-S4{20O@6Z%SWj zK3Ay&a^bFyj8wp{+*O(P?5SpF1U5^;dywtzZ3`cvf9z9W{%QlM;K0h?NRNC&2w7Y( zM5ou*qA5U_Eb&8SUX7VIgGZrIg&jH|*$B$HPZEYS5ka-D6 zYpq~kit7tH01bdn7E$|?+8VoQ2@p1DIUa~YSSt&B_Y zD`L{pULE}hCSs-50%0rw7Wn6R&E$ncNBN7Wgl~eBr?1(o5Fg_=t6tFqRer>`1)t*5 zh<{V&sv^cL1=~IJ<4I?#Gks>0XO-F2GVvTRb4;c!=*D%mo!>`?SBY>ZCJ+h{$ELjf zx89W_;~Pjd(8vLqo-i^&mRH&hU!@8I6+}dM~9!ZYoP|xpepyk(Jl={)<~af{KP9k>2YFSnrrow zE1HhW_7%D9*lUr+1O#w!NW^r*p_LFlmnQU>C|4B|1!P4o}g;R zMl$(;0u}+rXWOvi;^K>iT2l$ihH6qEk69-ymAV2Quj~Zu-I=rh@bQ|1SEA_St>8og zu)tRF{su!SxV5$Qvu2J+ylxiYvi~PKupYPKWdR1gxtUAkwMU>wxB<7;Jj->Y>Om&Ju5df9VtQcw+J zNT4A*6HqPa?x6fE+J+;fL00Duw)WkFNx1CdLLj}?;65MeR5yv#9H6^ zlL>#r<<|2{?j{oOJ|oFHL7q!OLPAtO3*hABRF{OZOAZ3Ua=t^+Ma15Rq!|42kPC({ z98;Y7kQC2+d)bSQPePK`Vl^1s@`?q1ywtQqXonnql;K|514mjJ3go3d2OP(=7c{xE zu7;N;co1)a7{$=3dK2Rc4Zf$i=xFcrs|E8!8Lq8$QVybEY&uD#rx78Y#Lg4KXmRF6 z>w=g=HD;C_uww~xndUZ<4Jjky za&0=IR~W%s@*NKk4`V}efCUKU?M@xl)z!s!;!q7!&~SfQP-Op&IZb5BS~-R|=kY%m ziQ#OkR#&!@6-6CdPjg2syN}@EpFbnq_9pTavGut`t3BAkEsWH+OCrL;S7ZTs#7H7= ziTtdxpS*bp@YAh6DW1kY$z9U%z5P7zRkqDCXlrc^k0D~chDNfdzhaaLJNQ+;Wil#~ zoSeLIc+|+2O%Fe>OLQ3Izuy0c4jkZB@%K`Tzd)T~)d<;dDU+G;Lx>R^`YZdfxvt?g zhWh96eER(PGYEo5JAmrCvben5Ty}3Vn-ADlhrQ3dyS>#c2TyfGGziHCn;(lhN*YBv zg4d|G0kY08nc{W zxTYzqSy%k2xsoU(#>N-QX&7fAAt9ApKN5f3!fT?iDf-f6XMH~u-ie5a#2;jH!plLnArsTNiPS=@$pYP418 zoCMcp0zM@qq$59p6VS>NSp^?vV*syWz`Cq`@2`)SP}J4bE=Tcn@&JzCK+BTO++ioA z�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 literal 0 HcmV?d00001 diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml index c0e1488fe5..ac17ad4443 100644 --- a/demo/src/main/res/values/strings.xml +++ b/demo/src/main/res/values/strings.xml @@ -16,8 +16,7 @@ - - ExoPlayer2 Demo + ExoPlayer Video From 25c18ca1b461870a0aaad15c339a522a38226627 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 25 Jan 2017 12:41:38 -0800 Subject: [PATCH 132/142] Re-add playback tests to Moe migration ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145583193 --- .../src/androidTest/AndroidManifest.xml | 42 + .../playbacktests/gts/DashTest.java | 1109 +++++++++++++++++ 2 files changed, 1151 insertions(+) create mode 100644 playbacktests/src/androidTest/AndroidManifest.xml create mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java diff --git a/playbacktests/src/androidTest/AndroidManifest.xml b/playbacktests/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..2f7bbe6d7c --- /dev/null +++ b/playbacktests/src/androidTest/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 0000000000..5752058c4e --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -0,0 +1,1109 @@ +/* + * 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.playbacktests.gts; + +import android.annotation.TargetApi; +import android.media.MediaDrm; +import android.media.MediaDrm.MediaDrmStateException; +import android.media.UnsupportedSchemeException; +import android.net.Uri; +import android.test.ActivityInstrumentationTestCase2; +import android.util.Log; +import android.util.Pair; +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.DrmSession.DrmSessionException; +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.OfflineLicenseHelper; +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.io.IOException; +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}. + */ +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) + .delay(10000).seek(30000).seek(31000).seek(32000).seek(33000).seek(34000) + .delay(1000).pause().delay(1000).play() + .delay(1000).pause().seek(120000).delay(1000).play() + .build(); + private static final ActionSchedule RENDERER_DISABLING_SCHEDULE = new ActionSchedule.Builder(TAG) + // Wait 10 seconds, disable the video renderer, wait another 10 seconds and enable it again. + .delay(10000).disableRenderer(VIDEO_RENDERER_INDEX) + .delay(10000).enableRenderer(VIDEO_RENDERER_INDEX) + // Ditto for the audio renderer. + .delay(10000).disableRenderer(AUDIO_RENDERER_INDEX) + .delay(10000).enableRenderer(AUDIO_RENDERER_INDEX) + // Wait 10 seconds, then disable and enable the video renderer 5 times in quick succession. + .delay(10000).disableRenderer(VIDEO_RENDERER_INDEX) + .enableRenderer(VIDEO_RENDERER_INDEX) + .disableRenderer(VIDEO_RENDERER_INDEX) + .enableRenderer(VIDEO_RENDERER_INDEX) + .disableRenderer(VIDEO_RENDERER_INDEX) + .enableRenderer(VIDEO_RENDERER_INDEX) + .disableRenderer(VIDEO_RENDERER_INDEX) + .enableRenderer(VIDEO_RENDERER_INDEX) + .disableRenderer(VIDEO_RENDERER_INDEX) + .enableRenderer(VIDEO_RENDERER_INDEX) + // Ditto for the audio renderer. + .delay(10000).disableRenderer(AUDIO_RENDERER_INDEX) + .enableRenderer(AUDIO_RENDERER_INDEX) + .disableRenderer(AUDIO_RENDERER_INDEX) + .enableRenderer(AUDIO_RENDERER_INDEX) + .disableRenderer(AUDIO_RENDERER_INDEX) + .enableRenderer(AUDIO_RENDERER_INDEX) + .disableRenderer(AUDIO_RENDERER_INDEX) + .enableRenderer(AUDIO_RENDERER_INDEX) + .disableRenderer(AUDIO_RENDERER_INDEX) + .enableRenderer(AUDIO_RENDERER_INDEX) + .delay(10000).seek(120000) + .build(); + + public DashTest() { + super(HostActivity.class); + } + + // H264 CDD. + + public void testH264Fixed() { + if (Util.SDK_INT < 16) { + // Pass. + return; + } + String streamName = "test_h264_fixed"; + testDashPlayback(getActivity(), streamName, H264_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, + MimeTypes.VIDEO_H264, false, H264_CDD_FIXED); + } + + public void testH264Adaptive() throws DecoderQueryException { + if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { + // Pass. + return; + } + String streamName = "test_h264_adaptive"; + testDashPlayback(getActivity(), streamName, H264_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, + MimeTypes.VIDEO_H264, ALLOW_ADDITIONAL_VIDEO_FORMATS, H264_CDD_ADAPTIVE); + } + + public void testH264AdaptiveWithSeeking() throws DecoderQueryException { + if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { + // Pass. + return; + } + String streamName = "test_h264_adaptive_with_seeking"; + testDashPlayback(getActivity(), streamName, SEEKING_SCHEDULE, false, H264_MANIFEST, + AAC_AUDIO_REPRESENTATION_ID, false, MimeTypes.VIDEO_H264, ALLOW_ADDITIONAL_VIDEO_FORMATS, + H264_CDD_ADAPTIVE); + } + + public void testH264AdaptiveWithRendererDisabling() throws DecoderQueryException { + if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { + // Pass. + return; + } + String streamName = "test_h264_adaptive_with_renderer_disabling"; + testDashPlayback(getActivity(), streamName, RENDERER_DISABLING_SCHEDULE, false, H264_MANIFEST, + AAC_AUDIO_REPRESENTATION_ID, false, MimeTypes.VIDEO_H264, ALLOW_ADDITIONAL_VIDEO_FORMATS, + H264_CDD_ADAPTIVE); + } + + // H265 CDD. + + public void testH265Fixed() { + if (Util.SDK_INT < 23) { + // Pass. + return; + } + String streamName = "test_h265_fixed"; + testDashPlayback(getActivity(), streamName, H265_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, + MimeTypes.VIDEO_H265, false, H265_CDD_FIXED); + } + + public void testH265Adaptive() throws DecoderQueryException { + if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { + // Pass. + return; + } + String streamName = "test_h265_adaptive"; + testDashPlayback(getActivity(), streamName, H265_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, + MimeTypes.VIDEO_H265, ALLOW_ADDITIONAL_VIDEO_FORMATS, H265_CDD_ADAPTIVE); + } + + public void testH265AdaptiveWithSeeking() throws DecoderQueryException { + if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { + // Pass. + return; + } + String streamName = "test_h265_adaptive_with_seeking"; + testDashPlayback(getActivity(), streamName, SEEKING_SCHEDULE, false, H265_MANIFEST, + AAC_AUDIO_REPRESENTATION_ID, false, MimeTypes.VIDEO_H265, ALLOW_ADDITIONAL_VIDEO_FORMATS, + H265_CDD_ADAPTIVE); + } + + public void testH265AdaptiveWithRendererDisabling() throws DecoderQueryException { + if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { + // Pass. + return; + } + String streamName = "test_h265_adaptive_with_renderer_disabling"; + testDashPlayback(getActivity(), streamName, RENDERER_DISABLING_SCHEDULE, false, + H265_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, false, MimeTypes.VIDEO_H265, + ALLOW_ADDITIONAL_VIDEO_FORMATS, H265_CDD_ADAPTIVE); + } + + // VP9 (CDD). + + public void testVp9Fixed360p() { + if (Util.SDK_INT < 23) { + // Pass. + return; + } + String streamName = "test_vp9_fixed_360p"; + testDashPlayback(getActivity(), streamName, VP9_MANIFEST, VORBIS_AUDIO_REPRESENTATION_ID, false, + MimeTypes.VIDEO_VP9, false, VP9_CDD_FIXED); + } + + public void testVp9Adaptive() throws DecoderQueryException { + if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { + // Pass. + return; + } + String streamName = "test_vp9_adaptive"; + testDashPlayback(getActivity(), streamName, VP9_MANIFEST, VORBIS_AUDIO_REPRESENTATION_ID, false, + MimeTypes.VIDEO_VP9, ALLOW_ADDITIONAL_VIDEO_FORMATS, VP9_CDD_ADAPTIVE); + } + + public void testVp9AdaptiveWithSeeking() throws DecoderQueryException { + if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { + // Pass. + return; + } + String streamName = "test_vp9_adaptive_with_seeking"; + testDashPlayback(getActivity(), streamName, SEEKING_SCHEDULE, false, VP9_MANIFEST, + VORBIS_AUDIO_REPRESENTATION_ID, false, MimeTypes.VIDEO_VP9, ALLOW_ADDITIONAL_VIDEO_FORMATS, + VP9_CDD_ADAPTIVE); + } + + public void testVp9AdaptiveWithRendererDisabling() throws DecoderQueryException { + if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { + // Pass. + return; + } + String streamName = "test_vp9_adaptive_with_renderer_disabling"; + testDashPlayback(getActivity(), streamName, RENDERER_DISABLING_SCHEDULE, false, + VP9_MANIFEST, VORBIS_AUDIO_REPRESENTATION_ID, false, MimeTypes.VIDEO_VP9, + ALLOW_ADDITIONAL_VIDEO_FORMATS, VP9_CDD_ADAPTIVE); + } + + // H264: Other frame-rates for output buffer count assertions. + + // 23.976 fps. + public void test23FpsH264Fixed() { + if (Util.SDK_INT < 23) { + // Pass. + return; + } + String streamName = "test_23fps_h264_fixed"; + testDashPlayback(getActivity(), streamName, H264_23_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, + false, MimeTypes.VIDEO_H264, false, H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID); + } + + // 24 fps. + public void test24FpsH264Fixed() { + if (Util.SDK_INT < 23) { + // Pass. + return; + } + String streamName = "test_24fps_h264_fixed"; + testDashPlayback(getActivity(), streamName, H264_24_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, + false, MimeTypes.VIDEO_H264, false, H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID); + } + + // 29.97 fps. + public void test29FpsH264Fixed() { + if (Util.SDK_INT < 23) { + // Pass. + return; + } + String streamName = "test_29fps_h264_fixed"; + testDashPlayback(getActivity(), streamName, H264_29_MANIFEST, AAC_AUDIO_REPRESENTATION_ID, + false, MimeTypes.VIDEO_H264, false, H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID); + } + + // Widevine encrypted media tests. + // H264 CDD. + + public void testWidevineH264Fixed() throws DecoderQueryException { + if (Util.SDK_INT < 18) { + // Pass. + return; + } + String streamName = "test_widevine_h264_fixed"; + testDashPlayback(getActivity(), streamName, WIDEVINE_H264_MANIFEST_PREFIX, + WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H264, false, + WIDEVINE_H264_CDD_FIXED); + } + + public void testWidevineH264Adaptive() throws DecoderQueryException { + if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { + // Pass. + return; + } + String streamName = "test_widevine_h264_adaptive"; + testDashPlayback(getActivity(), streamName, WIDEVINE_H264_MANIFEST_PREFIX, + WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H264, + ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_H264_CDD_ADAPTIVE); + } + + public void testWidevineH264AdaptiveWithSeeking() throws DecoderQueryException { + if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { + // Pass. + return; + } + String streamName = "test_widevine_h264_adaptive_with_seeking"; + testDashPlayback(getActivity(), streamName, SEEKING_SCHEDULE, false, + WIDEVINE_H264_MANIFEST_PREFIX, WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, + MimeTypes.VIDEO_H264, ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_H264_CDD_ADAPTIVE); + } + + public void testWidevineH264AdaptiveWithRendererDisabling() throws DecoderQueryException { + if (Util.SDK_INT < 18 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { + // Pass. + return; + } + String streamName = "test_widevine_h264_adaptive_with_renderer_disabling"; + testDashPlayback(getActivity(), streamName, RENDERER_DISABLING_SCHEDULE, false, + WIDEVINE_H264_MANIFEST_PREFIX, WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, + MimeTypes.VIDEO_H264, ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_H264_CDD_ADAPTIVE); + } + + // H265 CDD. + + public void testWidevineH265Fixed() throws DecoderQueryException { + if (Util.SDK_INT < 23) { + // Pass. + return; + } + String streamName = "test_widevine_h265_fixed"; + testDashPlayback(getActivity(), streamName, WIDEVINE_H265_MANIFEST_PREFIX, + WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H265, false, + WIDEVINE_H265_CDD_FIXED); + } + + public void testWidevineH265Adaptive() throws DecoderQueryException { + if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { + // Pass. + return; + } + String streamName = "test_widevine_h265_adaptive"; + testDashPlayback(getActivity(), streamName, WIDEVINE_H265_MANIFEST_PREFIX, + WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H265, + ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_H265_CDD_ADAPTIVE); + } + + public void testWidevineH265AdaptiveWithSeeking() throws DecoderQueryException { + if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { + // Pass. + return; + } + String streamName = "test_widevine_h265_adaptive_with_seeking"; + testDashPlayback(getActivity(), streamName, SEEKING_SCHEDULE, false, + WIDEVINE_H265_MANIFEST_PREFIX, WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, + MimeTypes.VIDEO_H265, ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_H265_CDD_ADAPTIVE); + } + + public void testWidevineH265AdaptiveWithRendererDisabling() throws DecoderQueryException { + if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H265)) { + // Pass. + return; + } + String streamName = "test_widevine_h265_adaptive_with_renderer_disabling"; + testDashPlayback(getActivity(), streamName, RENDERER_DISABLING_SCHEDULE, false, + WIDEVINE_H265_MANIFEST_PREFIX, WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, + MimeTypes.VIDEO_H265, ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_H265_CDD_ADAPTIVE); + } + + // VP9 (CDD). + + public void testWidevineVp9Fixed360p() throws DecoderQueryException { + if (Util.SDK_INT < 23) { + // Pass. + return; + } + String streamName = "test_widevine_vp9_fixed_360p"; + testDashPlayback(getActivity(), streamName, WIDEVINE_VP9_MANIFEST_PREFIX, + WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_VP9, false, + WIDEVINE_VP9_CDD_FIXED); + } + + public void testWidevineVp9Adaptive() throws DecoderQueryException { + if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { + // Pass. + return; + } + String streamName = "test_widevine_vp9_adaptive"; + testDashPlayback(getActivity(), streamName, WIDEVINE_VP9_MANIFEST_PREFIX, + WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_VP9, + ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_VP9_CDD_ADAPTIVE); + } + + public void testWidevineVp9AdaptiveWithSeeking() throws DecoderQueryException { + if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { + // Pass. + return; + } + String streamName = "test_widevine_vp9_adaptive_with_seeking"; + testDashPlayback(getActivity(), streamName, SEEKING_SCHEDULE, false, + WIDEVINE_VP9_MANIFEST_PREFIX, WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID, true, + MimeTypes.VIDEO_VP9, ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_VP9_CDD_ADAPTIVE); + } + + public void testWidevineVp9AdaptiveWithRendererDisabling() throws DecoderQueryException { + if (Util.SDK_INT < 24 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_VP9)) { + // Pass. + return; + } + String streamName = "test_widevine_vp9_adaptive_with_renderer_disabling"; + testDashPlayback(getActivity(), streamName, RENDERER_DISABLING_SCHEDULE, false, + WIDEVINE_VP9_MANIFEST_PREFIX, WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID, true, + MimeTypes.VIDEO_VP9, ALLOW_ADDITIONAL_VIDEO_FORMATS, WIDEVINE_VP9_CDD_ADAPTIVE); + } + + // H264: Other frame-rates for output buffer count assertions. + + // 23.976 fps. + public void testWidevine23FpsH264Fixed() throws DecoderQueryException { + if (Util.SDK_INT < 23) { + // Pass. + return; + } + String streamName = "test_widevine_23fps_h264_fixed"; + testDashPlayback(getActivity(), streamName, WIDEVINE_H264_23_MANIFEST_PREFIX, + WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H264, false, + WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID); + } + + // 24 fps. + public void testWidevine24FpsH264Fixed() throws DecoderQueryException { + if (Util.SDK_INT < 23) { + // Pass. + return; + } + String streamName = "test_widevine_24fps_h264_fixed"; + testDashPlayback(getActivity(), streamName, WIDEVINE_H264_24_MANIFEST_PREFIX, + WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H264, false, + WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID); + } + + // 29.97 fps. + public void testWidevine29FpsH264Fixed() throws DecoderQueryException { + if (Util.SDK_INT < 23) { + // Pass. + return; + } + String streamName = "test_widevine_29fps_h264_fixed"; + testDashPlayback(getActivity(), streamName, WIDEVINE_H264_29_MANIFEST_PREFIX, + WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, true, MimeTypes.VIDEO_H264, false, + WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID); + } + + // Offline license tests + + public void testWidevineOfflineLicense() throws Exception { + if (Util.SDK_INT < 22) { + // Pass. + return; + } + String streamName = "test_widevine_h264_fixed_offline"; + DashHostedTestEncParameters parameters = newDashHostedTestEncParameters( + WIDEVINE_H264_MANIFEST_PREFIX, true, MimeTypes.VIDEO_H264); + TestOfflineLicenseHelper helper = new TestOfflineLicenseHelper(parameters); + try { + byte[] keySetId = helper.downloadLicense(); + testDashPlayback(getActivity(), streamName, null, true, parameters, + WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, false, keySetId, WIDEVINE_H264_CDD_FIXED); + helper.renewLicense(); + } finally { + helper.releaseResources(); + } + } + + public void testWidevineOfflineReleasedLicense() throws Throwable { + if (Util.SDK_INT < 22) { + // Pass. + return; + } + String streamName = "test_widevine_h264_fixed_offline"; + DashHostedTestEncParameters parameters = newDashHostedTestEncParameters( + WIDEVINE_H264_MANIFEST_PREFIX, true, MimeTypes.VIDEO_H264); + TestOfflineLicenseHelper helper = new TestOfflineLicenseHelper(parameters); + try { + byte[] keySetId = helper.downloadLicense(); + helper.releaseLicense(); // keySetId no longer valid. + try { + testDashPlayback(getActivity(), streamName, null, true, parameters, + WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, false, keySetId, WIDEVINE_H264_CDD_FIXED); + 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; + } + } + } finally { + helper.releaseResources(); + } + } + + public void testWidevineOfflineExpiredLicense() throws Exception { + if (Util.SDK_INT < 22) { + // Pass. + return; + } + String streamName = "test_widevine_h264_fixed_offline"; + DashHostedTestEncParameters parameters = newDashHostedTestEncParameters( + WIDEVINE_H264_MANIFEST_PREFIX, true, MimeTypes.VIDEO_H264); + TestOfflineLicenseHelper helper = new TestOfflineLicenseHelper(parameters); + try { + byte[] keySetId = helper.downloadLicense(); + + // Wait until the license expires + long licenseDuration = helper.getLicenseDurationRemainingSec().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 = helper.getLicenseDurationRemainingSec().first; + assertTrue("License duration should be decreasing.", previousDuration > licenseDuration); + } + + // DefaultDrmSessionManager should renew the license and stream play fine + testDashPlayback(getActivity(), streamName, null, true, parameters, + WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, false, keySetId, WIDEVINE_H264_CDD_FIXED); + } finally { + helper.releaseResources(); + } + } + + public void testWidevineOfflineLicenseExpiresOnPause() throws Exception { + if (Util.SDK_INT < 22) { + // Pass. + return; + } + String streamName = "test_widevine_h264_fixed_offline"; + DashHostedTestEncParameters parameters = newDashHostedTestEncParameters( + WIDEVINE_H264_MANIFEST_PREFIX, true, MimeTypes.VIDEO_H264); + TestOfflineLicenseHelper helper = new TestOfflineLicenseHelper(parameters); + try { + byte[] keySetId = helper.downloadLicense(); + // During playback pause until the license expires then continue playback + Pair licenseDurationRemainingSec = helper.getLicenseDurationRemainingSec(); + 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 + testDashPlayback(getActivity(), streamName, schedule, true, parameters, + WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, false, keySetId, WIDEVINE_H264_CDD_FIXED); + } finally { + helper.releaseResources(); + } + } + + // Internal. + + private void testDashPlayback(HostActivity activity, String streamName, String manifestFileName, + String audioFormat, boolean isWidevineEncrypted, String videoMimeType, + boolean canIncludeAdditionalVideoFormats, String... videoFormats) { + testDashPlayback(activity, streamName, null, true, manifestFileName, audioFormat, + isWidevineEncrypted, videoMimeType, canIncludeAdditionalVideoFormats, videoFormats); + } + + private void testDashPlayback(HostActivity activity, String streamName, + ActionSchedule actionSchedule, boolean fullPlaybackNoSeeking, String manifestFileName, + String audioFormat, boolean isWidevineEncrypted, String videoMimeType, + boolean canIncludeAdditionalVideoFormats, String... videoFormats) { + testDashPlayback(activity, streamName, actionSchedule, fullPlaybackNoSeeking, + newDashHostedTestEncParameters(manifestFileName, isWidevineEncrypted, videoMimeType), + audioFormat, canIncludeAdditionalVideoFormats, null, videoFormats); + } + + private void testDashPlayback(HostActivity activity, String streamName, + ActionSchedule actionSchedule, boolean fullPlaybackNoSeeking, + DashHostedTestEncParameters parameters, String audioFormat, + boolean canIncludeAdditionalVideoFormats, byte[] offlineLicenseKeySetId, + String... videoFormats) { + MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(getInstrumentation(), TAG, + REPORT_NAME, REPORT_OBJECT_NAME); + DashHostedTest test = new DashHostedTest(streamName, metricsLogger, fullPlaybackNoSeeking, + audioFormat, canIncludeAdditionalVideoFormats, false, actionSchedule, parameters, + offlineLicenseKeySetId, videoFormats); + 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) { + metricsLogger = MetricsLogger.Factory.createDefault(getInstrumentation(), TAG, REPORT_NAME, + REPORT_OBJECT_NAME); + test = new DashHostedTest(streamName, metricsLogger, fullPlaybackNoSeeking, audioFormat, + false, true, actionSchedule, parameters, offlineLicenseKeySetId, videoFormats); + activity.runTest(test, TEST_TIMEOUT_MS); + } + } + + private static DashHostedTestEncParameters newDashHostedTestEncParameters(String manifestFileName, + boolean isWidevineEncrypted, String videoMimeType) { + String manifestPath = MANIFEST_URL_PREFIX + manifestFileName; + return new DashHostedTestEncParameters(manifestPath, isWidevineEncrypted, videoMimeType); + } + + private static boolean shouldSkipAdaptiveTest(String mimeType) throws DecoderQueryException { + MediaCodecInfo decoderInfo = MediaCodecUtil.getDecoderInfo(mimeType, false); + assertNotNull(decoderInfo); + if (decoderInfo.adaptive) { + return false; + } + assertTrue(Util.SDK_INT < 21); + return true; + } + + private static class DashHostedTestEncParameters { + + public final String manifestUrl; + public final boolean useL1Widevine; + public final String widevineLicenseUrl; + public final boolean isWidevineEncrypted; + + public DashHostedTestEncParameters(String manifestUrl, boolean isWidevineEncrypted, + String videoMimeType) { + this.isWidevineEncrypted = isWidevineEncrypted; + if (!isWidevineEncrypted) { + this.manifestUrl = manifestUrl; + this.useL1Widevine = true; + this.widevineLicenseUrl = null; + } else { + this.useL1Widevine = isL1WidevineAvailable(videoMimeType); + this.widevineLicenseUrl = WIDEVINE_LICENSE_URL + (useL1Widevine + ? WIDEVINE_HW_SECURE_DECODE_CONTENT_ID : WIDEVINE_SW_CRYPTO_CONTENT_ID); + this.manifestUrl = + manifestUrl + (useL1Widevine ? WIDEVINE_L1_SUFFIX : WIDEVINE_L3_SUFFIX); + } + } + + @TargetApi(18) + @SuppressWarnings("ResourceType") + private 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 (DecoderQueryException | UnsupportedSchemeException e) { + throw new IllegalStateException(e); + } + } + + } + + private static class TestOfflineLicenseHelper { + + private final DashHostedTestEncParameters parameters; + private final OfflineLicenseHelper offlineLicenseHelper; + private final DefaultHttpDataSourceFactory httpDataSourceFactory; + private byte[] offlineLicenseKeySetId; + + public TestOfflineLicenseHelper(DashHostedTestEncParameters parameters) + throws UnsupportedDrmException { + this.parameters = parameters; + httpDataSourceFactory = new DefaultHttpDataSourceFactory("ExoPlayerPlaybackTests"); + offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance( + parameters.widevineLicenseUrl, httpDataSourceFactory); + } + + public byte[] downloadLicense() throws InterruptedException, DrmSessionException, IOException { + assertNull(offlineLicenseKeySetId); + offlineLicenseKeySetId = offlineLicenseHelper + .download(httpDataSourceFactory.createDataSource(), parameters.manifestUrl); + assertNotNull(offlineLicenseKeySetId); + assertTrue(offlineLicenseKeySetId.length > 0); + return offlineLicenseKeySetId; + } + + public void renewLicense() throws DrmSessionException { + assertNotNull(offlineLicenseKeySetId); + offlineLicenseKeySetId = offlineLicenseHelper.renew(offlineLicenseKeySetId); + assertNotNull(offlineLicenseKeySetId); + } + + public void releaseLicense() throws DrmSessionException { + assertNotNull(offlineLicenseKeySetId); + offlineLicenseHelper.release(offlineLicenseKeySetId); + offlineLicenseKeySetId = null; + } + + public Pair getLicenseDurationRemainingSec() throws DrmSessionException { + return offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId); + } + + public void releaseResources() throws DrmSessionException { + if (offlineLicenseKeySetId != null) { + releaseLicense(); + } + if (offlineLicenseHelper != null) { + offlineLicenseHelper.releaseResources(); + } + } + + } + + @TargetApi(16) + private static class DashHostedTest extends ExoHostedTest { + + private final String streamName; + private final MetricsLogger metricsLogger; + private final boolean fullPlaybackNoSeeking; + private final boolean isCddLimitedRetry; + private final DashTestTrackSelector trackSelector; + private final DashHostedTestEncParameters parameters; + private final byte[] offlineLicenseKeySetId; + + private boolean needsCddLimitedRetry; + + /** + * @param streamName The name of the test stream for metric logging. + * @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 parameters Encryption parameters. + * @param offlineLicenseKeySetId The key set id of the license to be used. + * @param videoFormats The video formats. + */ + public DashHostedTest(String streamName, MetricsLogger metricsLogger, + boolean fullPlaybackNoSeeking, String audioFormat, + boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry, + ActionSchedule actionSchedule, DashHostedTestEncParameters parameters, + byte[] offlineLicenseKeySetId, String... videoFormats) { + super(TAG, fullPlaybackNoSeeking); + Assertions.checkArgument(!(isCddLimitedRetry && canIncludeAdditionalVideoFormats)); + this.streamName = streamName; + this.metricsLogger = metricsLogger; + this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; + this.isCddLimitedRetry = isCddLimitedRetry; + this.parameters = parameters; + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + trackSelector = new DashTestTrackSelector(audioFormat, videoFormats, + canIncludeAdditionalVideoFormats); + if (actionSchedule != null) { + setSchedule(actionSchedule); + } + } + + @Override + protected MappingTrackSelector buildTrackSelector(HostActivity host, + BandwidthMeter bandwidthMeter) { + return trackSelector; + } + + @Override + protected final DefaultDrmSessionManager buildDrmSessionManager( + final String userAgent) { + DefaultDrmSessionManager drmSessionManager = null; + if (parameters.isWidevineEncrypted) { + try { + MediaDrmCallback drmCallback = new HttpMediaDrmCallback(parameters.widevineLicenseUrl, + new DefaultHttpDataSourceFactory(userAgent)); + drmSessionManager = DefaultDrmSessionManager.newWidevineInstance(drmCallback, null, + null, null); + if (!parameters.useL1Widevine) { + drmSessionManager.setPropertyString(SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); + } + if (offlineLicenseKeySetId != null) { + drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, + offlineLicenseKeySetId); + } + } catch (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); + Uri manifestUri = Uri.parse(parameters.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; + } + + } + +} From f7b2452d46365f092f83102a543d23609d85b0fc Mon Sep 17 00:00:00 2001 From: Devin Tuchsen Date: Sun, 22 Jan 2017 13:41:29 -0600 Subject: [PATCH 133/142] Reference ALAC sample rate bug --- .../com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 2 ++ 1 file changed, 2 insertions(+) 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 27f329fbbf..4c9eaa49c4 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 @@ -89,6 +89,8 @@ import java.util.List; if (!hasOutputFormat) { channelCount = ffmpegGetChannelCount(nativeContext); if ("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(); From 6becba8c42ae87b715b822a01781eed6301ca2fb Mon Sep 17 00:00:00 2001 From: Devin Tuchsen Date: Sat, 28 Jan 2017 15:15:41 -0600 Subject: [PATCH 134/142] Only use ALAC workaround if sample rate is 0 This prevents the workaround from occuring once FFmpeg has the bug patched. --- .../google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 4c9eaa49c4..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 @@ -88,14 +88,13 @@ import java.util.List; } if (!hasOutputFormat) { channelCount = ffmpegGetChannelCount(nativeContext); - if ("alac".equals(codecName)) { + 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(); - } else { - sampleRate = ffmpegGetSampleRate(nativeContext); } hasOutputFormat = true; } From c49d142981ae1498840d6cd2b4f5be4461f037a3 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 26 Jan 2017 08:55:42 -0800 Subject: [PATCH 135/142] Prevent old playlist snapshots from being used on live HLS streams This aims to replace InvalidCodeResponse's with BLWE's caused by trying to load chunks that have been removed from the server. Issue:#2344 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145679171 --- .../exoplayer2/source/hls/HlsChunkSource.java | 22 +++++++------ .../hls/playlist/HlsPlaylistTracker.java | 33 +++++++++++++++++-- 2 files changed, 43 insertions(+), 12 deletions(-) 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 c2a345ace6..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 @@ -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/playlist/HlsPlaylistTracker.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 92e2480da7..95784092a9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -166,8 +166,24 @@ public final class HlsPlaylistTracker implements Loader.Callback mediaPlaylistLoadable; private HlsMediaPlaylist playlistSnapshot; + private long lastSnapshotLoadMs; private long lastSnapshotAccessTimeMs; private long blacklistUntilMs; @@ -429,6 +446,17 @@ public final class HlsPlaylistTracker implements Loader.Callback currentTimeMs; + } + public void release() { mediaPlaylistLoader.release(); } @@ -488,6 +516,7 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Mon, 30 Jan 2017 05:38:43 -0800 Subject: [PATCH 136/142] Resume from playback position by default when media source changes Issue:#2369 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145982198 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7ee8567f4a80b2ec9018cb0761fca41ac9ff9c9e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 30 Jan 2017 07:00:28 -0800 Subject: [PATCH 137/142] Fix demo app to avoid seeking if resume position is clear This fixed the resume live window issue by modifying the demo app. Issue:#2344 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145987470 --- .../google/android/exoplayer2/demo/PlayerActivity.java | 9 ++++++--- .../com/google/android/exoplayer2/SimpleExoPlayer.java | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 9add658d30..66ad2aebf1 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -317,8 +317,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); - player.seekTo(resumeWindow, resumePosition); - player.prepare(mediaSource, false, false); + boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; + if (haveResumePosition) { + player.seekTo(resumeWindow, resumePosition); + } + player.prepare(mediaSource, !haveResumePosition, !haveResumePosition); playerNeedsSource = false; updateButtonVisibilities(); } @@ -377,7 +380,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } private void clearResumePosition() { - resumeWindow = 0; + resumeWindow = C.INDEX_UNSET; resumePosition = C.TIME_UNSET; } 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..298e528246 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -479,8 +479,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 From e6bbd397d55f46a95bfdacdc7051ee6ac992ff4d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 30 Jan 2017 07:47:31 -0800 Subject: [PATCH 138/142] Add support for HLS's #EXT-X-PLAYLIST-TYPE Issue:#805 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145991145 --- .../playlist/HlsMediaPlaylistParserTest.java | 2 ++ .../source/hls/playlist/HlsMediaPlaylist.java | 36 +++++++++++++------ .../hls/playlist/HlsPlaylistParser.java | 17 +++++++-- .../hls/playlist/HlsPlaylistTracker.java | 3 +- 4 files changed, 45 insertions(+), 13 deletions(-) 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/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 0cd861c369..a211417501 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 @@ -43,6 +43,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser currentTimeMs; } From 615b707b1680c09185b23c3045413c629bf6c4df Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 30 Jan 2017 08:20:29 -0800 Subject: [PATCH 139/142] Bump version + update release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145994313 --- RELEASENOTES.md | 80 +++++++++++++++++-- build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 +- .../exoplayer2/ExoPlayerLibraryInfo.java | 4 +- 4 files changed, 79 insertions(+), 11 deletions(-) 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 358b8f1404..b10a17de81 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ allprojects { 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 1a7848eb5c..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"> 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} From 8ddfc12f05a2a0d0d5547dcd77a5b5cb0a572b6c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 30 Jan 2017 09:00:28 -0800 Subject: [PATCH 140/142] Avoid resetting the tracks on BLWE media source reinitialization ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=145998158 --- .../java/com/google/android/exoplayer2/demo/PlayerActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 66ad2aebf1..0badb07cc5 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -321,7 +321,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay if (haveResumePosition) { player.seekTo(resumeWindow, resumePosition); } - player.prepare(mediaSource, !haveResumePosition, !haveResumePosition); + player.prepare(mediaSource, !haveResumePosition, false); playerNeedsSource = false; updateButtonVisibilities(); } From 4b8a6572fd3dac9ead1b7b016fc07893fb065f0d Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 30 Jan 2017 09:32:57 -0800 Subject: [PATCH 141/142] Fix resume position if user seeks whilst in error state ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146001900 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 0badb07cc5..bbfadf34af 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -425,7 +425,12 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay @Override public void onPositionDiscontinuity() { - // Do nothing. + if (playerNeedsSource) { + // This will only occur if the user has performed a seek whilst in the error state. Update the + // resume position so that if the user then retries, playback will resume from the position to + // which they seeked. + updateResumePosition(); + } } @Override From 2e7f9fb6cbc12f0ac8b09e289cddc9bf3d9d4713 Mon Sep 17 00:00:00 2001 From: cblay Date: Mon, 30 Jan 2017 11:04:29 -0800 Subject: [PATCH 142/142] Don't setContentLength() in CacheDataSource if the current request ignores cache. Otherwise an empty cache span is created. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146014081 --- .../upstream/cache/CacheDataSourceTest.java | 16 ++++++++++++++-- .../upstream/cache/CacheDataSource.java | 10 +++++----- 2 files changed, 19 insertions(+), 7 deletions(-) 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..067cfe4fcd 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 @@ -119,6 +119,13 @@ 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()); + } + private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength) throws IOException { // Read all data from upstream and cache @@ -171,6 +178,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private CacheDataSource createCacheDataSource(boolean setReadException, boolean simulateUnknownLength) { + return createCacheDataSource(setReadException, simulateUnknownLength, + CacheDataSource.FLAG_BLOCK_ON_CACHE); + } + + private CacheDataSource createCacheDataSource(boolean setReadException, + boolean simulateUnknownLength, @CacheDataSource.Flags int flags) { Builder builder = new Builder(); if (setReadException) { builder.appendReadError(new IOException("Shouldn't read from upstream")); @@ -178,8 +191,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { 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); + return new CacheDataSource(simpleCache, upstream, flags, MAX_CACHE_FILE_SIZE); } } 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..9b29984d06 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 @@ -330,16 +330,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 {