From 0d679dad0d2fb8b5749f342b2cacaf961bbd6832 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 19 Oct 2016 07:59:35 -0700 Subject: [PATCH 001/106] Fix use of API level 19 method Issue: #1965 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136595233 --- .../android/exoplayer2/ui/PlaybackControlView.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 3823f1760e..096e67ec01 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -75,6 +75,7 @@ public class PlaybackControlView extends FrameLayout { private ExoPlayer player; private VisibilityListener visibilityListener; + private boolean isAttachedToWindow; private boolean dragging; private int rewindMs; private int fastForwardMs; @@ -264,7 +265,7 @@ public class PlaybackControlView extends FrameLayout { removeCallbacks(hideAction); if (showTimeoutMs > 0) { hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs; - if (isAttachedToWindow()) { + if (isAttachedToWindow) { postDelayed(hideAction, showTimeoutMs); } } else { @@ -279,7 +280,7 @@ public class PlaybackControlView extends FrameLayout { } private void updatePlayPauseButton() { - if (!isVisible() || !isAttachedToWindow()) { + if (!isVisible() || !isAttachedToWindow) { return; } boolean playing = player != null && player.getPlayWhenReady(); @@ -291,7 +292,7 @@ public class PlaybackControlView extends FrameLayout { } private void updateNavigation() { - if (!isVisible() || !isAttachedToWindow()) { + if (!isVisible() || !isAttachedToWindow) { return; } Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null; @@ -315,7 +316,7 @@ public class PlaybackControlView extends FrameLayout { } private void updateProgress() { - if (!isVisible() || !isAttachedToWindow()) { + if (!isVisible() || !isAttachedToWindow) { return; } long duration = player == null ? 0 : player.getDuration(); @@ -426,6 +427,7 @@ public class PlaybackControlView extends FrameLayout { @Override public void onAttachedToWindow() { super.onAttachedToWindow(); + isAttachedToWindow = true; if (hideAtMs != C.TIME_UNSET) { long delayMs = hideAtMs - SystemClock.uptimeMillis(); if (delayMs <= 0) { @@ -440,6 +442,7 @@ public class PlaybackControlView extends FrameLayout { @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); + isAttachedToWindow = false; removeCallbacks(updateProgressAction); removeCallbacks(hideAction); } From 75b00753403487ed30baaa10d874659171a36fee Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 19 Oct 2016 08:18:48 -0700 Subject: [PATCH 002/106] Add explicit TargetApi annotation to remove lint error ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136597149 --- .../google/android/exoplayer2/ui/PlaybackControlView.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 096e67ec01..89c778d072 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ui; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.os.SystemClock; @@ -351,13 +352,18 @@ public class PlaybackControlView extends FrameLayout { private void setButtonEnabled(boolean enabled, View view) { view.setEnabled(enabled); if (Util.SDK_INT >= 11) { - view.setAlpha(enabled ? 1f : 0.3f); + setViewAlphaV11(view, enabled ? 1f : 0.3f); view.setVisibility(VISIBLE); } else { view.setVisibility(enabled ? VISIBLE : INVISIBLE); } } + @TargetApi(11) + private void setViewAlphaV11(View view, float alpha) { + view.setAlpha(alpha); + } + private String stringForTime(long timeMs) { if (timeMs == C.TIME_UNSET) { timeMs = 0; From 8e0e0ca0dd56ecc5d32f9508014a001d7cb94c3c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 19 Oct 2016 09:58:41 -0700 Subject: [PATCH 003/106] Fix NPE when trying to play H265 in Ts files ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136607848 --- .../google/android/exoplayer2/extractor/ts/H265Reader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 6283371a19..57d7e77bb7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -44,6 +44,7 @@ import java.util.Collections; private static final int SUFFIX_SEI_NUT = 40; private TrackOutput output; + private SampleReader sampleReader; private SeiReader seiReader; // State that should not be reset on seek. @@ -56,7 +57,6 @@ import java.util.Collections; private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer prefixSei; private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed? - private final SampleReader sampleReader; private long totalBytesWritten; // Per packet state that gets reset at the start of each packet. @@ -72,7 +72,6 @@ import java.util.Collections; pps = new NalUnitTargetBuffer(PPS_NUT, 128); prefixSei = new NalUnitTargetBuffer(PREFIX_SEI_NUT, 128); suffixSei = new NalUnitTargetBuffer(SUFFIX_SEI_NUT, 128); - sampleReader = new SampleReader(output); seiWrapper = new ParsableByteArray(); } @@ -91,6 +90,7 @@ import java.util.Collections; @Override public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { output = extractorOutput.track(idGenerator.getNextId()); + sampleReader = new SampleReader(output); seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); } From c7824c4660d2958ea331d23dbcca9cc74d39ad98 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 20 Oct 2016 03:35:32 -0700 Subject: [PATCH 004/106] Bump version to r2.0.4 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136697697 --- RELEASENOTES.md | 21 ++++++++++++------- build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../exoplayer2/ExoPlayerLibraryInfo.java | 4 ++-- playbacktests/src/main/AndroidManifest.xml | 4 ++-- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9e0439dd12..ce002238ef 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,22 +1,27 @@ # Release notes # +### r2.0.4 ### + +This release contains important bug fixes. Users of earlier r2.0.x versions +should proactively update to this version. + +* Fix crash on Jellybean devices when using playback controls + ([#1965](https://github.com/google/ExoPlayer/issues/1965)). + ### r2.0.3 ### -This release contains important bug fixes. Users of r2.0.0, r2.0.1 and r2.0.2 -should proactively update to this version. - * Fixed NullPointerException in ExtractorMediaSource - ([#1914](https://github.com/google/ExoPlayer/issues/1914). + ([#1914](https://github.com/google/ExoPlayer/issues/1914)). * Fixed NullPointerException in HlsMediaPeriod - ([#1907](https://github.com/google/ExoPlayer/issues/1907). + ([#1907](https://github.com/google/ExoPlayer/issues/1907)). * Fixed memory leak in PlaybackControlView - ([#1908](https://github.com/google/ExoPlayer/issues/1908). + ([#1908](https://github.com/google/ExoPlayer/issues/1908)). * Fixed strict mode violation when using SimpleExoPlayer.setVideoPlayerTextureView(). * Fixed L3 Widevine provisioning - ([#1925](https://github.com/google/ExoPlayer/issues/1925). + ([#1925](https://github.com/google/ExoPlayer/issues/1925)). * Fixed hiding of controls with use_controller="false" - ([#1919](https://github.com/google/ExoPlayer/issues/1919). + ([#1919](https://github.com/google/ExoPlayer/issues/1919)). * Improvements to Cronet network stack extension. * Misc bug fixes. diff --git a/build.gradle b/build.gradle index c50dd31b27..8e9032be70 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ allprojects { releaseRepoName = 'exoplayer' releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.0.3' + releaseVersion = 'r2.0.4' releaseWebsite = 'https://github.com/google/ExoPlayer' } } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 7fc0ac3d9c..1f015827c9 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2004" + android:versionName="2.0.4"> 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 23e6d4d593..02c70bb0be 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.0.3"; + String VERSION = "2.0.4"; /** * 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 = 2000003; + int VERSION_INT = 2000004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/playbacktests/src/main/AndroidManifest.xml b/playbacktests/src/main/AndroidManifest.xml index 58ede793b2..6a10654af7 100644 --- a/playbacktests/src/main/AndroidManifest.xml +++ b/playbacktests/src/main/AndroidManifest.xml @@ -17,8 +17,8 @@ + android:versionCode="2004" + android:versionName="2.0.4"> From a3ca5c48b388cdcce89b016d0c635b31ec1f88e7 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Thu, 24 Nov 2016 19:28:24 +0000 Subject: [PATCH 005/106] Update ISSUE_TEMPLATE --- ISSUE_TEMPLATE | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 3667c8cc96..6e55f3dcd6 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,5 +1,7 @@ +*** PLEASE DO NOT IGNORE THIS ISSUE TEMPLATE *** + Please search the existing issues before filing a new one, including issues that -are closed. When filing a new issue please include all of the following, unless +are closed. When filing a new issue please include ALL of the following, unless you're certain that they're not useful for the particular issue being reported. - A description of the issue. From 4f2cced4da4b6c42f2da51f38516a17a415e13c3 Mon Sep 17 00:00:00 2001 From: Greg Slomin Date: Tue, 29 Nov 2016 11:36:34 -0600 Subject: [PATCH 006/106] Added support for PCM u/a-law audio in FLV containers --- .../extractor/flv/AudioTagPayloadReader.java | 37 +++++++++++++------ .../android/exoplayer2/util/MimeTypes.java | 2 + 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index b48c4881d9..5eed202c0b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor.flv; +import android.media.AudioFormat; +import android.media.AudioTrack; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -30,6 +32,8 @@ import java.util.Collections; /* package */ final class AudioTagPayloadReader extends TagPayloadReader { // Audio format + private static final int AUDIO_FORMAT_ALAW = 7; + private static final int AUDIO_FORMAT_ULAW = 8; private static final int AUDIO_FORMAT_AAC = 10; // AAC PACKET TYPE @@ -44,6 +48,7 @@ import java.util.Collections; // State variables private boolean hasParsedAudioDataHeader; private boolean hasOutputFormat; + private int audioFormat; public AudioTagPayloadReader(TrackOutput output) { super(output); @@ -58,15 +63,26 @@ import java.util.Collections; protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException { if (!hasParsedAudioDataHeader) { int header = data.readUnsignedByte(); - int audioFormat = (header >> 4) & 0x0F; + audioFormat = (header >> 4) & 0x0F; int sampleRateIndex = (header >> 2) & 0x03; + int encodingSize = header & 0x01; if (sampleRateIndex < 0 || sampleRateIndex >= AUDIO_SAMPLING_RATE_TABLE.length) { throw new UnsupportedFormatException("Invalid sample rate index: " + sampleRateIndex); } - // TODO: Add support for MP3 and PCM. - if (audioFormat != AUDIO_FORMAT_AAC) { - throw new UnsupportedFormatException("Audio format not supported: " + audioFormat); + // TODO: Add support for MP3. + if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { + + String type = (audioFormat == AUDIO_FORMAT_ALAW) ? MimeTypes.AUDIO_ALAW : MimeTypes.AUDIO_ULAW; + int encoding = (encodingSize == 1) ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; + Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE, Format.NO_VALUE, + 1, 8000, encoding, null, null, 0, null); + output.format(format); + + hasOutputFormat = true; + } else if (audioFormat != AUDIO_FORMAT_AAC ) { + throw new UnsupportedFormatException("Audio format not supported: " + audioFormat); } + hasParsedAudioDataHeader = true; } else { // Skip header if it was parsed previously. @@ -78,8 +94,9 @@ import java.util.Collections; @Override protected void parsePayload(ParsableByteArray data, long timeUs) { int packetType = data.readUnsignedByte(); - // Parse sequence header just in case it was not done before. + if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { + // Parse sequence header just in case it was not done before. byte[] audioSpecifiConfig = new byte[data.bytesLeft()]; data.readBytes(audioSpecifiConfig, 0, audioSpecifiConfig.length); Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( @@ -89,12 +106,10 @@ import java.util.Collections; Collections.singletonList(audioSpecifiConfig), null, 0, null); output.format(format); hasOutputFormat = true; - } else if (packetType == AAC_PACKET_TYPE_AAC_RAW) { - // Sample audio AAC frames - int bytesToWrite = data.bytesLeft(); - output.sampleData(data, bytesToWrite); - output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, bytesToWrite, 0, null); + } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) { + int bytes = data.bytesLeft(); + output.sampleData(data, bytes); + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, bytes, 0, null); } } - } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 4776e4d008..e08388f173 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -47,6 +47,8 @@ public final class MimeTypes { public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1"; public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2"; public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw"; + public static final String AUDIO_ALAW = BASE_TYPE_AUDIO + "/g711-alaw"; + public static final String AUDIO_ULAW = BASE_TYPE_AUDIO + "/g711-mlaw"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; From 17cd73d8ed55bacccd1f567b9bd30c882311168e Mon Sep 17 00:00:00 2001 From: ojw28 Date: Thu, 1 Dec 2016 14:54:27 +0000 Subject: [PATCH 007/106] Revert "Added support for PCM u/a-law audio in FLV containers" --- .../extractor/flv/AudioTagPayloadReader.java | 37 ++++++------------- .../android/exoplayer2/util/MimeTypes.java | 2 - 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index 5eed202c0b..b48c4881d9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.extractor.flv; -import android.media.AudioFormat; -import android.media.AudioTrack; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -32,8 +30,6 @@ import java.util.Collections; /* package */ final class AudioTagPayloadReader extends TagPayloadReader { // Audio format - private static final int AUDIO_FORMAT_ALAW = 7; - private static final int AUDIO_FORMAT_ULAW = 8; private static final int AUDIO_FORMAT_AAC = 10; // AAC PACKET TYPE @@ -48,7 +44,6 @@ import java.util.Collections; // State variables private boolean hasParsedAudioDataHeader; private boolean hasOutputFormat; - private int audioFormat; public AudioTagPayloadReader(TrackOutput output) { super(output); @@ -63,26 +58,15 @@ import java.util.Collections; protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException { if (!hasParsedAudioDataHeader) { int header = data.readUnsignedByte(); - audioFormat = (header >> 4) & 0x0F; + int audioFormat = (header >> 4) & 0x0F; int sampleRateIndex = (header >> 2) & 0x03; - int encodingSize = header & 0x01; if (sampleRateIndex < 0 || sampleRateIndex >= AUDIO_SAMPLING_RATE_TABLE.length) { throw new UnsupportedFormatException("Invalid sample rate index: " + sampleRateIndex); } - // TODO: Add support for MP3. - if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { - - String type = (audioFormat == AUDIO_FORMAT_ALAW) ? MimeTypes.AUDIO_ALAW : MimeTypes.AUDIO_ULAW; - int encoding = (encodingSize == 1) ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; - Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE, Format.NO_VALUE, - 1, 8000, encoding, null, null, 0, null); - output.format(format); - - hasOutputFormat = true; - } else if (audioFormat != AUDIO_FORMAT_AAC ) { - throw new UnsupportedFormatException("Audio format not supported: " + audioFormat); + // TODO: Add support for MP3 and PCM. + if (audioFormat != AUDIO_FORMAT_AAC) { + throw new UnsupportedFormatException("Audio format not supported: " + audioFormat); } - hasParsedAudioDataHeader = true; } else { // Skip header if it was parsed previously. @@ -94,9 +78,8 @@ import java.util.Collections; @Override protected void parsePayload(ParsableByteArray data, long timeUs) { int packetType = data.readUnsignedByte(); - + // Parse sequence header just in case it was not done before. if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { - // Parse sequence header just in case it was not done before. byte[] audioSpecifiConfig = new byte[data.bytesLeft()]; data.readBytes(audioSpecifiConfig, 0, audioSpecifiConfig.length); Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( @@ -106,10 +89,12 @@ import java.util.Collections; Collections.singletonList(audioSpecifiConfig), null, 0, null); output.format(format); hasOutputFormat = true; - } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) { - int bytes = data.bytesLeft(); - output.sampleData(data, bytes); - output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, bytes, 0, null); + } else if (packetType == AAC_PACKET_TYPE_AAC_RAW) { + // Sample audio AAC frames + int bytesToWrite = data.bytesLeft(); + output.sampleData(data, bytesToWrite); + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, bytesToWrite, 0, null); } } + } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index e08388f173..4776e4d008 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -47,8 +47,6 @@ public final class MimeTypes { public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1"; public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2"; public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw"; - public static final String AUDIO_ALAW = BASE_TYPE_AUDIO + "/g711-alaw"; - public static final String AUDIO_ULAW = BASE_TYPE_AUDIO + "/g711-mlaw"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; From e65110e1ed06da1ab0b6dbbf252f7e49e4879ce8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 14 Dec 2016 17:00:46 +0000 Subject: [PATCH 008/106] Remove ClippingMediaSource from release --- .../source/ClippingMediaPeriod.java | 247 ------------------ .../source/ClippingMediaSource.java | 185 ------------- 2 files changed, 432 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java delete mode 100644 library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java deleted file mode 100644 index c39bccda3d..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ /dev/null @@ -1,247 +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.source; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; - -/** - * Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their - * samples. - */ -/* package */ final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { - - public final MediaPeriod mediaPeriod; - private final ClippingMediaSource mediaSource; - - private MediaPeriod.Callback callback; - private long startUs; - private long endUs; - private ClippingSampleStream[] sampleStreams; - private boolean pendingInitialDiscontinuity; - - /** - * Creates a new clipping media period that provides a clipped view of the specified - * {@link MediaPeriod}'s sample streams. - * - * @param mediaPeriod The media period to clip. - * @param mediaSource The {@link ClippingMediaSource} to which this period belongs. - */ - public ClippingMediaPeriod(MediaPeriod mediaPeriod, ClippingMediaSource mediaSource) { - this.mediaPeriod = mediaPeriod; - this.mediaSource = mediaSource; - startUs = C.TIME_UNSET; - endUs = C.TIME_UNSET; - sampleStreams = new ClippingSampleStream[0]; - } - - @Override - public void prepare(MediaPeriod.Callback callback) { - this.callback = callback; - mediaPeriod.prepare(this); - } - - @Override - public void maybeThrowPrepareError() throws IOException { - mediaPeriod.maybeThrowPrepareError(); - } - - @Override - public TrackGroupArray getTrackGroups() { - return mediaPeriod.getTrackGroups(); - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - sampleStreams = new ClippingSampleStream[streams.length]; - SampleStream[] internalStreams = new SampleStream[streams.length]; - for (int i = 0; i < streams.length; i++) { - sampleStreams[i] = (ClippingSampleStream) streams[i]; - internalStreams[i] = sampleStreams[i] != null ? sampleStreams[i].stream : null; - } - long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags, - internalStreams, streamResetFlags, positionUs + startUs); - Assertions.checkState(enablePositionUs == positionUs + startUs - || (enablePositionUs >= startUs && enablePositionUs <= endUs)); - for (int i = 0; i < streams.length; i++) { - if (internalStreams[i] == null) { - sampleStreams[i] = null; - } else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) { - sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs, - pendingInitialDiscontinuity); - } - streams[i] = sampleStreams[i]; - } - return enablePositionUs - startUs; - } - - @Override - public long readDiscontinuity() { - if (pendingInitialDiscontinuity) { - for (ClippingSampleStream sampleStream : sampleStreams) { - if (sampleStream != null) { - sampleStream.clearPendingDiscontinuity(); - } - } - pendingInitialDiscontinuity = false; - // Always read an initial discontinuity, using mediaPeriod's discontinuity if set. - long discontinuityUs = readDiscontinuity(); - return discontinuityUs != C.TIME_UNSET ? discontinuityUs : 0; - } - long discontinuityUs = mediaPeriod.readDiscontinuity(); - if (discontinuityUs == C.TIME_UNSET) { - return C.TIME_UNSET; - } - Assertions.checkState(discontinuityUs >= startUs && discontinuityUs <= endUs); - return discontinuityUs - startUs; - } - - @Override - public long getBufferedPositionUs() { - long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); - if (bufferedPositionUs == C.TIME_END_OF_SOURCE || bufferedPositionUs >= endUs) { - return C.TIME_END_OF_SOURCE; - } - return Math.max(0, bufferedPositionUs - startUs); - } - - @Override - public long seekToUs(long positionUs) { - for (ClippingSampleStream sampleStream : sampleStreams) { - if (sampleStream != null) { - sampleStream.clearSentEos(); - } - } - long seekUs = mediaPeriod.seekToUs(positionUs + startUs); - Assertions.checkState(seekUs == positionUs + startUs || (seekUs >= startUs && seekUs <= endUs)); - return seekUs - startUs; - } - - @Override - public long getNextLoadPositionUs() { - long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); - if (nextLoadPositionUs == C.TIME_END_OF_SOURCE || nextLoadPositionUs >= endUs) { - return C.TIME_END_OF_SOURCE; - } - return nextLoadPositionUs - startUs; - } - - @Override - public boolean continueLoading(long positionUs) { - return mediaPeriod.continueLoading(positionUs + startUs); - } - - // MediaPeriod.Callback implementation. - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - startUs = mediaSource.getStartUs(); - endUs = mediaSource.getEndUs(); - Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET); - // If the clipping start position is non-zero, the clipping sample streams will adjust - // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer - // timestamps can be negative, because sample streams provide buffers starting at a key-frame, - // which may be before the clipping start point. When the renderer reads a buffer with a - // negative timestamp, its offset timestamp can jump backwards compared to the last timestamp - // read in the previous period. Renderer implementations may not allow this, so we signal a - // discontinuity which resets the renderers before they read the clipping sample stream. - pendingInitialDiscontinuity = startUs != 0; - callback.onPrepared(this); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); - } - - /** - * Wraps a {@link SampleStream} and clips its samples. - */ - private static final class ClippingSampleStream implements SampleStream { - - private final MediaPeriod mediaPeriod; - private final SampleStream stream; - private final long startUs; - private final long endUs; - - private boolean pendingDiscontinuity; - private boolean sentEos; - - public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs, - long endUs, boolean pendingDiscontinuity) { - this.mediaPeriod = mediaPeriod; - this.stream = stream; - this.startUs = startUs; - this.endUs = endUs; - this.pendingDiscontinuity = pendingDiscontinuity; - } - - public void clearPendingDiscontinuity() { - pendingDiscontinuity = false; - } - - public void clearSentEos() { - sentEos = false; - } - - @Override - public boolean isReady() { - return stream.isReady(); - } - - @Override - public void maybeThrowError() throws IOException { - stream.maybeThrowError(); - } - - @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - if (pendingDiscontinuity) { - return C.RESULT_NOTHING_READ; - } - if (sentEos) { - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } - int result = stream.readData(formatHolder, buffer); - // TODO: Clear gapless playback metadata if a format was read (if applicable). - if ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) - || (result == C.RESULT_NOTHING_READ - && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE)) { - buffer.clear(); - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - sentEos = true; - return C.RESULT_BUFFER_READ; - } - if (result == C.RESULT_BUFFER_READ) { - buffer.timeUs -= startUs; - } - return result; - } - - @Override - public void skipToKeyframeBefore(long timeUs) { - stream.skipToKeyframeBefore(startUs + timeUs); - } - - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java deleted file mode 100644 index e92dce8231..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ /dev/null @@ -1,185 +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.source; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; - -/** - * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end - * positions. The wrapped source may only have a single period/window and it must not be dynamic - * (live). The specified start position must correspond to a synchronization sample in the period. - */ -public final class ClippingMediaSource implements MediaSource, MediaSource.Listener { - - private final MediaSource mediaSource; - private final long startUs; - private final long endUs; - - private MediaSource.Listener sourceListener; - private ClippingTimeline clippingTimeline; - - /** - * Creates a new clipping source that wraps the specified source. - * - * @param mediaSource The single-period, non-dynamic source to wrap. - * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to - * start providing samples, in microseconds. - * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop - * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples - * from the specified start point up to the end of the source. - */ - public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) { - Assertions.checkArgument(startPositionUs >= 0); - this.mediaSource = Assertions.checkNotNull(mediaSource); - startUs = startPositionUs; - endUs = endPositionUs; - } - - /** - * Returns the start position of the clipping source's timeline in microseconds. - */ - /* package */ long getStartUs() { - return clippingTimeline.startUs; - } - - /** - * Returns the end position of the clipping source's timeline in microseconds. - */ - /* package */ long getEndUs() { - return clippingTimeline.endUs; - } - - @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - this.sourceListener = listener; - mediaSource.prepareSource(player, false, this); - } - - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - mediaSource.maybeThrowSourceInfoRefreshError(); - } - - @Override - public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { - return new ClippingMediaPeriod( - mediaSource.createPeriod(index, allocator, startUs + positionUs), this); - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); - } - - @Override - public void releaseSource() { - mediaSource.releaseSource(); - } - - // MediaSource.Listener implementation. - - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - clippingTimeline = new ClippingTimeline(timeline, startUs, endUs); - sourceListener.onSourceInfoRefreshed(clippingTimeline, manifest); - } - - /** - * Provides a clipped view of a specified timeline. - */ - private static final class ClippingTimeline extends Timeline { - - private final Timeline timeline; - private final long startUs; - private final long endUs; - - /** - * Creates a new timeline that wraps the specified timeline. - * - * @param timeline The timeline to clip. - * @param startUs The number of microseconds to clip from the start of {@code timeline}. - * @param endUs The end position in microseconds for the clipped timeline relative to the start - * of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end. - */ - public ClippingTimeline(Timeline timeline, long startUs, long endUs) { - Assertions.checkArgument(timeline.getWindowCount() == 1); - Assertions.checkArgument(timeline.getPeriodCount() == 1); - Window window = timeline.getWindow(0, new Window(), false); - Assertions.checkArgument(!window.isDynamic); - long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs; - if (window.durationUs != C.TIME_UNSET) { - Assertions.checkArgument(startUs == 0 || window.isSeekable); - Assertions.checkArgument(resolvedEndUs <= window.durationUs); - Assertions.checkArgument(startUs <= resolvedEndUs); - } - Period period = timeline.getPeriod(0, new Period()); - Assertions.checkArgument(period.getPositionInWindowUs() == 0); - this.timeline = timeline; - this.startUs = startUs; - this.endUs = resolvedEndUs; - } - - @Override - public int getWindowCount() { - return 1; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - window = timeline.getWindow(0, window, setIds, defaultPositionProjectionUs); - window.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; - if (window.defaultPositionUs != C.TIME_UNSET) { - window.defaultPositionUs = Math.max(window.defaultPositionUs, startUs); - window.defaultPositionUs = endUs == C.TIME_UNSET ? window.defaultPositionUs - : Math.min(window.defaultPositionUs, endUs); - window.defaultPositionUs -= startUs; - } - long startMs = C.usToMs(startUs); - if (window.presentationStartTimeMs != C.TIME_UNSET) { - window.presentationStartTimeMs += startMs; - } - if (window.windowStartTimeMs != C.TIME_UNSET) { - window.windowStartTimeMs += startMs; - } - return window; - } - - @Override - public int getPeriodCount() { - return 1; - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - period = timeline.getPeriod(0, period, setIds); - period.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; - return period; - } - - @Override - public int getIndexOfPeriod(Object uid) { - return timeline.getIndexOfPeriod(uid); - } - - } - -} From 47ea5c909a55261f58e0de3ebda1d633666e1d2c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 14 Dec 2016 23:01:51 +0000 Subject: [PATCH 009/106] 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 8277999b16894a5c54035d5b025d7c65bf9b35ec Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sat, 17 Dec 2016 13:39:47 -0500 Subject: [PATCH 010/106] add imageview into simpleexoplayerview to display subtitles that are image based --- .../android/exoplayer2/text/ImageCue.java | 15 +++ .../exoplayer2/ui/SimpleExoPlayerView.java | 94 +++++++++++++++++-- .../res/layout/exo_simple_player_view.xml | 6 ++ 3 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java new file mode 100644 index 0000000000..6274493780 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java @@ -0,0 +1,15 @@ +package com.google.android.exoplayer2.text; + +import android.graphics.Bitmap; + +public class ImageCue extends Cue { + + public ImageCue() { super(""); } + + public Bitmap getBitmap() { return null; } + public int getX() { return 0; } + public int getY() { return 0; } + public int getWidth() { return 0; } + public int getHeight() { return 0; } + public boolean isForcedSubtitle() { return false; } +} 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 97c564a3a6..e01956c803 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 @@ -40,6 +40,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.ImageCue; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -171,6 +172,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private final View surfaceView; private final ImageView artworkView; private final SubtitleView subtitleView; + private final ImageView subtitleImageView; private final PlaybackControlView controller; private final ComponentListener componentListener; private final FrameLayout overlayFrameLayout; @@ -178,6 +180,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private SimpleExoPlayer player; private boolean useController; private boolean useArtwork; + private boolean subtitlesEnabled = false; private int controllerShowTimeoutMs; public SimpleExoPlayerView(Context context) { @@ -253,6 +256,8 @@ public final class SimpleExoPlayerView extends FrameLayout { subtitleView.setUserDefaultTextSize(); } + subtitleImageView = (ImageView) findViewById(R.id.exo_subtitles_image); + // Playback control view. View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); if (controllerPlaceholder != null) { @@ -523,14 +528,26 @@ public final class SimpleExoPlayerView extends FrameLayout { return; } TrackSelectionArray selections = player.getCurrentTrackSelections(); + boolean quickExit = false; for (int i = 0; i < selections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { - // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in - // onRenderedFirstFrame(). - hideArtwork(); - return; + switch(player.getRendererType(i)) { + case C.TRACK_TYPE_VIDEO: + if (selections.get(i) != null) { + // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in + // onRenderedFirstFrame(). + + hideArtwork(); + quickExit = true; + } + break; + case C.TRACK_TYPE_TEXT: + if (selections.get(i) != null) + subtitlesEnabled = true; + break; } } + if (quickExit) + return; // Video disabled so the shutter must be closed. if (shutterView != null) { shutterView.setVisibility(VISIBLE); @@ -597,20 +614,85 @@ public final class SimpleExoPlayerView extends FrameLayout { private final class ComponentListener implements SimpleExoPlayer.VideoListener, TextRenderer.Output, ExoPlayer.EventListener { + + private int sourceWidth = 0; + private int sourceHeight = 0; + // TextRenderer.Output implementation @Override public void onCues(List cues) { - if (subtitleView != null) { + + boolean skipNormalCues = false; + if (subtitleImageView != null) { + + final ImageCue cue = (cues != null && !cues.isEmpty() && cues.get(0) instanceof ImageCue) ? (ImageCue) cues.get(0) : null; + skipNormalCues = (cue != null); + if (cue == null || (!subtitlesEnabled && !cue.isForcedSubtitle())) { + subtitleImageView.setImageBitmap(null); + subtitleImageView.setVisibility(View.INVISIBLE); + } + else { + handleImageCue(cue); + } + } + if (!skipNormalCues && subtitleView != null) { subtitleView.onCues(cues); } } + private void handleImageCue(ImageCue cue) { + Bitmap bitmap = cue.getBitmap(); + if (bitmap != null && surfaceView != null) { + int surfaceAnchorX = (int) surfaceView.getX(); + int surfaceAnchorY = (int) surfaceView.getY(); + int surfaceWidth = surfaceView.getWidth(); + int surfaceHeight = surfaceView.getHeight(); + int subAnchorX = cue.getX(); + int subAnchorY = cue.getY(); + int subScaleWidth = cue.getWidth(); + int subScaleHeight = cue.getHeight(); + + // they should change together as we keep the aspect ratio + if ((surfaceHeight != sourceHeight || surfaceWidth != sourceWidth) + && sourceHeight > 0 && sourceWidth > 0) { + double scale; + if (surfaceWidth != sourceWidth) + scale = (double) surfaceWidth / (double) sourceWidth; + else + scale = (double) surfaceHeight / (double) sourceHeight; + subScaleHeight = (int) (scale * subScaleHeight); + subScaleWidth = (int) (scale * subScaleWidth); + } + if (surfaceAnchorX != 0) + subAnchorX += surfaceAnchorX; + if (subAnchorY != 0) + subAnchorY += surfaceAnchorY; + + ViewGroup.LayoutParams params = subtitleImageView.getLayoutParams(); + params.width = subScaleWidth; + params.height = subScaleHeight; + subtitleImageView.setX(subAnchorX); + subtitleImageView.setY(subAnchorY); + subtitleImageView.setLayoutParams(params); + subtitleImageView.setImageBitmap(bitmap); + subtitleImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + subtitleImageView.setVisibility(View.VISIBLE); + } + else { + subtitleImageView.setImageBitmap(null); + subtitleImageView.setVisibility(View.INVISIBLE); + } + } + // SimpleExoPlayer.VideoListener implementation @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + + sourceWidth = width; + sourceHeight = height; if (contentFrame != null) { float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; contentFrame.setAspectRatio(aspectRatio); diff --git a/library/src/main/res/layout/exo_simple_player_view.xml b/library/src/main/res/layout/exo_simple_player_view.xml index 1f59b7796d..bfd5638713 100644 --- a/library/src/main/res/layout/exo_simple_player_view.xml +++ b/library/src/main/res/layout/exo_simple_player_view.xml @@ -36,6 +36,12 @@ android:layout_width="match_parent" android:layout_height="match_parent"/> + + Date: Sat, 17 Dec 2016 14:43:07 -0500 Subject: [PATCH 011/106] less interfacy.. more classy --- .../android/exoplayer2/text/ImageCue.java | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java index 6274493780..2dc0c97238 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java @@ -4,12 +4,30 @@ import android.graphics.Bitmap; public class ImageCue extends Cue { - public ImageCue() { super(""); } + final private long start_display_time; + final private int x; + final private int y; + final private int height; + final private int width; + final private Bitmap bitmap; + final private boolean isForced; - public Bitmap getBitmap() { return null; } - public int getX() { return 0; } - public int getY() { return 0; } - public int getWidth() { return 0; } - public int getHeight() { return 0; } - public boolean isForcedSubtitle() { return false; } + public ImageCue(Bitmap bitmap, long start_display_time, int x, int y, int width, int height, boolean isForced) { + super(""); + this.bitmap = bitmap; + this.start_display_time = start_display_time; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.isForced = isForced; + } + + public long getStartDisplayTime() { return start_display_time; } + public Bitmap getBitmap() { return bitmap; } + public int getX() { return x; } + public int getY() { return y; } + public int getWidth() { return width; } + public int getHeight() { return height; } + public boolean isForcedSubtitle() { return isForced; } } From 47d8b7ff164ae2b162913ec9fea92bcc7fa58839 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sun, 18 Dec 2016 22:37:37 -0500 Subject: [PATCH 012/106] get source dimensions from plane stored in subs --- .../android/exoplayer2/text/ImageCue.java | 22 +++++++++++++------ .../exoplayer2/ui/SimpleExoPlayerView.java | 12 ++++------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java index 2dc0c97238..b5906cf41d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java @@ -7,19 +7,25 @@ public class ImageCue extends Cue { final private long start_display_time; final private int x; final private int y; - final private int height; - final private int width; + final private int bitmap_height; + final private int bitmap_width; + final private int plane_height; + final private int plane_width; final private Bitmap bitmap; final private boolean isForced; - public ImageCue(Bitmap bitmap, long start_display_time, int x, int y, int width, int height, boolean isForced) { + public ImageCue(Bitmap bitmap, long start_display_time, + int x, int y, int bitmap_width, int bitmap_height, boolean isForced, + int plane_width, int plane_height) { super(""); this.bitmap = bitmap; this.start_display_time = start_display_time; this.x = x; this.y = y; - this.width = width; - this.height = height; + this.bitmap_width = bitmap_width; + this.bitmap_height = bitmap_height; + this.plane_width = plane_width; + this.plane_height = plane_height; this.isForced = isForced; } @@ -27,7 +33,9 @@ public class ImageCue extends Cue { public Bitmap getBitmap() { return bitmap; } public int getX() { return x; } public int getY() { return y; } - public int getWidth() { return width; } - public int getHeight() { return height; } + public int getBitmapWidth() { return bitmap_width; } + public int getBitmapHeight() { return bitmap_height; } + public int getPlaneWidth() { return plane_width; } + public int getPlaneHeight() { return plane_height; } public boolean isForcedSubtitle() { return isForced; } } 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 e01956c803..ffe2ca31e0 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 @@ -614,10 +614,6 @@ public final class SimpleExoPlayerView extends FrameLayout { private final class ComponentListener implements SimpleExoPlayer.VideoListener, TextRenderer.Output, ExoPlayer.EventListener { - - private int sourceWidth = 0; - private int sourceHeight = 0; - // TextRenderer.Output implementation @Override @@ -648,10 +644,12 @@ public final class SimpleExoPlayerView extends FrameLayout { int surfaceAnchorY = (int) surfaceView.getY(); int surfaceWidth = surfaceView.getWidth(); int surfaceHeight = surfaceView.getHeight(); + int sourceWidth = cue.getPlaneWidth(); + int sourceHeight = cue.getPlaneHeight(); int subAnchorX = cue.getX(); int subAnchorY = cue.getY(); - int subScaleWidth = cue.getWidth(); - int subScaleHeight = cue.getHeight(); + int subScaleWidth = cue.getBitmapWidth(); + int subScaleHeight = cue.getBitmapHeight(); // they should change together as we keep the aspect ratio if ((surfaceHeight != sourceHeight || surfaceWidth != sourceWidth) @@ -691,8 +689,6 @@ public final class SimpleExoPlayerView extends FrameLayout { public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - sourceWidth = width; - sourceHeight = height; if (contentFrame != null) { float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; contentFrame.setAspectRatio(aspectRatio); From fc3ed0bf31c7c55c89aae74844134316759e349d Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 14 Dec 2016 09:47:02 -0800 Subject: [PATCH 013/106] 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 fa6c42ca88..a3faadacce 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 ### @@ -185,11 +185,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 a6360ab6c05235302ae9a54c8dab7678fa880d5e Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 15 Dec 2016 03:31:18 -0800 Subject: [PATCH 014/106] 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 ada19a25403ac11f4899624febfca8a2d17e17b2 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 16 Dec 2016 06:03:28 -0800 Subject: [PATCH 015/106] 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 f3d1065b5f26824535cea51dafa90daff13b90b4 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 19 Dec 2016 06:56:04 -0800 Subject: [PATCH 016/106] 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 cb3f3499265e5dbf50ac4544b5a3a8fe7fd0ad8c Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 20 Dec 2016 03:28:19 -0800 Subject: [PATCH 017/106] 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 a3faadacce..a468e72a7a 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 807e2db02617a324968a5c672e050261e7bbca0a Mon Sep 17 00:00:00 2001 From: ojw28 Date: Wed, 21 Dec 2016 00:51:06 +0000 Subject: [PATCH 018/106] Delete HlsTest --- .../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 44b21f2e3b65787aa6b5b547d5ac67e97a924a61 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Tue, 20 Dec 2016 22:49:18 -0500 Subject: [PATCH 019/106] remove imagecue and add bitmap to cue with size_height, change to painter for displaying --- .../google/android/exoplayer2/text/Cue.java | 45 ++++++++ .../android/exoplayer2/text/ImageCue.java | 41 ------- .../exoplayer2/ui/SimpleExoPlayerView.java | 65 +---------- .../exoplayer2/ui/SubtitlePainter.java | 104 +++++++++++++----- .../res/layout/exo_simple_player_view.xml | 6 - 5 files changed, 125 insertions(+), 136 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java 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 93b1dc1d9a..23cce573f7 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.Bitmap; import android.support.annotation.IntDef; import android.text.Layout.Alignment; import java.lang.annotation.Retention; @@ -78,6 +79,10 @@ public class Cue { * The alignment of the cue text within the cue box, or null if the alignment is undefined. */ public final Alignment textAlignment; + /** + * The cue image. + */ + public final Bitmap bitmap; /** * 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 @@ -85,6 +90,8 @@ 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. + *

+ * If {@link #bitmap} is not null then this value is used to indicate the top position */ public final float line; /** @@ -119,6 +126,8 @@ public class Cue { * For horizontal text, this is the horizontal position relative to the left of the viewport. Note * that positioning is relative to the left of the viewport even in the case of right-to-left * text. + *

+ * If {@link #bitmap} is not null then this value is used to indicate the left position */ public final float position; /** @@ -134,9 +143,25 @@ public class Cue { /** * 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}. + *

+ * If {@link #bitmap} is not null then this value is used to indicate the width */ public final float size; + /** + * The height size of the cue box when a {@link #bitmap} is set specified as a fraction of the + * viewport size in that direction, or {@link #DIMEN_UNSET}. + */ + public final float size_height; + + /** + * + */ + public Cue(Bitmap bitmap, float left, float top, float size, float size_height) { + this(null, null, top, LINE_TYPE_FRACTION, TYPE_UNSET, left, TYPE_UNSET, size, size_height, + bitmap); + } + /** * 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}. @@ -159,6 +184,24 @@ 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, + DIMEN_UNSET, null); + } + /** + * @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 size_height See {@link #size_height}. + * @param bitmap See {@link #bitmap}. + */ + private Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, + @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, + float size_height, Bitmap bitmap) { this.text = text; this.textAlignment = textAlignment; this.line = line; @@ -167,6 +210,8 @@ public class Cue { this.position = position; this.positionAnchor = positionAnchor; this.size = size; + this.size_height = size_height; + this.bitmap = bitmap; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java b/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java deleted file mode 100644 index b5906cf41d..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/text/ImageCue.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.google.android.exoplayer2.text; - -import android.graphics.Bitmap; - -public class ImageCue extends Cue { - - final private long start_display_time; - final private int x; - final private int y; - final private int bitmap_height; - final private int bitmap_width; - final private int plane_height; - final private int plane_width; - final private Bitmap bitmap; - final private boolean isForced; - - public ImageCue(Bitmap bitmap, long start_display_time, - int x, int y, int bitmap_width, int bitmap_height, boolean isForced, - int plane_width, int plane_height) { - super(""); - this.bitmap = bitmap; - this.start_display_time = start_display_time; - this.x = x; - this.y = y; - this.bitmap_width = bitmap_width; - this.bitmap_height = bitmap_height; - this.plane_width = plane_width; - this.plane_height = plane_height; - this.isForced = isForced; - } - - public long getStartDisplayTime() { return start_display_time; } - public Bitmap getBitmap() { return bitmap; } - public int getX() { return x; } - public int getY() { return y; } - public int getBitmapWidth() { return bitmap_width; } - public int getBitmapHeight() { return bitmap_height; } - public int getPlaneWidth() { return plane_width; } - public int getPlaneHeight() { return plane_height; } - public boolean isForcedSubtitle() { return isForced; } -} 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 ffe2ca31e0..0e63ee9ba4 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 @@ -40,7 +40,6 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.ImageCue; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -172,7 +171,6 @@ public final class SimpleExoPlayerView extends FrameLayout { private final View surfaceView; private final ImageView artworkView; private final SubtitleView subtitleView; - private final ImageView subtitleImageView; private final PlaybackControlView controller; private final ComponentListener componentListener; private final FrameLayout overlayFrameLayout; @@ -256,8 +254,6 @@ public final class SimpleExoPlayerView extends FrameLayout { subtitleView.setUserDefaultTextSize(); } - subtitleImageView = (ImageView) findViewById(R.id.exo_subtitles_image); - // Playback control view. View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); if (controllerPlaceholder != null) { @@ -619,70 +615,11 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onCues(List cues) { - boolean skipNormalCues = false; - if (subtitleImageView != null) { - - final ImageCue cue = (cues != null && !cues.isEmpty() && cues.get(0) instanceof ImageCue) ? (ImageCue) cues.get(0) : null; - skipNormalCues = (cue != null); - if (cue == null || (!subtitlesEnabled && !cue.isForcedSubtitle())) { - subtitleImageView.setImageBitmap(null); - subtitleImageView.setVisibility(View.INVISIBLE); - } - else { - handleImageCue(cue); - } - } - if (!skipNormalCues && subtitleView != null) { + if (subtitleView != null) { subtitleView.onCues(cues); } } - private void handleImageCue(ImageCue cue) { - Bitmap bitmap = cue.getBitmap(); - if (bitmap != null && surfaceView != null) { - int surfaceAnchorX = (int) surfaceView.getX(); - int surfaceAnchorY = (int) surfaceView.getY(); - int surfaceWidth = surfaceView.getWidth(); - int surfaceHeight = surfaceView.getHeight(); - int sourceWidth = cue.getPlaneWidth(); - int sourceHeight = cue.getPlaneHeight(); - int subAnchorX = cue.getX(); - int subAnchorY = cue.getY(); - int subScaleWidth = cue.getBitmapWidth(); - int subScaleHeight = cue.getBitmapHeight(); - - // they should change together as we keep the aspect ratio - if ((surfaceHeight != sourceHeight || surfaceWidth != sourceWidth) - && sourceHeight > 0 && sourceWidth > 0) { - double scale; - if (surfaceWidth != sourceWidth) - scale = (double) surfaceWidth / (double) sourceWidth; - else - scale = (double) surfaceHeight / (double) sourceHeight; - subScaleHeight = (int) (scale * subScaleHeight); - subScaleWidth = (int) (scale * subScaleWidth); - } - if (surfaceAnchorX != 0) - subAnchorX += surfaceAnchorX; - if (subAnchorY != 0) - subAnchorY += surfaceAnchorY; - - ViewGroup.LayoutParams params = subtitleImageView.getLayoutParams(); - params.width = subScaleWidth; - params.height = subScaleHeight; - subtitleImageView.setX(subAnchorX); - subtitleImageView.setY(subAnchorY); - subtitleImageView.setLayoutParams(params); - subtitleImageView.setImageBitmap(bitmap); - subtitleImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - subtitleImageView.setVisibility(View.VISIBLE); - } - else { - subtitleImageView.setImageBitmap(null); - subtitleImageView.setVisibility(View.INVISIBLE); - } - } - // SimpleExoPlayer.VideoListener implementation @Override 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 8c3ac77cb2..73c465fe95 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -18,11 +18,13 @@ package com.google.android.exoplayer2.ui; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Join; import android.graphics.Paint.Style; +import android.graphics.Rect; import android.graphics.RectF; import android.text.Layout.Alignment; import android.text.StaticLayout; @@ -63,6 +65,7 @@ import com.google.android.exoplayer2.util.Util; private final Paint paint; // Previous input variables. + private Bitmap cueBitmap; private CharSequence cueText; private Alignment cueTextAlignment; private float cueLine; @@ -74,6 +77,7 @@ import com.google.android.exoplayer2.util.Util; @Cue.AnchorType private int cuePositionAnchor; private float cueSize; + private float cueSizeHeight; private boolean applyEmbeddedStyles; private int foregroundColor; private int backgroundColor; @@ -93,6 +97,7 @@ import com.google.android.exoplayer2.util.Util; private int textLeft; private int textTop; private int textPaddingX; + private Rect bitmapRect; @SuppressWarnings("ResourceType") public SubtitlePainter(Context context) { @@ -142,40 +147,43 @@ import com.google.android.exoplayer2.util.Util; float bottomPaddingFraction, Canvas canvas, int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) { CharSequence cueText = cue.text; - if (TextUtils.isEmpty(cueText)) { + boolean textIsEmpty = TextUtils.isEmpty(cueText); + if (textIsEmpty && cue.bitmap == null) { // Nothing to draw. return; } - if (!applyEmbeddedStyles) { + if (!applyEmbeddedStyles && !textIsEmpty) { // Strip out any embedded styling. cueText = cueText.toString(); } - if (areCharSequencesEqual(this.cueText, cueText) - && Util.areEqual(this.cueTextAlignment, cue.textAlignment) - && this.cueLine == cue.line - && this.cueLineType == cue.lineType - && Util.areEqual(this.cueLineAnchor, cue.lineAnchor) - && this.cuePosition == cue.position - && Util.areEqual(this.cuePositionAnchor, cue.positionAnchor) - && this.cueSize == cue.size - && this.applyEmbeddedStyles == applyEmbeddedStyles - && this.foregroundColor == style.foregroundColor - && this.backgroundColor == style.backgroundColor - && this.windowColor == style.windowColor - && this.edgeType == style.edgeType - && this.edgeColor == style.edgeColor - && Util.areEqual(this.textPaint.getTypeface(), style.typeface) - && this.textSizePx == textSizePx - && this.bottomPaddingFraction == bottomPaddingFraction - && this.parentLeft == cueBoxLeft - && this.parentTop == cueBoxTop - && this.parentRight == cueBoxRight - && this.parentBottom == cueBoxBottom) { + if (((cue.bitmap != null && cue.bitmap == cueBitmap) || + (!textIsEmpty && areCharSequencesEqual(this.cueText, cueText))) + && Util.areEqual(this.cueTextAlignment, cue.textAlignment) + && this.cueLine == cue.line + && this.cueLineType == cue.lineType + && Util.areEqual(this.cueLineAnchor, cue.lineAnchor) + && this.cuePosition == cue.position + && Util.areEqual(this.cuePositionAnchor, cue.positionAnchor) + && this.cueSize == cue.size + && this.applyEmbeddedStyles == applyEmbeddedStyles + && this.foregroundColor == style.foregroundColor + && this.backgroundColor == style.backgroundColor + && this.windowColor == style.windowColor + && this.edgeType == style.edgeType + && this.edgeColor == style.edgeColor + && Util.areEqual(this.textPaint.getTypeface(), style.typeface) + && this.textSizePx == textSizePx + && this.bottomPaddingFraction == bottomPaddingFraction + && this.parentLeft == cueBoxLeft + && this.parentTop == cueBoxTop + && this.parentRight == cueBoxRight + && this.parentBottom == cueBoxBottom) { // We can use the cached layout. drawLayout(canvas); return; } + this.cueBitmap = cue.bitmap; this.cueText = cueText; this.cueTextAlignment = cue.textAlignment; this.cueLine = cue.line; @@ -184,6 +192,7 @@ import com.google.android.exoplayer2.util.Util; this.cuePosition = cue.position; this.cuePositionAnchor = cue.positionAnchor; this.cueSize = cue.size; + this.cueSizeHeight = cue.size_height; this.applyEmbeddedStyles = applyEmbeddedStyles; this.foregroundColor = style.foregroundColor; this.backgroundColor = style.backgroundColor; @@ -198,6 +207,32 @@ import com.google.android.exoplayer2.util.Util; this.parentRight = cueBoxRight; this.parentBottom = cueBoxBottom; + if (this.cueBitmap != null) + setupBitmapLayout(); + else + setupTextLayout(); + + drawLayout(canvas); + } + + /** + * Setup {@link #textLayout} for later drawing. + */ + private void setupBitmapLayout() { + + int parentWidth = parentRight - parentLeft; + int parentHeight = parentBottom - parentTop; + int x = parentLeft + (int) ((float) parentWidth * cuePosition); + int y = parentTop + (int) ((float) parentHeight * cueLine); + bitmapRect = new Rect(x,y, + x + (int)((float) parentWidth * cueSize),y + (int)((float) parentHeight * cueSizeHeight)); + } + + /** + * Setup {@link #textLayout} for later drawing. + */ + private void setupTextLayout() { + int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; @@ -275,8 +310,27 @@ import com.google.android.exoplayer2.util.Util; this.textLeft = textLeft; this.textTop = textTop; this.textPaddingX = textPaddingX; + } - drawLayout(canvas); + /** + * Draws {@link #textLayout} or {@link #cueBitmap} into the provided canvas. + * + * @param canvas The canvas into which to draw. + */ + private void drawLayout(Canvas canvas) { + if (cueBitmap != null) + drawBitmapLayout(canvas); + else + drawTextLayout(canvas); + } + + /** + * Draws {@link #cueBitmap} into the provided canvas. + * + * @param canvas The canvas into which to draw. + */ + private void drawBitmapLayout(Canvas canvas) { + canvas.drawBitmap(cueBitmap, null, bitmapRect, null); } /** @@ -284,7 +338,7 @@ import com.google.android.exoplayer2.util.Util; * * @param canvas The canvas into which to draw. */ - private void drawLayout(Canvas canvas) { + private void drawTextLayout(Canvas canvas) { final StaticLayout layout = textLayout; if (layout == null) { // Nothing to draw. diff --git a/library/src/main/res/layout/exo_simple_player_view.xml b/library/src/main/res/layout/exo_simple_player_view.xml index bfd5638713..1f59b7796d 100644 --- a/library/src/main/res/layout/exo_simple_player_view.xml +++ b/library/src/main/res/layout/exo_simple_player_view.xml @@ -36,12 +36,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"/> - - Date: Wed, 21 Dec 2016 07:54:23 -0500 Subject: [PATCH 020/106] remove unneeded changes --- .../exoplayer2/ui/SimpleExoPlayerView.java | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 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 0e63ee9ba4..97c564a3a6 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 @@ -178,7 +178,6 @@ public final class SimpleExoPlayerView extends FrameLayout { private SimpleExoPlayer player; private boolean useController; private boolean useArtwork; - private boolean subtitlesEnabled = false; private int controllerShowTimeoutMs; public SimpleExoPlayerView(Context context) { @@ -524,26 +523,14 @@ public final class SimpleExoPlayerView extends FrameLayout { return; } TrackSelectionArray selections = player.getCurrentTrackSelections(); - boolean quickExit = false; for (int i = 0; i < selections.length; i++) { - switch(player.getRendererType(i)) { - case C.TRACK_TYPE_VIDEO: - if (selections.get(i) != null) { - // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in - // onRenderedFirstFrame(). - - hideArtwork(); - quickExit = true; - } - break; - case C.TRACK_TYPE_TEXT: - if (selections.get(i) != null) - subtitlesEnabled = true; - break; + if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { + // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in + // onRenderedFirstFrame(). + hideArtwork(); + return; } } - if (quickExit) - return; // Video disabled so the shutter must be closed. if (shutterView != null) { shutterView.setVisibility(VISIBLE); @@ -614,7 +601,6 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onCues(List cues) { - if (subtitleView != null) { subtitleView.onCues(cues); } @@ -625,7 +611,6 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (contentFrame != null) { float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; contentFrame.setAspectRatio(aspectRatio); From d816d26d83804339007806fca2ffd84316b2b975 Mon Sep 17 00:00:00 2001 From: Ippei Nawate Date: Wed, 4 Jan 2017 17:24:53 +0900 Subject: [PATCH 021/106] Fix RELEASENOTES.md --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a468e72a7a..fdb5e3d8a2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -58,7 +58,7 @@ this version. * 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 - is attached ([#2077](https://github.com/google/ExoPlayer/issues/1976)). + is attached ([#2077](https://github.com/google/ExoPlayer/issues/2077)). * OGG: Fix playback of short OGG files ([#1976](https://github.com/google/ExoPlayer/issues/1976)). * MP4: Support `.mp3` tracks From 75eb047e626ff5405c55cf1da382735124935c35 Mon Sep 17 00:00:00 2001 From: Hassan Abid Date: Thu, 5 Jan 2017 21:31:08 +0900 Subject: [PATCH 022/106] remove ClippingMediaSourceTest from library --- .../source/ClippingMediaSourceTest.java | 143 ------------------ 1 file changed, 143 deletions(-) delete mode 100644 library/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java deleted file mode 100644 index 0933fb858b..0000000000 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ /dev/null @@ -1,143 +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.source; - -import static org.mockito.Mockito.doAnswer; - -import android.test.InstrumentationTestCase; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.Timeline.Window; -import com.google.android.exoplayer2.source.MediaSource.Listener; -import com.google.android.exoplayer2.testutil.TestUtil; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -/** - * Unit tests for {@link ClippingMediaSource}. - */ -public final class ClippingMediaSourceTest extends InstrumentationTestCase { - - private static final long TEST_PERIOD_DURATION_US = 1000000; - private static final long TEST_CLIP_AMOUNT_US = 300000; - - @Mock - private MediaSource mockMediaSource; - private Timeline clippedTimeline; - private Window window; - private Period period; - - @Override - protected void setUp() throws Exception { - TestUtil.setUpMockito(this); - window = new Timeline.Window(); - period = new Timeline.Period(); - } - - public void testNoClipping() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); - - Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); - - assertEquals(1, clippedTimeline.getWindowCount()); - assertEquals(1, clippedTimeline.getPeriodCount()); - assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getWindow(0, window).getDurationUs()); - assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getPeriod(0, period).getDurationUs()); - } - - public void testClippingUnseekableWindowThrows() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false); - - // If the unseekable window isn't clipped, clipping succeeds. - getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); - try { - // If the unseekable window is clipped, clipping fails. - getClippedTimeline(timeline, 1, TEST_PERIOD_DURATION_US); - fail("Expected clipping to fail."); - } catch (IllegalArgumentException e) { - // Expected. - } - } - - public void testClippingStart() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); - - Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, - TEST_PERIOD_DURATION_US); - assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, - clippedTimeline.getWindow(0, window).getDurationUs()); - assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, - clippedTimeline.getPeriod(0, period).getDurationUs()); - } - - public void testClippingEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); - - Timeline clippedTimeline = getClippedTimeline(timeline, 0, - TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); - assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, - clippedTimeline.getWindow(0, window).getDurationUs()); - assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US, - clippedTimeline.getPeriod(0, period).getDurationUs()); - } - - public void testClippingStartAndEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); - - Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, - TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); - assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3, - clippedTimeline.getWindow(0, window).getDurationUs()); - assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3, - clippedTimeline.getPeriod(0, period).getDurationUs()); - } - - /** - * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. - */ - private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { - mockMediaSourceSourceWithTimeline(timeline); - new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true, - new Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - clippedTimeline = timeline; - } - }); - return clippedTimeline; - } - - /** - * Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info. - */ - private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) { - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - MediaSource.Listener listener = (MediaSource.Listener) invocation.getArguments()[2]; - listener.onSourceInfoRefreshed(timeline, null); - return null; - } - }).when(mockMediaSource).prepareSource(Mockito.any(ExoPlayer.class), Mockito.anyBoolean(), - Mockito.any(MediaSource.Listener.class)); - return mockMediaSource; - } - -} From 0468a80d4125af669c0dadf66bd53144db323423 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Thu, 5 Jan 2017 21:27:24 -0500 Subject: [PATCH 023/106] just use original plane width to calculate width proportion to use later as size and keep aspect ratio for height --- .../google/android/exoplayer2/text/Cue.java | 25 ++++--------------- .../exoplayer2/ui/SubtitlePainter.java | 7 +++--- 2 files changed, 8 insertions(+), 24 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 23cce573f7..5acb29da33 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 @@ -90,8 +90,6 @@ 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. - *

- * If {@link #bitmap} is not null then this value is used to indicate the top position */ public final float line; /** @@ -126,8 +124,6 @@ public class Cue { * For horizontal text, this is the horizontal position relative to the left of the viewport. Note * that positioning is relative to the left of the viewport even in the case of right-to-left * text. - *

- * If {@link #bitmap} is not null then this value is used to indicate the left position */ public final float position; /** @@ -143,23 +139,15 @@ public class Cue { /** * 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}. - *

- * If {@link #bitmap} is not null then this value is used to indicate the width */ public final float size; - /** - * The height size of the cue box when a {@link #bitmap} is set specified as a fraction of the - * viewport size in that direction, or {@link #DIMEN_UNSET}. - */ - public final float size_height; - /** * */ - public Cue(Bitmap bitmap, float left, float top, float size, float size_height) { - this(null, null, top, LINE_TYPE_FRACTION, TYPE_UNSET, left, TYPE_UNSET, size, size_height, - bitmap); + public Cue(Bitmap bitmap, float left, float top, int plane_width) { + this(null, null, top, LINE_TYPE_FRACTION, ANCHOR_TYPE_START, left, ANCHOR_TYPE_START, + (float) bitmap.getWidth() / plane_width, bitmap); } /** @@ -184,8 +172,7 @@ 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, - DIMEN_UNSET, null); + this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, null); } /** * @param text See {@link #text}. @@ -196,12 +183,11 @@ public class Cue { * @param position See {@link #position}. * @param positionAnchor See {@link #positionAnchor}. * @param size See {@link #size}. - * @param size_height See {@link #size_height}. * @param bitmap See {@link #bitmap}. */ private Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, - float size_height, Bitmap bitmap) { + Bitmap bitmap) { this.text = text; this.textAlignment = textAlignment; this.line = line; @@ -210,7 +196,6 @@ public class Cue { this.position = position; this.positionAnchor = positionAnchor; this.size = size; - this.size_height = size_height; this.bitmap = bitmap; } 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 73c465fe95..b8b55e27ce 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 @@ -77,7 +77,6 @@ import com.google.android.exoplayer2.util.Util; @Cue.AnchorType private int cuePositionAnchor; private float cueSize; - private float cueSizeHeight; private boolean applyEmbeddedStyles; private int foregroundColor; private int backgroundColor; @@ -192,7 +191,6 @@ import com.google.android.exoplayer2.util.Util; this.cuePosition = cue.position; this.cuePositionAnchor = cue.positionAnchor; this.cueSize = cue.size; - this.cueSizeHeight = cue.size_height; this.applyEmbeddedStyles = applyEmbeddedStyles; this.foregroundColor = style.foregroundColor; this.backgroundColor = style.backgroundColor; @@ -224,8 +222,9 @@ import com.google.android.exoplayer2.util.Util; int parentHeight = parentBottom - parentTop; int x = parentLeft + (int) ((float) parentWidth * cuePosition); int y = parentTop + (int) ((float) parentHeight * cueLine); - bitmapRect = new Rect(x,y, - x + (int)((float) parentWidth * cueSize),y + (int)((float) parentHeight * cueSizeHeight)); + int width = (int) (parentWidth * cueSize); + int height = (int) (width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); + bitmapRect = new Rect(x, y, x + width, y + height); } /** From 30c59c7a3f93387bbf2edd66a2089d54705dddf8 Mon Sep 17 00:00:00 2001 From: Devin Tuchsen Date: Fri, 13 Jan 2017 19:45:36 -0600 Subject: [PATCH 024/106] Apple Lossless (ALAC) support --- extensions/ffmpeg/README.md | 1 + .../android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 7 ++++++- .../android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java | 2 ++ .../com/google/android/exoplayer2/extractor/mp4/Atom.java | 1 + .../android/exoplayer2/extractor/mp4/AtomParsers.java | 8 +++++++- .../com/google/android/exoplayer2/util/MimeTypes.java | 1 + 6 files changed, 18 insertions(+), 2 deletions(-) diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index d7c5e21fcc..0d669f826d 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -63,6 +63,7 @@ git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ --enable-decoder=vorbis \ --enable-decoder=opus \ --enable-decoder=flac \ + --enable-decoder=alac \ && \ make -j4 && \ make install-libs diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 12f4bcf672..92240a50c1 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 @@ -87,7 +87,11 @@ import java.util.List; } if (!hasOutputFormat) { channelCount = ffmpegGetChannelCount(nativeContext); - sampleRate = ffmpegGetSampleRate(nativeContext); + if ("alac".equals(codecName)) { + sampleRate = ByteBuffer.wrap(extraData, extraData.length - 4, 4).getInt(); + } else { + sampleRate = ffmpegGetSampleRate(nativeContext); + } hasOutputFormat = true; } outputBuffer.data.position(0); @@ -123,6 +127,7 @@ import java.util.List; private static byte[] getExtraData(String mimeType, List initializationData) { switch (mimeType) { case MimeTypes.AUDIO_AAC: + case MimeTypes.AUDIO_ALAC: case MimeTypes.AUDIO_OPUS: return initializationData.get(0); case MimeTypes.AUDIO_VORBIS: diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index 90b42c01bb..4992bcbb3e 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -92,6 +92,8 @@ public final class FfmpegLibrary { return "amrwb"; case MimeTypes.AUDIO_FLAC: return "flac"; + case MimeTypes.AUDIO_ALAC: + return "alac"; default: return null; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index c8ee8ff8c3..cc7e662336 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -135,6 +135,7 @@ import java.util.List; public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); public static final int TYPE_camm = Util.getIntegerCodeForString("camm"); + public static final int TYPE_alac = Util.getIntegerCodeForString("alac"); public final int type; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 9dc0578263..5288a3e6ba 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -604,7 +604,7 @@ import java.util.List; || childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl || childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt - || childAtomType == Atom.TYPE__mp3) { + || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac) { parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, isQuickTime, drmInitData, out, i); } else if (childAtomType == Atom.TYPE_TTML) { @@ -839,6 +839,8 @@ import java.util.List; mimeType = MimeTypes.AUDIO_RAW; } else if (atomType == Atom.TYPE__mp3) { mimeType = MimeTypes.AUDIO_MPEG; + } else if (atomType == Atom.TYPE_alac) { + mimeType = MimeTypes.AUDIO_ALAC; } byte[] initializationData = null; @@ -876,6 +878,10 @@ import java.util.List; out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); + } else if (childAtomType == Atom.TYPE_alac) { + initializationData = new byte[childAtomSize]; + parent.setPosition(childPosition); + parent.readBytes(initializationData, 0, childAtomSize); } childPosition += childAtomSize; } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 9870b6547a..1c43ccddc1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -60,6 +60,7 @@ public final class MimeTypes { public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp"; public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb"; public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/x-flac"; + public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; From 24724158b1622955c3af2cfcefeb19248deced1e Mon Sep 17 00:00:00 2001 From: Devin Tuchsen Date: Sun, 22 Jan 2017 13:05:00 -0600 Subject: [PATCH 025/106] 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 026/106] 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 027/106] 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 028/106] 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 029/106] 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 030/106] 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 031/106] 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 032/106] 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 033/106] 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 034/106] 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 035/106] 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 036/106] 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 037/106] 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 038/106] 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 039/106] 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 040/106] 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 041/106] 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 042/106] 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 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 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 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 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 043/106] 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 044/106] 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 045/106] 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 046/106] 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 047/106] 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 048/106] 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 049/106] 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 050/106] 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 051/106] 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 052/106] 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 053/106] 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 { From 933b7faadd97d57c0b1de625c2600aa0caa2347a Mon Sep 17 00:00:00 2001 From: twisstosin Date: Sun, 12 Feb 2017 23:06:51 +0100 Subject: [PATCH 054/106] Updated Gradle, Compile and Build Tools Version --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index b10a17de81..3f4bab597a 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.1' + classpath 'com.android.tools.build:gradle:2.2.3' classpath 'com.novoda:bintray-release:0.3.4' } } @@ -29,9 +29,9 @@ allprojects { jcenter() } project.ext { - compileSdkVersion=24 - targetSdkVersion=24 - buildToolsVersion='23.0.3' + compileSdkVersion=25 + targetSdkVersion=25 + buildToolsVersion='25' releaseRepoName = 'exoplayer' releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' From 460dc2561a07eca9e5bffaf334a0c9d6858581e9 Mon Sep 17 00:00:00 2001 From: twisstosin Date: Tue, 14 Feb 2017 19:23:08 +0100 Subject: [PATCH 055/106] Changed Text Size --- library/src/main/res/layout/exo_playback_control_view.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/res/layout/exo_playback_control_view.xml b/library/src/main/res/layout/exo_playback_control_view.xml index f8ef5a6fdd..531ee4c6fa 100644 --- a/library/src/main/res/layout/exo_playback_control_view.xml +++ b/library/src/main/res/layout/exo_playback_control_view.xml @@ -58,7 +58,7 @@ Date: Mon, 30 Jan 2017 12:55:35 -0800 Subject: [PATCH 056/106] Fixed issue with TextRenderer and RawCC 608/708 captions in which all the captions prior to the playback position would be rendered in [] succession when a VOD stream starts. Instead of clearing the DECODE_ONLY flag for all input buffers in TextRenderer (i.e. for all caption types), we now only clear it on the output buffer in SimpleSubtitleDecoder. The number if input buffers in CeaDecoder has also been increased to reduce the delay before captions appear for playback sessions that start far ahead into the content. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146028680 --- .../android/exoplayer2/text/SimpleSubtitleDecoder.java | 3 +++ .../com/google/android/exoplayer2/text/TextRenderer.java | 2 -- .../google/android/exoplayer2/text/cea/CeaDecoder.java | 8 +++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index ae3bd309ff..dd25ef8345 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.SimpleDecoder; import java.nio.ByteBuffer; @@ -68,6 +69,8 @@ public abstract class SimpleSubtitleDecoder extends ByteBuffer inputData = inputBuffer.data; Subtitle subtitle = decode(inputData.array(), inputData.limit()); outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); + // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). + outputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); return null; } catch (SubtitleDecoderException e) { return e; diff --git a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 8dbde1be5e..649575865e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -191,8 +191,6 @@ public final class TextRenderer extends BaseRenderer implements Callback { // Try and read the next subtitle from the source. int result = readSource(formatHolder, nextInputBuffer); if (result == C.RESULT_BUFFER_READ) { - // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]) and queue the buffer. - nextInputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY); if (nextInputBuffer.isEndOfStream()) { inputStreamEnded = true; } else { diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java index f479050d57..fac0982e65 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java @@ -75,7 +75,13 @@ import java.util.TreeSet; public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException { Assertions.checkArgument(inputBuffer != null); Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); - queuedInputBuffers.add(inputBuffer); + if (inputBuffer.isDecodeOnly()) { + // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow + // for decoding to begin mid-stream. + releaseInputBuffer(inputBuffer); + } else { + queuedInputBuffers.add(inputBuffer); + } dequeuedInputBuffer = null; } From 8f482cb2ed99b49826b2140aaa53b270422560ed Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 31 Jan 2017 07:45:03 -0800 Subject: [PATCH 057/106] Fixing some Javadoc errors - Cea708 Javadoc references private internals with @link. Doc about device accessibility settings doesn't relate to the decoder, either. - Creator Javadoc unnecessary. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146118092 --- .../main/java/com/google/android/exoplayer2/Format.java | 3 --- .../google/android/exoplayer2/text/cea/Cea708Decoder.java | 7 ------- 2 files changed, 10 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index 0b558153fd..bf113119a6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -672,9 +672,6 @@ public final class Format implements Parcelable { dest.writeParcelable(metadata, 0); } - /** - * {@link Creator} implementation. - */ public static final Creator CREATOR = new Creator() { @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 5ca5ce1270..fe97dc62a5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -43,13 +43,6 @@ import java.util.List; /** * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708"). - * - *

This implementation does not provide full compatibility with the CEA-708 specification. Note - * that only the default pen/text and window/cue colors (i.e. text with - * {@link CueBuilder#COLOR_SOLID_WHITE} foreground and {@link CueBuilder#COLOR_SOLID_BLACK} - * background, and cues with {@link CueBuilder#COLOR_SOLID_BLACK} fill) will be overridden with - * device accessibility settings; all others will use the colors and opacity specified by the - * caption data. */ public final class Cea708Decoder extends CeaDecoder { From af98ca661a43c3ef4ee52e57e5d592549e406eb6 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 31 Jan 2017 07:47:28 -0800 Subject: [PATCH 058/106] Refactor DashTest class Moved DashHostedTest to top level classes. Added DashHostedTest.Builder. Move widevine offline tests to separate class with custom setUp and tearDown methods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146118310 --- .../playbacktests/gts/DashHostedTest.java | 462 +++++++ .../playbacktests/gts/DashTest.java | 1098 +++++------------ .../playbacktests/gts/DashTestData.java | 141 +++ .../gts/DashWidevineOfflineTest.java | 180 +++ .../playbacktests/util/ExoHostedTest.java | 3 +- 5 files changed, 1072 insertions(+), 812 deletions(-) create mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java create mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java create mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java new file mode 100644 index 0000000000..24765f282d --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashHostedTest.java @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.playbacktests.gts; + +import static com.google.android.exoplayer2.C.WIDEVINE_UUID; + +import android.annotation.TargetApi; +import android.app.Instrumentation; +import android.media.MediaDrm; +import android.media.UnsupportedSchemeException; +import android.net.Uri; +import android.util.Log; +import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.drm.MediaDrmCallback; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; +import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; +import com.google.android.exoplayer2.playbacktests.util.DebugSimpleExoPlayer; +import com.google.android.exoplayer2.playbacktests.util.DecoderCountersUtil; +import com.google.android.exoplayer2.playbacktests.util.ExoHostedTest; +import com.google.android.exoplayer2.playbacktests.util.HostActivity; +import com.google.android.exoplayer2.playbacktests.util.HostActivity.HostedTest; +import com.google.android.exoplayer2.playbacktests.util.MetricsLogger; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.trackselection.FixedTrackSelection; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.RandomTrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import junit.framework.AssertionFailedError; + +/** + * A {@link HostedTest} for DASH playback tests. + */ +@TargetApi(16) +public final class DashHostedTest extends ExoHostedTest { + + /** {@link DashHostedTest} builder. */ + public static final class Builder { + + private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000; + + private static final String REPORT_NAME = "GtsExoPlayerTestCases"; + private static final String REPORT_OBJECT_NAME = "playbacktest"; + + // Whether adaptive tests should enable video formats beyond those mandated by the Android CDD + // if the device advertises support for them. + private static final boolean ALLOW_ADDITIONAL_VIDEO_FORMATS = Util.SDK_INT >= 24; + + private final String tag; + + private String streamName; + private boolean fullPlaybackNoSeeking; + private String audioFormat; + private boolean canIncludeAdditionalVideoFormats; + private ActionSchedule actionSchedule; + private byte[] offlineLicenseKeySetId; + private String[] videoFormats; + private String manifestUrl; + private boolean useL1Widevine; + private String widevineLicenseUrl; + + public Builder(String tag) { + this.tag = tag; + } + + public Builder setStreamName(String streamName) { + this.streamName = streamName; + return this; + } + + public Builder setFullPlaybackNoSeeking(boolean fullPlaybackNoSeeking) { + this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; + return this; + } + + public Builder setCanIncludeAdditionalVideoFormats( + boolean canIncludeAdditionalVideoFormats) { + this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats + && ALLOW_ADDITIONAL_VIDEO_FORMATS; + return this; + } + + public Builder setActionSchedule(ActionSchedule actionSchedule) { + this.actionSchedule = actionSchedule; + return this; + } + + public Builder setOfflineLicenseKeySetId(byte[] offlineLicenseKeySetId) { + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + return this; + } + + public Builder setAudioVideoFormats(String audioFormat, String... videoFormats) { + this.audioFormat = audioFormat; + this.videoFormats = videoFormats; + return this; + } + + public Builder setManifestUrl(String manifestUrl) { + this.manifestUrl = MANIFEST_URL_PREFIX + manifestUrl; + return this; + } + + public Builder setManifestUrlForWidevine(String manifestUrl, String videoMimeType) { + this.useL1Widevine = isL1WidevineAvailable(videoMimeType); + this.manifestUrl = getWidevineManifestUrl(manifestUrl, useL1Widevine); + this.widevineLicenseUrl = getWidevineLicenseUrl(useL1Widevine); + return this; + } + + private DashHostedTest createDashHostedTest(boolean canIncludeAdditionalVideoFormats, + boolean isCddLimitedRetry, Instrumentation instrumentation) { + MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(instrumentation, tag, + REPORT_NAME, REPORT_OBJECT_NAME); + return new DashHostedTest(tag, streamName, manifestUrl, metricsLogger, fullPlaybackNoSeeking, + audioFormat, canIncludeAdditionalVideoFormats, isCddLimitedRetry, actionSchedule, + offlineLicenseKeySetId, widevineLicenseUrl, useL1Widevine, videoFormats); + } + + public void runTest(HostActivity activity, Instrumentation instrumentation) { + DashHostedTest test = createDashHostedTest(canIncludeAdditionalVideoFormats, false, + instrumentation); + activity.runTest(test, TEST_TIMEOUT_MS); + // Retry test exactly once if adaptive test fails due to excessive dropped buffers when + // playing non-CDD required formats (b/28220076). + if (test.needsCddLimitedRetry) { + activity.runTest(createDashHostedTest(false, true, instrumentation), TEST_TIMEOUT_MS); + } + } + + } + + private static final String AUDIO_TAG_SUFFIX = ":Audio"; + private static final String VIDEO_TAG_SUFFIX = ":Video"; + static final int VIDEO_RENDERER_INDEX = 0; + static final int AUDIO_RENDERER_INDEX = 1; + + private static final int MIN_LOADABLE_RETRY_COUNT = 10; + private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10; + private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; + + private static final String MANIFEST_URL_PREFIX = "https://storage.googleapis.com/exoplayer-test-" + + "media-1/gen-3/screens/dash-vod-single-segment/"; + + private static final String WIDEVINE_L1_SUFFIX = "-hw.mpd"; + private static final String WIDEVINE_L3_SUFFIX = "-sw.mpd"; + + private static final String WIDEVINE_LICENSE_URL = + "https://proxy.uat.widevine.com/proxy?provider=widevine_test&video_id="; + private static final String WIDEVINE_SW_CRYPTO_CONTENT_ID = "exoplayer_test_1"; + private static final String WIDEVINE_HW_SECURE_DECODE_CONTENT_ID = "exoplayer_test_2"; + private static final String WIDEVINE_SECURITY_LEVEL_1 = "L1"; + private static final String WIDEVINE_SECURITY_LEVEL_3 = "L3"; + private static final String SECURITY_LEVEL_PROPERTY = "securityLevel"; + + private final String streamName; + private final String manifestUrl; + private final MetricsLogger metricsLogger; + private final boolean fullPlaybackNoSeeking; + private final boolean isCddLimitedRetry; + private final DashTestTrackSelector trackSelector; + private final byte[] offlineLicenseKeySetId; + private final String widevineLicenseUrl; + private final boolean useL1Widevine; + + boolean needsCddLimitedRetry; + + public static String getWidevineManifestUrl(String manifestUrl, boolean useL1Widevine) { + return MANIFEST_URL_PREFIX + manifestUrl + + (useL1Widevine ? WIDEVINE_L1_SUFFIX : WIDEVINE_L3_SUFFIX); + } + + public static String getWidevineLicenseUrl(boolean useL1Widevine) { + return WIDEVINE_LICENSE_URL + + (useL1Widevine ? WIDEVINE_HW_SECURE_DECODE_CONTENT_ID : WIDEVINE_SW_CRYPTO_CONTENT_ID); + } + + @TargetApi(18) + @SuppressWarnings("ResourceType") + public static boolean isL1WidevineAvailable(String videoMimeType) { + try { + // Force L3 if secure decoder is not available. + if (MediaCodecUtil.getDecoderInfo(videoMimeType, true) == null) { + return false; + } + + MediaDrm mediaDrm = new MediaDrm(WIDEVINE_UUID); + String securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY); + mediaDrm.release(); + return WIDEVINE_SECURITY_LEVEL_1.equals(securityProperty); + } catch (MediaCodecUtil.DecoderQueryException | UnsupportedSchemeException e) { + throw new IllegalStateException(e); + } + } + + /** + * @param tag A tag to use for logging. + * @param streamName The name of the test stream for metric logging. + * @param manifestUrl The manifest url. + * @param metricsLogger Logger to log metrics from the test. + * @param fullPlaybackNoSeeking Whether the test will play the entire source with no seeking. + * @param audioFormat The audio format. + * @param canIncludeAdditionalVideoFormats Whether to use video formats in addition to those + * listed in the videoFormats argument, if the device is capable of playing them. + * @param isCddLimitedRetry Whether this is a CDD limited retry following a previous failure. + * @param actionSchedule The action schedule for the test. + * @param offlineLicenseKeySetId The key set id of the license to be used. + * @param widevineLicenseUrl If the video is Widevine encrypted, this is the license url + * otherwise null. + * @param useL1Widevine Whether to use L1 Widevine. + * @param videoFormats The video formats. + */ + private DashHostedTest(String tag, String streamName, String manifestUrl, + MetricsLogger metricsLogger, boolean fullPlaybackNoSeeking, String audioFormat, + boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry, + ActionSchedule actionSchedule, byte[] offlineLicenseKeySetId, String widevineLicenseUrl, + boolean useL1Widevine, String... videoFormats) { + super(tag, fullPlaybackNoSeeking); + Assertions.checkArgument(!(isCddLimitedRetry && canIncludeAdditionalVideoFormats)); + this.streamName = streamName; + this.manifestUrl = manifestUrl; + this.metricsLogger = metricsLogger; + this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; + this.isCddLimitedRetry = isCddLimitedRetry; + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + this.widevineLicenseUrl = widevineLicenseUrl; + this.useL1Widevine = useL1Widevine; + trackSelector = new DashTestTrackSelector(tag, audioFormat, videoFormats, + canIncludeAdditionalVideoFormats); + if (actionSchedule != null) { + setSchedule(actionSchedule); + } + } + + @Override + protected MappingTrackSelector buildTrackSelector(HostActivity host, + BandwidthMeter bandwidthMeter) { + return trackSelector; + } + + @Override + protected DefaultDrmSessionManager buildDrmSessionManager( + final String userAgent) { + if (widevineLicenseUrl == null) { + return null; + } + try { + MediaDrmCallback drmCallback = new HttpMediaDrmCallback(widevineLicenseUrl, + new DefaultHttpDataSourceFactory(userAgent)); + DefaultDrmSessionManager drmSessionManager = + DefaultDrmSessionManager.newWidevineInstance(drmCallback, null, null, null); + if (!useL1Widevine) { + drmSessionManager.setPropertyString( + SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); + } + if (offlineLicenseKeySetId != null) { + drmSessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, + offlineLicenseKeySetId); + } + return drmSessionManager; + } catch (UnsupportedDrmException e) { + throw new IllegalStateException(e); + } + } + + @Override + protected SimpleExoPlayer buildExoPlayer(HostActivity host, Surface surface, + MappingTrackSelector trackSelector, + DrmSessionManager drmSessionManager) { + SimpleExoPlayer player = new DebugSimpleExoPlayer(host, trackSelector, + new DefaultLoadControl(), drmSessionManager); + player.setVideoSurface(surface); + return player; + } + + @Override + protected MediaSource buildSource(HostActivity host, String userAgent, + TransferListener mediaTransferListener) { + DataSource.Factory manifestDataSourceFactory = new DefaultDataSourceFactory(host, userAgent); + DataSource.Factory mediaDataSourceFactory = new DefaultDataSourceFactory(host, userAgent, + mediaTransferListener); + Uri manifestUri = Uri.parse(manifestUrl); + DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( + mediaDataSourceFactory); + return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory, + MIN_LOADABLE_RETRY_COUNT, 0 /* livePresentationDelayMs */, null, null); + } + + @Override + protected void logMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters) { + metricsLogger.logMetric(MetricsLogger.KEY_TEST_NAME, streamName); + metricsLogger.logMetric(MetricsLogger.KEY_IS_CDD_LIMITED_RETRY, isCddLimitedRetry); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_DROPPED_COUNT, + videoCounters.droppedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT, + videoCounters.maxConsecutiveDroppedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_SKIPPED_COUNT, + videoCounters.skippedOutputBufferCount); + metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_RENDERED_COUNT, + videoCounters.renderedOutputBufferCount); + metricsLogger.close(); + } + + @Override + protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) { + if (fullPlaybackNoSeeking) { + // We shouldn't have skipped any output buffers. + DecoderCountersUtil.assertSkippedOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, 0); + DecoderCountersUtil.assertSkippedOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, 0); + // We allow one fewer output buffer due to the way that MediaCodecRenderer and the + // underlying decoders handle the end of stream. This should be tightened up in the future. + DecoderCountersUtil.assertTotalOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, + audioCounters.inputBufferCount - 1, audioCounters.inputBufferCount); + DecoderCountersUtil.assertTotalOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, + videoCounters.inputBufferCount - 1, videoCounters.inputBufferCount); + } + try { + int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION + * DecoderCountersUtil.getTotalOutputBuffers(videoCounters)); + // Assert that performance is acceptable. + // Assert that total dropped frames were within limit. + DecoderCountersUtil.assertDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, videoCounters, + droppedFrameLimit); + // Assert that consecutive dropped frames were within limit. + DecoderCountersUtil.assertConsecutiveDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, + videoCounters, MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES); + } catch (AssertionFailedError e) { + if (trackSelector.includedAdditionalVideoFormats) { + // Retry limiting to CDD mandated formats (b/28220076). + Log.e(tag, "Too many dropped or consecutive dropped frames.", e); + needsCddLimitedRetry = true; + } else { + throw e; + } + } + } + + private static final class DashTestTrackSelector extends MappingTrackSelector { + + private final String tag; + private final String audioFormatId; + private final String[] videoFormatIds; + private final boolean canIncludeAdditionalVideoFormats; + + public boolean includedAdditionalVideoFormats; + + private DashTestTrackSelector(String tag, String audioFormatId, String[] videoFormatIds, + boolean canIncludeAdditionalVideoFormats) { + this.tag = tag; + this.audioFormatId = audioFormatId; + this.videoFormatIds = videoFormatIds; + this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats; + } + + @Override + protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + throws ExoPlaybackException { + Assertions.checkState(rendererCapabilities[VIDEO_RENDERER_INDEX].getTrackType() + == C.TRACK_TYPE_VIDEO); + Assertions.checkState(rendererCapabilities[AUDIO_RENDERER_INDEX].getTrackType() + == C.TRACK_TYPE_AUDIO); + Assertions.checkState(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].length == 1); + Assertions.checkState(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].length == 1); + TrackSelection[] selections = new TrackSelection[rendererCapabilities.length]; + selections[VIDEO_RENDERER_INDEX] = new RandomTrackSelection( + rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), + getVideoTrackIndices(rendererTrackGroupArrays[VIDEO_RENDERER_INDEX].get(0), + rendererFormatSupports[VIDEO_RENDERER_INDEX][0], videoFormatIds, + canIncludeAdditionalVideoFormats), + 0 /* seed */); + selections[AUDIO_RENDERER_INDEX] = new FixedTrackSelection( + rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), + getTrackIndex(rendererTrackGroupArrays[AUDIO_RENDERER_INDEX].get(0), audioFormatId)); + includedAdditionalVideoFormats = + selections[VIDEO_RENDERER_INDEX].length() > videoFormatIds.length; + return selections; + } + + private int[] getVideoTrackIndices(TrackGroup trackGroup, int[] formatSupport, + String[] formatIds, boolean canIncludeAdditionalFormats) { + List trackIndices = new ArrayList<>(); + + // Always select explicitly listed representations. + for (String formatId : formatIds) { + int trackIndex = getTrackIndex(trackGroup, formatId); + Log.d(tag, "Adding base video format: " + + Format.toLogString(trackGroup.getFormat(trackIndex))); + trackIndices.add(trackIndex); + } + + // Select additional video representations, if supported by the device. + if (canIncludeAdditionalFormats) { + for (int i = 0; i < trackGroup.length; i++) { + if (!trackIndices.contains(i) && isFormatHandled(formatSupport[i])) { + Log.d(tag, "Adding extra video format: " + + Format.toLogString(trackGroup.getFormat(i))); + trackIndices.add(i); + } + } + } + + int[] trackIndicesArray = Util.toArray(trackIndices); + Arrays.sort(trackIndicesArray); + return trackIndicesArray; + } + + private static int getTrackIndex(TrackGroup trackGroup, String formatId) { + for (int i = 0; i < trackGroup.length; i++) { + if (trackGroup.getFormat(i).id.equals(formatId)) { + return i; + } + } + throw new IllegalStateException("Format " + formatId + " not found."); + } + + private static boolean isFormatHandled(int formatSupport) { + return (formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK) + == RendererCapabilities.FORMAT_HANDLED; + } + + } + +} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java index 5752058c4e..6ae66f24e1 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -15,63 +15,15 @@ */ 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}. @@ -79,147 +31,6 @@ import junit.framework.AssertionFailedError; public final class DashTest extends ActivityInstrumentationTestCase2 { private static final String TAG = "DashTest"; - private static final String VIDEO_TAG = TAG + ":Video"; - private static final String AUDIO_TAG = TAG + ":Audio"; - private static final String REPORT_NAME = "GtsExoPlayerTestCases"; - private static final String REPORT_OBJECT_NAME = "playbacktest"; - private static final int VIDEO_RENDERER_INDEX = 0; - private static final int AUDIO_RENDERER_INDEX = 1; - - private static final long TEST_TIMEOUT_MS = 5 * 60 * 1000; - private static final int MIN_LOADABLE_RETRY_COUNT = 10; - private static final int MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES = 10; - private static final float MAX_DROPPED_VIDEO_FRAME_FRACTION = 0.01f; - - private static final String MANIFEST_URL_PREFIX = "https://storage.googleapis.com/exoplayer-test-" - + "media-1/gen-3/screens/dash-vod-single-segment/"; - // Clear content manifests. - private static final String H264_MANIFEST = "manifest-h264.mpd"; - private static final String H265_MANIFEST = "manifest-h265.mpd"; - private static final String VP9_MANIFEST = "manifest-vp9.mpd"; - private static final String H264_23_MANIFEST = "manifest-h264-23.mpd"; - private static final String H264_24_MANIFEST = "manifest-h264-24.mpd"; - private static final String H264_29_MANIFEST = "manifest-h264-29.mpd"; - // Widevine encrypted content manifests. - private static final String WIDEVINE_H264_MANIFEST_PREFIX = "manifest-h264-enc"; - private static final String WIDEVINE_H265_MANIFEST_PREFIX = "manifest-h265-enc"; - private static final String WIDEVINE_VP9_MANIFEST_PREFIX = "manifest-vp9-enc"; - private static final String WIDEVINE_H264_23_MANIFEST_PREFIX = "manifest-h264-23-enc"; - private static final String WIDEVINE_H264_24_MANIFEST_PREFIX = "manifest-h264-24-enc"; - private static final String WIDEVINE_H264_29_MANIFEST_PREFIX = "manifest-h264-29-enc"; - private static final String WIDEVINE_L1_SUFFIX = "-hw.mpd"; - private static final String WIDEVINE_L3_SUFFIX = "-sw.mpd"; - - private static final String AAC_AUDIO_REPRESENTATION_ID = "141"; - private static final String H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "avc-baseline-240"; - private static final String H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "avc-baseline-480"; - private static final String H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "avc-main-240"; - private static final String H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "avc-main-480"; - // The highest quality H264 format mandated by the Android CDD. - private static final String H264_CDD_FIXED = Util.SDK_INT < 23 - ? H264_BASELINE_480P_VIDEO_REPRESENTATION_ID : H264_MAIN_480P_VIDEO_REPRESENTATION_ID; - // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile - // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 - // when switching between baseline and main profiles on certain devices. - private static final String[] H264_CDD_ADAPTIVE = Util.SDK_INT < 24 - ? new String[] { - H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} - : new String[] { - H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, - H264_MAIN_240P_VIDEO_REPRESENTATION_ID, - H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; - - private static final String H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = - "avc-baseline-480-23"; - private static final String H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = - "avc-baseline-480-24"; - private static final String H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = - "avc-baseline-480-29"; - - private static final String H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "hevc-main-288"; - private static final String H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "hevc-main-360"; - // The highest quality H265 format mandated by the Android CDD. - private static final String H265_CDD_FIXED = H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; - // Multiple H265 formats mandated by the Android CDD. - private static final String[] H265_CDD_ADAPTIVE = - new String[] { - H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, - H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; - - private static final String VORBIS_AUDIO_REPRESENTATION_ID = "4"; - private static final String VP9_180P_VIDEO_REPRESENTATION_ID = "0"; - private static final String VP9_360P_VIDEO_REPRESENTATION_ID = "1"; - // The highest quality VP9 format mandated by the Android CDD. - private static final String VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; - // Multiple VP9 formats mandated by the Android CDD. - private static final String[] VP9_CDD_ADAPTIVE = - new String[] { - VP9_180P_VIDEO_REPRESENTATION_ID, - VP9_360P_VIDEO_REPRESENTATION_ID}; - - // Widevine encrypted content representation ids. - private static final String WIDEVINE_AAC_AUDIO_REPRESENTATION_ID = "0"; - private static final String WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "1"; - private static final String WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "2"; - private static final String WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "3"; - private static final String WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "4"; - // The highest quality H264 format mandated by the Android CDD. - private static final String WIDEVINE_H264_CDD_FIXED = Util.SDK_INT < 23 - ? WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID - : WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID; - // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile - // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 - // when switching between baseline and main profiles on certain devices. - private static final String[] WIDEVINE_H264_CDD_ADAPTIVE = Util.SDK_INT < 24 - ? new String[] { - WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} - : new String[] { - WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; - - private static final String WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = "2"; - private static final String WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = "2"; - private static final String WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = "2"; - - private static final String WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "1"; - private static final String WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "2"; - // The highest quality H265 format mandated by the Android CDD. - private static final String WIDEVINE_H265_CDD_FIXED = - WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; - // Multiple H265 formats mandated by the Android CDD. - private static final String[] WIDEVINE_H265_CDD_ADAPTIVE = - new String[] { - WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, - WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; - - private static final String WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID = "0"; - private static final String WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID = "1"; - private static final String WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID = "2"; - // The highest quality VP9 format mandated by the Android CDD. - private static final String WIDEVINE_VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; - // Multiple VP9 formats mandated by the Android CDD. - private static final String[] WIDEVINE_VP9_CDD_ADAPTIVE = - new String[] { - WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID, - WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID}; - - private static final String WIDEVINE_LICENSE_URL = - "https://proxy.uat.widevine.com/proxy?provider=widevine_test&video_id="; - private static final String WIDEVINE_SW_CRYPTO_CONTENT_ID = "exoplayer_test_1"; - private static final String WIDEVINE_HW_SECURE_DECODE_CONTENT_ID = "exoplayer_test_2"; - private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); - private static final String WIDEVINE_SECURITY_LEVEL_1 = "L1"; - private static final String WIDEVINE_SECURITY_LEVEL_3 = "L3"; - private static final String SECURITY_LEVEL_PROPERTY = "securityLevel"; - - // Whether adaptive tests should enable video formats beyond those mandated by the Android CDD - // if the device advertises support for them. - private static final boolean ALLOW_ADDITIONAL_VIDEO_FORMATS = Util.SDK_INT >= 24; private static final ActionSchedule SEEKING_SCHEDULE = new ActionSchedule.Builder(TAG) .delay(10000).seek(15000) @@ -229,33 +40,33 @@ public final class DashTest extends ActivityInstrumentationTestCase2 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(); - } + new DashHostedTest.Builder(TAG) + .setStreamName("test_widevine_29fps_h264_fixed") + .setManifestUrlForWidevine(DashTestData.WIDEVINE_H264_29_MANIFEST_PREFIX, + MimeTypes.VIDEO_H264) + .setFullPlaybackNoSeeking(true) + .setCanIncludeAdditionalVideoFormats(false) + .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, + DashTestData.WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID) + .runTest(getActivity(), getInstrumentation()); } // 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); @@ -778,332 +582,4 @@ public final class DashTest extends ActivityInstrumentationTestCase2 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; - } - - } - } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java new file mode 100644 index 0000000000..c95614bc87 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestData.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.playbacktests.gts; + +import com.google.android.exoplayer2.util.Util; + +/** + * Test data for {@link DashTest} and {@link DashWidevineOfflineTest). + */ +public final class DashTestData { + + // Clear content manifests. + public static final String H264_MANIFEST = "manifest-h264.mpd"; + public static final String H265_MANIFEST = "manifest-h265.mpd"; + public static final String VP9_MANIFEST = "manifest-vp9.mpd"; + public static final String H264_23_MANIFEST = "manifest-h264-23.mpd"; + public static final String H264_24_MANIFEST = "manifest-h264-24.mpd"; + public static final String H264_29_MANIFEST = "manifest-h264-29.mpd"; + // Widevine encrypted content manifests. + public static final String WIDEVINE_H264_MANIFEST_PREFIX = "manifest-h264-enc"; + public static final String WIDEVINE_H265_MANIFEST_PREFIX = "manifest-h265-enc"; + public static final String WIDEVINE_VP9_MANIFEST_PREFIX = "manifest-vp9-enc"; + public static final String WIDEVINE_H264_23_MANIFEST_PREFIX = "manifest-h264-23-enc"; + public static final String WIDEVINE_H264_24_MANIFEST_PREFIX = "manifest-h264-24-enc"; + public static final String WIDEVINE_H264_29_MANIFEST_PREFIX = "manifest-h264-29-enc"; + + public static final String AAC_AUDIO_REPRESENTATION_ID = "141"; + public static final String H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "avc-baseline-240"; + public static final String H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "avc-baseline-480"; + public static final String H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "avc-main-240"; + public static final String H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "avc-main-480"; + // The highest quality H264 format mandated by the Android CDD. + public static final String H264_CDD_FIXED = Util.SDK_INT < 23 + ? H264_BASELINE_480P_VIDEO_REPRESENTATION_ID : H264_MAIN_480P_VIDEO_REPRESENTATION_ID; + // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile + // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 + // when switching between baseline and main profiles on certain devices. + public static final String[] H264_CDD_ADAPTIVE = Util.SDK_INT < 24 + ? new String[] { + H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} + : new String[] { + H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, + H264_MAIN_240P_VIDEO_REPRESENTATION_ID, + H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; + + public static final String H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = + "avc-baseline-480-23"; + public static final String H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = + "avc-baseline-480-24"; + public static final String H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = + "avc-baseline-480-29"; + + public static final String H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "hevc-main-288"; + public static final String H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "hevc-main-360"; + // The highest quality H265 format mandated by the Android CDD. + public static final String H265_CDD_FIXED = H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; + // Multiple H265 formats mandated by the Android CDD. + public static final String[] H265_CDD_ADAPTIVE = + new String[] { + H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, + H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; + + public static final String VORBIS_AUDIO_REPRESENTATION_ID = "4"; + public static final String VP9_180P_VIDEO_REPRESENTATION_ID = "0"; + public static final String VP9_360P_VIDEO_REPRESENTATION_ID = "1"; + // The highest quality VP9 format mandated by the Android CDD. + public static final String VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; + // Multiple VP9 formats mandated by the Android CDD. + public static final String[] VP9_CDD_ADAPTIVE = + new String[] { + VP9_180P_VIDEO_REPRESENTATION_ID, + VP9_360P_VIDEO_REPRESENTATION_ID}; + + // Widevine encrypted content representation ids. + public static final String WIDEVINE_AAC_AUDIO_REPRESENTATION_ID = "0"; + public static final String WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID = "1"; + public static final String WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID = "3"; + public static final String WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID = "4"; + // The highest quality H264 format mandated by the Android CDD. + public static final String WIDEVINE_H264_CDD_FIXED = Util.SDK_INT < 23 + ? WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID + : WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID; + // Multiple H264 formats mandated by the Android CDD. Note: The CDD actually mandated main profile + // support from API level 23, but we opt to test only from 24 due to known issues on API level 23 + // when switching between baseline and main profiles on certain devices. + public static final String[] WIDEVINE_H264_CDD_ADAPTIVE = Util.SDK_INT < 24 + ? new String[] { + WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID} + : new String[] { + WIDEVINE_H264_BASELINE_240P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_BASELINE_480P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_MAIN_240P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H264_MAIN_480P_VIDEO_REPRESENTATION_ID}; + + public static final String WIDEVINE_H264_BASELINE_480P_23FPS_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H264_BASELINE_480P_24FPS_VIDEO_REPRESENTATION_ID = "2"; + public static final String WIDEVINE_H264_BASELINE_480P_29FPS_VIDEO_REPRESENTATION_ID = "2"; + + public static final String WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID = "1"; + public static final String WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID = "2"; + // The highest quality H265 format mandated by the Android CDD. + public static final String WIDEVINE_H265_CDD_FIXED = + WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID; + // Multiple H265 formats mandated by the Android CDD. + public static final String[] WIDEVINE_H265_CDD_ADAPTIVE = + new String[] { + WIDEVINE_H265_BASELINE_288P_VIDEO_REPRESENTATION_ID, + WIDEVINE_H265_BASELINE_360P_VIDEO_REPRESENTATION_ID}; + + public static final String WIDEVINE_VORBIS_AUDIO_REPRESENTATION_ID = "0"; + public static final String WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID = "1"; + public static final String WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID = "2"; + // The highest quality VP9 format mandated by the Android CDD. + public static final String WIDEVINE_VP9_CDD_FIXED = VP9_360P_VIDEO_REPRESENTATION_ID; + // Multiple VP9 formats mandated by the Android CDD. + public static final String[] WIDEVINE_VP9_CDD_ADAPTIVE = + new String[] { + WIDEVINE_VP9_180P_VIDEO_REPRESENTATION_ID, + WIDEVINE_VP9_360P_VIDEO_REPRESENTATION_ID}; + + private DashTestData() { + } + +} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java new file mode 100644 index 0000000000..3bf9508128 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.playbacktests.gts; + +import android.media.MediaDrm.MediaDrmStateException; +import android.test.ActivityInstrumentationTestCase2; +import android.util.Pair; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.OfflineLicenseHelper; +import com.google.android.exoplayer2.playbacktests.util.ActionSchedule; +import com.google.android.exoplayer2.playbacktests.util.HostActivity; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import junit.framework.Assert; + +/** + * Tests Widevine encrypted DASH playbacks using offline keys. + */ +public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCase2 { + + private static final String TAG = "DashWidevineOfflineTest"; + private static final String USER_AGENT = "ExoPlayerPlaybackTests"; + + private DashHostedTest.Builder builder; + private String widevineManifestUrl; + private DefaultHttpDataSourceFactory httpDataSourceFactory; + private OfflineLicenseHelper offlineLicenseHelper; + private byte[] offlineLicenseKeySetId; + + public DashWidevineOfflineTest() { + super(HostActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + builder = new DashHostedTest.Builder(TAG) + .setStreamName("test_widevine_h264_fixed_offline") + .setManifestUrlForWidevine(DashTestData.WIDEVINE_H264_MANIFEST_PREFIX, MimeTypes.VIDEO_H264) + .setFullPlaybackNoSeeking(true) + .setCanIncludeAdditionalVideoFormats(false) + .setAudioVideoFormats(DashTestData.WIDEVINE_AAC_AUDIO_REPRESENTATION_ID, + DashTestData.WIDEVINE_H264_CDD_FIXED); + + boolean useL1Widevine = DashHostedTest.isL1WidevineAvailable(MimeTypes.VIDEO_H264); + widevineManifestUrl = DashHostedTest + .getWidevineManifestUrl(DashTestData.WIDEVINE_H264_MANIFEST_PREFIX, useL1Widevine); + String widevineLicenseUrl = DashHostedTest.getWidevineLicenseUrl(useL1Widevine); + httpDataSourceFactory = new DefaultHttpDataSourceFactory(USER_AGENT); + offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(widevineLicenseUrl, + httpDataSourceFactory); + } + + @Override + protected void tearDown() throws Exception { + if (offlineLicenseKeySetId != null) { + releaseLicense(); + } + if (offlineLicenseHelper != null) { + offlineLicenseHelper.releaseResources(); + } + super.tearDown(); + } + + // Offline license tests + + public void testWidevineOfflineLicense() throws Exception { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + builder.runTest(getActivity(), getInstrumentation()); + + // Renew license after playback should still work + offlineLicenseKeySetId = offlineLicenseHelper.renew(offlineLicenseKeySetId); + Assert.assertNotNull(offlineLicenseKeySetId); + } + + public void testWidevineOfflineReleasedLicense() throws Throwable { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + releaseLicense(); // keySetId no longer valid. + + try { + builder.runTest(getActivity(), getInstrumentation()); + fail("Playback should fail because the license has been released."); + } catch (Throwable e) { + // Get the root cause + while (true) { + Throwable cause = e.getCause(); + if (cause == null || cause == e) { + break; + } + e = cause; + } + // It should be a MediaDrmStateException instance + if (!(e instanceof MediaDrmStateException)) { + throw e; + } + } + } + + public void testWidevineOfflineExpiredLicense() throws Exception { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + + // Wait until the license expires + long licenseDuration = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId).first; + assertTrue("License duration should be less than 30 sec. " + + "Server settings might have changed.", licenseDuration < 30); + while (licenseDuration > 0) { + synchronized (this) { + wait(licenseDuration * 1000 + 2000); + } + long previousDuration = licenseDuration; + licenseDuration = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId).first; + assertTrue("License duration should be decreasing.", previousDuration > licenseDuration); + } + + // DefaultDrmSessionManager should renew the license and stream play fine + builder.runTest(getActivity(), getInstrumentation()); + } + + public void testWidevineOfflineLicenseExpiresOnPause() throws Exception { + if (Util.SDK_INT < 22) { + return; // Pass. + } + downloadLicense(); + + // During playback pause until the license expires then continue playback + Pair licenseDurationRemainingSec = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId); + long licenseDuration = licenseDurationRemainingSec.first; + assertTrue("License duration should be less than 30 sec. " + + "Server settings might have changed.", licenseDuration < 30); + ActionSchedule schedule = new ActionSchedule.Builder(TAG) + .delay(3000).pause().delay(licenseDuration * 1000 + 2000).play().build(); + + // DefaultDrmSessionManager should renew the license and stream play fine + builder + .setActionSchedule(schedule) + .runTest(getActivity(), getInstrumentation()); + } + + private void downloadLicense() throws InterruptedException, DrmSessionException, IOException { + offlineLicenseKeySetId = offlineLicenseHelper.download( + httpDataSourceFactory.createDataSource(), widevineManifestUrl); + Assert.assertNotNull(offlineLicenseKeySetId); + Assert.assertTrue(offlineLicenseKeySetId.length > 0); + builder.setOfflineLicenseKeySetId(offlineLicenseKeySetId); + } + + private void releaseLicense() throws DrmSessionException { + offlineLicenseHelper.release(offlineLicenseKeySetId); + offlineLicenseKeySetId = null; + } + +} diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java index 7bf8985b64..74262f4422 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/util/ExoHostedTest.java @@ -63,7 +63,8 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen public static final long EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS = -2; public static final long EXPECTED_PLAYING_TIME_UNSET = -1; - private final String tag; + protected final String tag; + private final boolean failOnPlayerError; private final long expectedPlayingTimeMs; private final DecoderCounters videoDecoderCounters; From 7c1b2beb84f6f706490b3e7a241dce87d22ed370 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 31 Jan 2017 08:13:15 -0800 Subject: [PATCH 059/106] Support dyanmically setting key request headers Issue: #1924 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146120465 --- .../exoplayer2/demo/PlayerActivity.java | 24 +++----- .../exoplayer2/drm/HttpMediaDrmCallback.java | 55 +++++++++++++++++-- .../exoplayer2/drm/OfflineLicenseHelper.java | 2 +- .../exoplayer2/upstream/HttpDataSource.java | 18 +++--- 4 files changed, 68 insertions(+), 31 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 bbfadf34af..6c7b72522a 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 @@ -70,8 +70,6 @@ import com.google.android.exoplayer2.util.Util; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; /** @@ -239,19 +237,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay if (drmSchemeUuid != null) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); - Map keyRequestProperties; - if (keyRequestPropertiesArray == null || keyRequestPropertiesArray.length < 2) { - keyRequestProperties = null; - } else { - keyRequestProperties = new HashMap<>(); - for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { - keyRequestProperties.put(keyRequestPropertiesArray[i], - keyRequestPropertiesArray[i + 1]); - } - } try { drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl, - keyRequestProperties); + keyRequestPropertiesArray); } catch (UnsupportedDrmException e) { int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME @@ -349,12 +337,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } private DrmSessionManager buildDrmSessionManager(UUID uuid, - String licenseUrl, Map keyRequestProperties) throws UnsupportedDrmException { + String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { if (Util.SDK_INT < 18) { return null; } HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, - buildHttpDataSourceFactory(false), keyRequestProperties); + buildHttpDataSourceFactory(false)); + if (keyRequestPropertiesArray != null) { + for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { + drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], + keyRequestPropertiesArray[i + 1]); + } + } return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger); } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index e0c9ca5296..f9d5efffb1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -24,6 +24,8 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.HashMap; @@ -57,21 +59,62 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { } /** + * @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request + * properties can be set by calling {@link #setKeyRequestProperty(String, String)}. * @param defaultUrl The default license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @param keyRequestProperties Request properties to set when making key requests, or null. */ + @Deprecated public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory, Map keyRequestProperties) { this.dataSourceFactory = dataSourceFactory; this.defaultUrl = defaultUrl; - this.keyRequestProperties = keyRequestProperties; + this.keyRequestProperties = new HashMap<>(); + if (keyRequestProperties != null) { + this.keyRequestProperties.putAll(keyRequestProperties); + } + } + + /** + * Sets a header for key requests made by the callback. + * + * @param name The name of the header field. + * @param value The value of the field. + */ + public void setKeyRequestProperty(String name, String value) { + Assertions.checkNotNull(name); + Assertions.checkNotNull(value); + synchronized (keyRequestProperties) { + keyRequestProperties.put(name, value); + } + } + + /** + * Clears a header for key requests made by the callback. + * + * @param name The name of the header field. + */ + public void clearKeyRequestProperty(String name) { + Assertions.checkNotNull(name); + synchronized (keyRequestProperties) { + keyRequestProperties.remove(name); + } + } + + /** + * Clears all headers for key requests made by the callback. + */ + public void clearAllKeyRequestProperties() { + synchronized (keyRequestProperties) { + keyRequestProperties.clear(); + } } @Override public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); - return executePost(url, new byte[0], null); + return executePost(dataSourceFactory, url, new byte[0], null); } @Override @@ -85,14 +128,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { if (C.PLAYREADY_UUID.equals(uuid)) { requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES); } - if (keyRequestProperties != null) { + synchronized (keyRequestProperties) { requestProperties.putAll(keyRequestProperties); } - return executePost(url, request.getData(), requestProperties); + return executePost(dataSourceFactory, url, request.getData(), requestProperties); } - private byte[] executePost(String url, byte[] data, Map requestProperties) - throws IOException { + private static byte[] executePost(HttpDataSource.Factory dataSourceFactory, String url, + byte[] data, Map requestProperties) throws IOException { HttpDataSource dataSource = dataSourceFactory.createDataSource(); if (requestProperties != null) { for (Map.Entry requestProperty : requestProperties.entrySet()) { diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index a11d65d4d3..f4a65931b6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -93,7 +93,7 @@ public final class OfflineLicenseHelper { public static OfflineLicenseHelper newWidevineInstance( String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { return newWidevineInstance( - new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null); + new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 8df8624102..a988cf1a33 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -41,8 +41,8 @@ public interface HttpDataSource extends DataSource { HttpDataSource createDataSource(); /** - * Sets a default request header field for {@link HttpDataSource} instances subsequently - * created by the factory. Previously created instances are not affected. + * Sets a default request header for {@link HttpDataSource} instances subsequently created by + * the factory. Previously created instances are not affected. * * @param name The name of the header field. * @param value The value of the field. @@ -50,16 +50,16 @@ public interface HttpDataSource extends DataSource { void setDefaultRequestProperty(String name, String value); /** - * Clears a default request header field for {@link HttpDataSource} instances subsequently - * created by the factory. Previously created instances are not affected. + * Clears a default request header for {@link HttpDataSource} instances subsequently created by + * the factory. Previously created instances are not affected. * * @param name The name of the header field. */ void clearDefaultRequestProperty(String name); /** - * Clears all default request header fields for all {@link HttpDataSource} instances - * subsequently created by the factory. Previously created instances are not affected. + * Clears all default request header for all {@link HttpDataSource} instances subsequently + * created by the factory. Previously created instances are not affected. */ void clearAllDefaultRequestProperties(); @@ -232,7 +232,7 @@ public interface HttpDataSource extends DataSource { int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException; /** - * Sets the value of a request header field. The value will be used for subsequent connections + * Sets the value of a request header. The value will be used for subsequent connections * established by the source. * * @param name The name of the header field. @@ -241,7 +241,7 @@ public interface HttpDataSource extends DataSource { void setRequestProperty(String name, String value); /** - * Clears the value of a request header field. The change will apply to subsequent connections + * Clears the value of a request header. The change will apply to subsequent connections * established by the source. * * @param name The name of the header field. @@ -249,7 +249,7 @@ public interface HttpDataSource extends DataSource { void clearRequestProperty(String name); /** - * Clears all request header fields that were set by {@link #setRequestProperty(String, String)}. + * Clears all request headers that were set by {@link #setRequestProperty(String, String)}. */ void clearAllRequestProperties(); From 3edeec2495d9ca2c7f60b9edb212ecf5e6b40103 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 31 Jan 2017 09:28:16 -0800 Subject: [PATCH 060/106] Document passing null cacheWriteDataSink to CacheDataSource constructor ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146128328 --- .../upstream/cache/CacheDataSourceTest.java | 27 ++++++++++++++----- .../upstream/cache/CacheDataSource.java | 13 ++++++--- 2 files changed, 29 insertions(+), 11 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 067cfe4fcd..a5b272cebd 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -20,9 +20,9 @@ import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSource; -import com.google.android.exoplayer2.testutil.FakeDataSource.Builder; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.FileDataSource; import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -126,9 +126,15 @@ public class CacheDataSourceTest extends InstrumentationTestCase { MoreAsserts.assertEmpty(simpleCache.getKeys()); } + public void testReadOnlyCache() throws Exception { + CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null); + assertReadDataContentLength(cacheDataSource, false, false); + assertEquals(0, cacheDir.list().length); + } + private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength) throws IOException { - // Read all data from upstream and cache + // Read all data from upstream and write to cache CacheDataSource cacheDataSource = createCacheDataSource(false, simulateUnknownLength); assertReadDataContentLength(cacheDataSource, unboundedRequest, simulateUnknownLength); @@ -184,14 +190,21 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private CacheDataSource createCacheDataSource(boolean setReadException, boolean simulateUnknownLength, @CacheDataSource.Flags int flags) { - Builder builder = new Builder(); + return createCacheDataSource(setReadException, simulateUnknownLength, flags, + new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE)); + } + + private CacheDataSource createCacheDataSource(boolean setReadException, + boolean simulateUnknownLength, @CacheDataSource.Flags int flags, + CacheDataSink cacheWriteDataSink) { + FakeDataSource.Builder builder = new FakeDataSource.Builder(); if (setReadException) { builder.appendReadError(new IOException("Shouldn't read from upstream")); } - builder.setSimulateUnknownLength(simulateUnknownLength); - builder.appendReadData(TEST_DATA); - FakeDataSource upstream = builder.build(); - return new CacheDataSource(simpleCache, upstream, flags, MAX_CACHE_FILE_SIZE); + FakeDataSource upstream = + builder.setSimulateUnknownLength(simulateUnknownLength).appendReadData(TEST_DATA).build(); + return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink, + flags, null); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 9b29984d06..dc8797362f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -142,7 +142,8 @@ public final class CacheDataSource implements DataSource { * @param cache The cache. * @param upstream A {@link DataSource} for reading data not in the cache. * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. - * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. + * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is + * accessed read-only. * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. * @param eventListener An optional {@link EventListener} to receive events. @@ -283,7 +284,6 @@ public final class CacheDataSource implements DataSource { currentDataSource = cacheReadDataSource; } else { // Data is not cached, and data is not locked, read from upstream with cache backing. - lockedSpan = span; long length; if (span.isOpenEnded()) { length = bytesRemaining; @@ -294,8 +294,13 @@ public final class CacheDataSource implements DataSource { } } dataSpec = new DataSpec(uri, readPosition, length, key, flags); - currentDataSource = cacheWriteDataSource != null ? cacheWriteDataSource - : upstreamDataSource; + if (cacheWriteDataSource != null) { + currentDataSource = cacheWriteDataSource; + lockedSpan = span; + } else { + currentDataSource = upstreamDataSource; + cache.releaseHoleSpan(span); + } } currentRequestUnbounded = dataSpec.length == C.LENGTH_UNSET; From 4301606200d63462a109a94233b304839bbcc8b0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 31 Jan 2017 09:29:09 -0800 Subject: [PATCH 061/106] Add a BufferProcessor for resampling. This initial version of the BufferProcessor interface assumes that buffers are handled in their entirety on each invocation. Move PCM resampling out of AudioTrack into a BufferProcessor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146128411 --- .../android/exoplayer2/audio/AudioTrack.java | 215 ++++++------------ .../exoplayer2/audio/BufferProcessor.java | 37 +++ .../audio/ResamplingBufferProcessor.java | 112 +++++++++ 3 files changed, 218 insertions(+), 146 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java 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 71049c9de8..11c388fdab 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -25,7 +25,6 @@ import android.os.ConditionVariable; import android.os.SystemClock; import android.util.Log; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -271,9 +270,9 @@ public final class AudioTrack { @C.StreamType private int streamType; @C.Encoding - private int sourceEncoding; + private int inputEncoding; @C.Encoding - private int targetEncoding; + private int outputEncoding; private boolean passthrough; private int pcmFrameSize; private int bufferSize; @@ -299,12 +298,12 @@ public final class AudioTrack { private long latencyUs; private float volume; - private byte[] temporaryBuffer; - private int temporaryBufferOffset; - private ByteBuffer currentSourceBuffer; + private ByteBuffer inputBuffer; + private ByteBuffer outputBuffer; + private byte[] preV21OutputBuffer; + private int preV21OutputBufferOffset; - private ByteBuffer resampledBuffer; - private boolean useResampledBuffer; + private BufferProcessor resampler; private boolean playing; private int audioSessionId; @@ -470,17 +469,17 @@ public final class AudioTrack { channelConfig = AudioFormat.CHANNEL_OUT_STEREO; } - @C.Encoding int sourceEncoding; + @C.Encoding int inputEncoding; if (passthrough) { - sourceEncoding = getEncodingForMimeType(mimeType); + inputEncoding = getEncodingForMimeType(mimeType); } else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT || pcmEncoding == C.ENCODING_PCM_24BIT || pcmEncoding == C.ENCODING_PCM_32BIT) { - sourceEncoding = pcmEncoding; + inputEncoding = pcmEncoding; } else { throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding); } - if (isInitialized() && this.sourceEncoding == sourceEncoding && this.sampleRate == sampleRate + if (isInitialized() && this.inputEncoding == inputEncoding && this.sampleRate == sampleRate && this.channelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; @@ -488,28 +487,31 @@ public final class AudioTrack { reset(); - this.sourceEncoding = sourceEncoding; + this.inputEncoding = inputEncoding; this.passthrough = passthrough; this.sampleRate = sampleRate; this.channelConfig = channelConfig; - targetEncoding = passthrough ? sourceEncoding : C.ENCODING_PCM_16BIT; pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels. + outputEncoding = passthrough ? inputEncoding : C.ENCODING_PCM_16BIT; + + resampler = outputEncoding != inputEncoding ? new ResamplingBufferProcessor(inputEncoding) + : null; if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; } else if (passthrough) { // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into // account. [Internal: b/25181305] - if (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3) { + if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { // AC-3 allows bitrates up to 640 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND); - } else /* (targetEncoding == C.ENCODING_DTS || targetEncoding == C.ENCODING_DTS_HD */ { + } else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ { // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); } } else { int minBufferSize = - android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, targetEncoding); + android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * pcmFrameSize; @@ -531,15 +533,15 @@ public final class AudioTrack { releasingConditionVariable.block(); if (tunneling) { - audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding, + audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding, bufferSize, audioSessionId); } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - targetEncoding, bufferSize, MODE_STREAM); + outputEncoding, bufferSize, MODE_STREAM); } else { // Re-attach to the same audio session. audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - targetEncoding, bufferSize, MODE_STREAM, audioSessionId); + outputEncoding, bufferSize, MODE_STREAM, audioSessionId); } checkAudioTrackInitialized(); @@ -611,8 +613,10 @@ public final class AudioTrack { * @throws InitializationException If an error occurs initializing the track. * @throws WriteException If an error occurs writing the audio data. */ + @SuppressWarnings("ReferenceEquality") public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws InitializationException, WriteException { + Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer); if (!isInitialized()) { initialize(); if (playing) { @@ -620,27 +624,12 @@ public final class AudioTrack { } } - boolean hadData = hasData; - hasData = hasPendingData(); - if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { - long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; - listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); - } - boolean result = writeBuffer(buffer, presentationTimeUs); - lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); - return result; - } - - @SuppressWarnings("ReferenceEquality") - private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { - boolean isNewSourceBuffer = currentSourceBuffer == null; - Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer); - currentSourceBuffer = buffer; - if (needsPassthroughWorkarounds()) { // An AC-3 audio track continues to play data written while it is paused. Stop writing so its // buffer empties. See [Internal: b/18899620]. if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) { + // We force an underrun to pause the track, so don't notify the listener in this case. + hasData = false; return false; } @@ -653,27 +642,25 @@ public final class AudioTrack { } } - if (isNewSourceBuffer) { - // We're seeing this buffer for the first time. + boolean hadData = hasData; + hasData = hasPendingData(); + if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { + long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; + listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); + } - if (!currentSourceBuffer.hasRemaining()) { + if (inputBuffer == null) { + // We are seeing this buffer for the first time. + if (!buffer.hasRemaining()) { // The buffer is empty. - currentSourceBuffer = null; return true; } - useResampledBuffer = targetEncoding != sourceEncoding; - if (useResampledBuffer) { - Assertions.checkState(targetEncoding == C.ENCODING_PCM_16BIT); - // Resample the buffer to get the data in the target encoding. - resampledBuffer = resampleTo16BitPcm(currentSourceBuffer, sourceEncoding, resampledBuffer); - buffer = resampledBuffer; - } - if (passthrough && framesPerEncodedSample == 0) { // If this is the first encoded sample, calculate the sample size in frames. - framesPerEncodedSample = getFramesPerEncodedSample(targetEncoding, buffer); + framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); } + if (startMediaTimeState == START_NOT_SET) { startMediaTimeUs = Math.max(0, presentationTimeUs); startMediaTimeState = START_IN_SYNC; @@ -695,21 +682,31 @@ public final class AudioTrack { listener.onPositionDiscontinuity(); } } + + inputBuffer = buffer; + outputBuffer = resampler != null ? resampler.handleBuffer(inputBuffer, outputBuffer) + : inputBuffer; if (Util.SDK_INT < 21) { - // Copy {@code buffer} into {@code temporaryBuffer}. - int bytesRemaining = buffer.remaining(); - if (temporaryBuffer == null || temporaryBuffer.length < bytesRemaining) { - temporaryBuffer = new byte[bytesRemaining]; + int bytesRemaining = outputBuffer.remaining(); + if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { + preV21OutputBuffer = new byte[bytesRemaining]; } - int originalPosition = buffer.position(); - buffer.get(temporaryBuffer, 0, bytesRemaining); - buffer.position(originalPosition); - temporaryBufferOffset = 0; + int originalPosition = outputBuffer.position(); + outputBuffer.get(preV21OutputBuffer, 0, bytesRemaining); + outputBuffer.position(originalPosition); + preV21OutputBufferOffset = 0; } } - buffer = useResampledBuffer ? resampledBuffer : buffer; - int bytesRemaining = buffer.remaining(); + if (writeOutputBuffer(presentationTimeUs)) { + inputBuffer = null; + return true; + } + return false; + } + + private boolean writeOutputBuffer(long presentationTimeUs) throws WriteException { + int bytesRemaining = outputBuffer.remaining(); int bytesWritten = 0; if (Util.SDK_INT < 21) { // passthrough == false // Work out how many bytes we can write without the risk of blocking. @@ -718,18 +715,21 @@ public final class AudioTrack { int bytesToWrite = bufferSize - bytesPending; if (bytesToWrite > 0) { bytesToWrite = Math.min(bytesRemaining, bytesToWrite); - bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite); - if (bytesWritten >= 0) { - temporaryBufferOffset += bytesWritten; + bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite); + if (bytesWritten > 0) { + preV21OutputBufferOffset += bytesWritten; + outputBuffer.position(outputBuffer.position() + bytesWritten); } - buffer.position(buffer.position() + bytesWritten); } + } else if (tunneling) { + bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, outputBuffer, bytesRemaining, + presentationTimeUs); } else { - bytesWritten = tunneling - ? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs) - : writeNonBlockingV21(audioTrack, buffer, bytesRemaining); + bytesWritten = writeNonBlockingV21(audioTrack, outputBuffer, bytesRemaining); } + lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); + if (bytesWritten < 0) { throw new WriteException(bytesWritten); } @@ -741,7 +741,6 @@ public final class AudioTrack { if (passthrough) { submittedEncodedFrames += framesPerEncodedSample; } - currentSourceBuffer = null; return true; } return false; @@ -885,7 +884,7 @@ public final class AudioTrack { submittedPcmBytes = 0; submittedEncodedFrames = 0; framesPerEncodedSample = 0; - currentSourceBuffer = null; + inputBuffer = null; avSyncHeader = null; bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; @@ -1094,7 +1093,7 @@ public final class AudioTrack { */ private boolean needsPassthroughWorkarounds() { return Util.SDK_INT < 23 - && (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3); + && (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3); } /** @@ -1129,82 +1128,6 @@ public final class AudioTrack { sessionId); } - /** - * Converts the provided buffer into 16-bit PCM. - * - * @param buffer The buffer containing the data to convert. - * @param sourceEncoding The data encoding. - * @param out A buffer into which the output should be written, if its capacity is sufficient. - * @return The 16-bit PCM output. Different to the out parameter if null was passed, or if the - * capacity was insufficient for the output. - */ - private static ByteBuffer resampleTo16BitPcm(ByteBuffer buffer, @C.PcmEncoding int sourceEncoding, - ByteBuffer out) { - int offset = buffer.position(); - int limit = buffer.limit(); - int size = limit - offset; - - int resampledSize; - switch (sourceEncoding) { - case C.ENCODING_PCM_8BIT: - resampledSize = size * 2; - break; - case C.ENCODING_PCM_24BIT: - resampledSize = (size / 3) * 2; - break; - case C.ENCODING_PCM_32BIT: - resampledSize = size / 2; - break; - case C.ENCODING_PCM_16BIT: - case C.ENCODING_INVALID: - case Format.NO_VALUE: - default: - // Never happens. - throw new IllegalStateException(); - } - - ByteBuffer resampledBuffer = out; - if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) { - resampledBuffer = ByteBuffer.allocateDirect(resampledSize); - } - resampledBuffer.position(0); - resampledBuffer.limit(resampledSize); - - // Samples are little endian. - switch (sourceEncoding) { - case C.ENCODING_PCM_8BIT: - // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. - for (int i = offset; i < limit; i++) { - resampledBuffer.put((byte) 0); - resampledBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128)); - } - break; - case C.ENCODING_PCM_24BIT: - // 24->16 bit resampling. Drop the least significant byte. - for (int i = offset; i < limit; i += 3) { - resampledBuffer.put(buffer.get(i + 1)); - resampledBuffer.put(buffer.get(i + 2)); - } - break; - case C.ENCODING_PCM_32BIT: - // 32->16 bit resampling. Drop the two least significant bytes. - for (int i = offset; i < limit; i += 4) { - resampledBuffer.put(buffer.get(i + 2)); - resampledBuffer.put(buffer.get(i + 3)); - } - break; - case C.ENCODING_PCM_16BIT: - case C.ENCODING_INVALID: - case Format.NO_VALUE: - default: - // Never happens. - throw new IllegalStateException(); - } - - resampledBuffer.position(0); - return resampledBuffer; - } - @C.Encoding private static int getEncodingForMimeType(String mimeType) { switch (mimeType) { diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java new file mode 100644 index 0000000000..a10e8c05af --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.audio; + +import java.nio.ByteBuffer; + +/** + * Interface for processors of buffers, for use with {@link AudioTrack}. + */ +public interface BufferProcessor { + + /** + * Processes the data in the specified input buffer in its entirety. Populates {@code output} with + * processed data if is not {@code null} and has sufficient capacity. Otherwise a different buffer + * will be populated and returned. + * + * @param input A buffer containing the input data to process. + * @param output A buffer into which the output should be written, if its capacity is sufficient. + * @return The processed output. Different to {@code output} if null was passed, or if its + * capacity was insufficient. + */ + ByteBuffer handleBuffer(ByteBuffer input, ByteBuffer output); + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java new file mode 100644 index 0000000000..f0ea5e60c7 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.audio; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Assertions; +import java.nio.ByteBuffer; + +/** + * A {@link BufferProcessor} that converts PCM input buffers from a specified input bit depth to + * {@link C#ENCODING_PCM_16BIT} in preparation for writing to an {@link android.media.AudioTrack}. + */ +/* package */ final class ResamplingBufferProcessor implements BufferProcessor { + + @C.PcmEncoding + private final int inputEncoding; + + /** + * Creates a new buffer processor for resampling input in the specified encoding. + * + * @param inputEncoding The PCM encoding of input buffers. + * @throws IllegalArgumentException Thrown if the input encoding is not PCM or its bit depth is + * not 8, 24 or 32-bits. + */ + public ResamplingBufferProcessor(@C.PcmEncoding int inputEncoding) { + Assertions.checkArgument(inputEncoding == C.ENCODING_PCM_8BIT + || inputEncoding == C.ENCODING_PCM_24BIT || inputEncoding == C.ENCODING_PCM_32BIT); + this.inputEncoding = inputEncoding; + } + + @Override + public ByteBuffer handleBuffer(ByteBuffer input, ByteBuffer output) { + int offset = input.position(); + int limit = input.limit(); + int size = limit - offset; + + int resampledSize; + switch (inputEncoding) { + case C.ENCODING_PCM_8BIT: + resampledSize = size * 2; + break; + case C.ENCODING_PCM_24BIT: + resampledSize = (size / 3) * 2; + break; + case C.ENCODING_PCM_32BIT: + resampledSize = size / 2; + break; + case C.ENCODING_PCM_16BIT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + ByteBuffer resampledBuffer = output; + if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) { + resampledBuffer = ByteBuffer.allocateDirect(resampledSize); + } + resampledBuffer.position(0); + resampledBuffer.limit(resampledSize); + + // Samples are little endian. + switch (inputEncoding) { + case C.ENCODING_PCM_8BIT: + // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. + for (int i = offset; i < limit; i++) { + resampledBuffer.put((byte) 0); + resampledBuffer.put((byte) ((input.get(i) & 0xFF) - 128)); + } + break; + case C.ENCODING_PCM_24BIT: + // 24->16 bit resampling. Drop the least significant byte. + for (int i = offset; i < limit; i += 3) { + resampledBuffer.put(input.get(i + 1)); + resampledBuffer.put(input.get(i + 2)); + } + break; + case C.ENCODING_PCM_32BIT: + // 32->16 bit resampling. Drop the two least significant bytes. + for (int i = offset; i < limit; i += 4) { + resampledBuffer.put(input.get(i + 2)); + resampledBuffer.put(input.get(i + 3)); + } + break; + case C.ENCODING_PCM_16BIT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + resampledBuffer.position(0); + return resampledBuffer; + } + +} From feeec77407f8c5878ce4294039520d788be45c9d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 31 Jan 2017 12:33:43 -0800 Subject: [PATCH 062/106] Add support for multiple programs in a single TS * Prevents calling endTracks() before all PMTs have been processed. * Adds a unique ID to the format of each track. This allows the track selector to identify which track belongs to each program. The format of each id is "/". Note: This CL will break malformed TS files whose PAT declares more PMTs than it actually contains, which previously were supported. This does not apply for HLS mode. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146151642 --- .../androidTest/assets/ts/sample.ac3.0.dump | 2 +- .../androidTest/assets/ts/sample.adts.0.dump | 4 +- .../androidTest/assets/ts/sample.ps.0.dump | 4 +- .../androidTest/assets/ts/sample.ts.0.dump | 4 +- .../extractor/ts/TsExtractorTest.java | 7 ++- .../exoplayer2/extractor/ts/Ac3Reader.java | 10 ++- .../exoplayer2/extractor/ts/AdtsReader.java | 14 +++-- .../exoplayer2/extractor/ts/DtsReader.java | 7 ++- .../exoplayer2/extractor/ts/H262Reader.java | 12 ++-- .../exoplayer2/extractor/ts/H264Reader.java | 11 +++- .../exoplayer2/extractor/ts/H265Reader.java | 17 +++-- .../exoplayer2/extractor/ts/Id3Reader.java | 7 ++- .../extractor/ts/MpegAudioReader.java | 11 ++-- .../exoplayer2/extractor/ts/SeiReader.java | 4 +- .../extractor/ts/SpliceInfoSectionReader.java | 7 ++- .../exoplayer2/extractor/ts/TsExtractor.java | 63 +++++++++++++------ .../extractor/ts/TsPayloadReader.java | 62 +++++++++++++++--- .../exoplayer2/util/TimestampAdjuster.java | 2 +- 18 files changed, 175 insertions(+), 73 deletions(-) diff --git a/library/src/androidTest/assets/ts/sample.ac3.0.dump b/library/src/androidTest/assets/ts/sample.ac3.0.dump index c5f241950b..1b6c77efb6 100644 --- a/library/src/androidTest/assets/ts/sample.ac3.0.dump +++ b/library/src/androidTest/assets/ts/sample.ac3.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 1 track 0: format: bitrate = -1 - id = null + id = 0 containerMimeType = null sampleMimeType = audio/ac3 maxInputSize = -1 diff --git a/library/src/androidTest/assets/ts/sample.adts.0.dump b/library/src/androidTest/assets/ts/sample.adts.0.dump index 3325abcfeb..0a7427d3f1 100644 --- a/library/src/androidTest/assets/ts/sample.adts.0.dump +++ b/library/src/androidTest/assets/ts/sample.adts.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 2 track 0: format: bitrate = -1 - id = null + id = 0 containerMimeType = null sampleMimeType = audio/mp4a-latm maxInputSize = -1 @@ -606,7 +606,7 @@ track 0: track 1: format: bitrate = -1 - id = null + id = 1 containerMimeType = null sampleMimeType = application/id3 maxInputSize = -1 diff --git a/library/src/androidTest/assets/ts/sample.ps.0.dump b/library/src/androidTest/assets/ts/sample.ps.0.dump index 48127ce1c6..3b44fb6fb9 100644 --- a/library/src/androidTest/assets/ts/sample.ps.0.dump +++ b/library/src/androidTest/assets/ts/sample.ps.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 2 track 192: format: bitrate = -1 - id = null + id = 192 containerMimeType = null sampleMimeType = audio/mpeg-L2 maxInputSize = 4096 @@ -45,7 +45,7 @@ track 192: track 224: format: bitrate = -1 - id = null + id = 224 containerMimeType = null sampleMimeType = video/mpeg2 maxInputSize = -1 diff --git a/library/src/androidTest/assets/ts/sample.ts.0.dump b/library/src/androidTest/assets/ts/sample.ts.0.dump index 8b0da7bd02..26c6665aaa 100644 --- a/library/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/src/androidTest/assets/ts/sample.ts.0.dump @@ -6,7 +6,7 @@ numberOfTracks = 2 track 256: format: bitrate = -1 - id = null + id = 1/256 containerMimeType = null sampleMimeType = video/mpeg2 maxInputSize = -1 @@ -38,7 +38,7 @@ track 256: track 257: format: bitrate = -1 - id = null + id = 1/257 containerMimeType = null sampleMimeType = audio/mpeg-L2 maxInputSize = 4096 diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 2dce742158..74e0748119 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 @@ -92,7 +92,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { TrackOutput trackOutput = reader.getTrackOutput(); assertTrue(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */)); assertEquals( - Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, "und", null, 0), + Format.createTextSampleFormat("1/257", "mime", null, 0, 0, "und", null, 0), ((FakeTrackOutput) trackOutput).format); } @@ -178,8 +178,9 @@ public final class TsExtractorTest extends InstrumentationTestCase { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId()); + output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), "mime", null, 0, 0, language, null, 0)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index 52faa8c673..afef154ed4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final ParsableByteArray headerScratchBytes; private final String language; + private String trackFormatId; private TrackOutput output; private int state; @@ -84,7 +85,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { - output = extractorOutput.track(generator.getNextId()); + generator.generateNewId(); + trackFormatId = generator.getFormatId(); + output = extractorOutput.track(generator.getTrackId()); } @Override @@ -180,8 +183,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; headerScratchBits.skipBits(40); isEac3 = headerScratchBits.readBits(5) == 16; headerScratchBits.setPosition(headerScratchBits.getPosition() - 45); - format = isEac3 ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, null, language , null) - : Ac3Util.parseAc3SyncframeFormat(headerScratchBits, null, language, null); + format = isEac3 + ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, trackFormatId, language , null) + : Ac3Util.parseAc3SyncframeFormat(headerScratchBits, trackFormatId, language, null); output.format(format); } sampleSize = isEac3 ? Ac3Util.parseEAc3SyncframeSize(headerScratchBits.data) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 47cb217fc7..56793119e4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -61,6 +61,7 @@ import java.util.Collections; private final ParsableByteArray id3HeaderBuffer; private final String language; + private String formatId; private TrackOutput output; private TrackOutput id3Output; @@ -108,11 +109,14 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId()); if (exposeId3) { - id3Output = extractorOutput.track(idGenerator.getNextId()); - id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, - Format.NO_VALUE, null)); + idGenerator.generateNewId(); + id3Output = extractorOutput.track(idGenerator.getTrackId()); + id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(), + MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); } else { id3Output = new DummyTrackOutput(); } @@ -300,7 +304,7 @@ import java.util.Collections; Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( audioSpecificConfig); - Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, + Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first, Collections.singletonList(audioSpecificConfig), null, 0, language); // In this class a sample is an access unit, but the MediaFormat sample rate specifies the diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 9707685295..50be258ae5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final ParsableByteArray headerScratchBytes; private final String language; + private String formatId; private TrackOutput output; private int state; @@ -79,7 +80,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId()); } @Override @@ -165,7 +168,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private void parseHeader() { byte[] frameData = headerScratchBytes.data; if (format == null) { - format = DtsUtil.parseDtsFormat(frameData, null, language, null); + format = DtsUtil.parseDtsFormat(frameData, formatId, language, null); output.format(format); } sampleSize = DtsUtil.getDtsFrameSize(frameData); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index 02ea6d7c4e..df6ba208c3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -37,6 +37,7 @@ import java.util.Collections; private static final int START_EXTENSION = 0xB5; private static final int START_GROUP = 0xB8; + private String formatId; private TrackOutput output; // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. @@ -78,7 +79,9 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId()); } @Override @@ -126,7 +129,7 @@ import java.util.Collections; int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { // The csd data is complete, so we can decode and output the media format. - Pair result = parseCsdBuffer(csdBuffer); + Pair result = parseCsdBuffer(csdBuffer, formatId); output.format(result.first); frameDurationUs = result.second; hasOutputFormat = true; @@ -166,10 +169,11 @@ import java.util.Collections; * Parses the {@link Format} and frame duration from a csd buffer. * * @param csdBuffer The csd buffer. + * @param formatId The id for the generated format. May be null. * @return A pair consisting of the {@link Format} and the frame duration in microseconds, or * 0 if the duration could not be determined. */ - private static Pair parseCsdBuffer(CsdBuffer csdBuffer) { + private static Pair parseCsdBuffer(CsdBuffer csdBuffer, String formatId) { byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); int firstByte = csdData[4] & 0xFF; @@ -195,7 +199,7 @@ import java.util.Collections; break; } - Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_MPEG2, null, + Format format = Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_MPEG2, null, Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, Collections.singletonList(csdData), Format.NO_VALUE, pixelWidthHeightRatio, null); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index ed4682d9b9..0de6bdeaf9 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -47,6 +47,7 @@ import java.util.List; private long totalBytesWritten; private final boolean[] prefixFlags; + private String formatId; private TrackOutput output; private SeiReader seiReader; private SampleReader sampleReader; @@ -88,9 +89,13 @@ import java.util.List; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId()); sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); + idGenerator.generateNewId(); + seiReader = new SeiReader(extractorOutput.track(idGenerator.getTrackId()), + idGenerator.getFormatId()); } @Override @@ -175,7 +180,7 @@ import java.util.List; initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength)); NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(sps.nalData, 3, sps.nalLength); NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(pps.nalData, 3, pps.nalLength); - output.format(Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, + output.format(Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, spsData.width, spsData.height, Format.NO_VALUE, initializationData, Format.NO_VALUE, spsData.pixelWidthAspectRatio, null)); hasOutputFormat = true; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index a78169a054..0f8a7745a5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -44,6 +44,7 @@ import java.util.Collections; private static final int PREFIX_SEI_NUT = 39; private static final int SUFFIX_SEI_NUT = 40; + private String formatId; private TrackOutput output; private SampleReader sampleReader; private SeiReader seiReader; @@ -90,9 +91,13 @@ import java.util.Collections; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId()); sampleReader = new SampleReader(output); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); + idGenerator.generateNewId(); + seiReader = new SeiReader(extractorOutput.track(idGenerator.getTrackId()), + idGenerator.getFormatId()); } @Override @@ -183,7 +188,7 @@ import java.util.Collections; sps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding); if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) { - output.format(parseMediaFormat(vps, sps, pps)); + output.format(parseMediaFormat(formatId, vps, sps, pps)); hasOutputFormat = true; } } @@ -205,8 +210,8 @@ import java.util.Collections; } } - private static Format parseMediaFormat(NalUnitTargetBuffer vps, NalUnitTargetBuffer sps, - NalUnitTargetBuffer pps) { + private static Format parseMediaFormat(String formatId, NalUnitTargetBuffer vps, + NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) { // Build codec-specific data. byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength); @@ -311,7 +316,7 @@ import java.util.Collections; } } - return Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H265, null, Format.NO_VALUE, + return Format.createVideoSampleFormat(formatId, MimeTypes.VIDEO_H265, null, Format.NO_VALUE, Format.NO_VALUE, picWidthInLumaSamples, picHeightInLumaSamples, Format.NO_VALUE, Collections.singletonList(csd), Format.NO_VALUE, pixelWidthHeightRatio, null); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index c19bc9d14e..27eb2a1bb4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -56,9 +56,10 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, - null)); + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId()); + output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3, + null, Format.NO_VALUE, null)); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index c67e7ad0ab..ae7edc51e4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final MpegAudioHeader header; private final String language; + private String formatId; private TrackOutput output; private int state; @@ -76,7 +77,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { - output = extractorOutput.track(idGenerator.getNextId()); + idGenerator.generateNewId(); + formatId = idGenerator.getFormatId(); + output = extractorOutput.track(idGenerator.getTrackId()); } @Override @@ -176,9 +179,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; frameSize = header.frameSize; if (!hasOutputFormat) { frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate; - Format format = Format.createAudioSampleFormat(null, header.mimeType, null, Format.NO_VALUE, - MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, null, null, 0, - language); + Format format = Format.createAudioSampleFormat(formatId, header.mimeType, null, + Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, header.channels, header.sampleRate, + null, null, 0, language); output.format(format); hasOutputFormat = true; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index 6e2e42d8e2..471c585277 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 @@ -28,9 +28,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final TrackOutput output; - public SeiReader(TrackOutput output) { + public SeiReader(TrackOutput output, String formatId) { this.output = output; - output.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null, + output.format(Format.createTextSampleFormat(formatId, MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); } 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 057fa636ce..625bb70560 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 @@ -36,9 +36,10 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, TsPayloadReader.TrackIdGenerator idGenerator) { this.timestampAdjuster = timestampAdjuster; - output = extractorOutput.track(idGenerator.getNextId()); - output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, null, - Format.NO_VALUE, null)); + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId()); + output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35, + null, Format.NO_VALUE, null)); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 61d66afbc2..99f5d0832e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -34,7 +34,10 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * Facilitates the extraction of data from the MPEG-2 TS container format. @@ -79,7 +82,7 @@ public final class TsExtractor implements Extractor { private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT; private final boolean hlsMode; - private final TimestampAdjuster timestampAdjuster; + private final List timestampAdjusters; private final ParsableByteArray tsPacketBuffer; private final ParsableBitArray tsScratch; private final SparseIntArray continuityCounters; @@ -89,18 +92,12 @@ public final class TsExtractor implements Extractor { // Accessed only by the loading thread. private ExtractorOutput output; + private int remainingPmts; private boolean tracksEnded; private TsPayloadReader id3Reader; public TsExtractor() { - this(new TimestampAdjuster(0)); - } - - /** - * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. - */ - public TsExtractor(TimestampAdjuster timestampAdjuster) { - this(timestampAdjuster, new DefaultTsPayloadReaderFactory(), false); + this(new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory(), false); } /** @@ -111,7 +108,12 @@ public final class TsExtractor implements Extractor { */ public TsExtractor(TimestampAdjuster timestampAdjuster, TsPayloadReader.Factory payloadReaderFactory, boolean hlsMode) { - this.timestampAdjuster = timestampAdjuster; + if (hlsMode) { + timestampAdjusters = Collections.singletonList(timestampAdjuster); + } else { + timestampAdjusters = new ArrayList<>(); + timestampAdjusters.add(timestampAdjuster); + } this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); this.hlsMode = hlsMode; tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); @@ -150,7 +152,10 @@ public final class TsExtractor implements Extractor { @Override public void seek(long position, long timeUs) { - timestampAdjuster.reset(); + int timestampAdjustersCount = timestampAdjusters.size(); + for (int i = 0; i < timestampAdjustersCount; i++) { + timestampAdjusters.get(i).reset(); + } tsPacketBuffer.reset(); continuityCounters.clear(); // Elementary stream readers' state should be cleared to get consistent behaviours when seeking. @@ -307,8 +312,12 @@ public final class TsExtractor implements Extractor { } else { int pid = patScratch.readBits(13); tsPayloadReaders.put(pid, new SectionReader(new PmtReader(pid))); + remainingPmts++; } } + if (!hlsMode) { + tsPayloadReaders.remove(TS_PAT_PID); + } } } @@ -345,10 +354,21 @@ public final class TsExtractor implements Extractor { // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment. return; } - // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), program_number (16), - // reserved (2), version_number (5), current_next_indicator (1), // section_number (8), + // TimestampAdjuster assignment. + TimestampAdjuster timestampAdjuster; + if (hlsMode || remainingPmts == 1) { + timestampAdjuster = timestampAdjusters.get(0); + } else { + timestampAdjuster = new TimestampAdjuster(timestampAdjusters.get(0).firstSampleTimestampUs); + timestampAdjusters.add(timestampAdjuster); + } + + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12) + sectionData.skipBytes(2); + int programNumber = sectionData.readUnsignedShort(); + // reserved (2), version_number (5), current_next_indicator (1), section_number (8), // last_section_number (8), reserved (3), PCR_PID (13) - sectionData.skipBytes(9); + sectionData.skipBytes(5); // Read program_info_length. sectionData.readBytes(pmtScratch, 2); @@ -364,7 +384,7 @@ public final class TsExtractor implements Extractor { EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]); id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo); id3Reader.init(timestampAdjuster, output, - new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); + new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); } int remainingEntriesLength = sectionData.bytesLeft(); @@ -393,7 +413,8 @@ public final class TsExtractor implements Extractor { } else { reader = payloadReaderFactory.createPayloadReader(streamType, esInfo); if (reader != null) { - reader.init(timestampAdjuster, output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE)); + reader.init(timestampAdjuster, output, + new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE)); } } @@ -404,13 +425,17 @@ public final class TsExtractor implements Extractor { if (hlsMode) { if (!tracksEnded) { output.endTracks(); + remainingPmts = 0; + tracksEnded = true; } } else { - tsPayloadReaders.remove(TS_PAT_PID); tsPayloadReaders.remove(pid); - output.endTracks(); + remainingPmts--; + if (remainingPmts == 0) { + output.endTracks(); + tracksEnded = true; + } } - tracksEnded = true; } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index 5785c50a7b..4169e0f3a0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -81,17 +81,63 @@ public interface TsPayloadReader { */ final class TrackIdGenerator { - private final int firstId; - private final int idIncrement; - private int generatedIdCount; + private static final int ID_UNSET = Integer.MIN_VALUE; - public TrackIdGenerator(int firstId, int idIncrement) { - this.firstId = firstId; - this.idIncrement = idIncrement; + private final String formatIdPrefix; + private final int firstTrackId; + private final int trackIdIncrement; + private int trackId; + private String formatId; + + public TrackIdGenerator(int firstTrackId, int trackIdIncrement) { + this(ID_UNSET, firstTrackId, trackIdIncrement); } - public int getNextId() { - return firstId + idIncrement * generatedIdCount++; + public TrackIdGenerator(int programNumber, int firstTrackId, int trackIdIncrement) { + this.formatIdPrefix = programNumber != ID_UNSET ? programNumber + "/" : ""; + this.firstTrackId = firstTrackId; + this.trackIdIncrement = trackIdIncrement; + trackId = ID_UNSET; + } + + /** + * Generates a new set of track and track format ids. Must be called before {@code get*} + * methods. + */ + public void generateNewId() { + trackId = trackId == ID_UNSET ? firstTrackId : trackId + trackIdIncrement; + formatId = formatIdPrefix + trackId; + } + + /** + * Returns the last generated track id. Must be called after the first {@link #generateNewId()} + * call. + * + * @return The last generated track id. + */ + public int getTrackId() { + maybeThrowUninitializedError(); + return trackId; + } + + /** + * Returns the last generated format id, with the format {@code "programNumber/trackId"}. If no + * {@code programNumber} was provided, the {@code trackId} alone is used as format id. Must be + * called after the first {@link #generateNewId()} call. + * + * @return The last generated format id, with the format {@code "programNumber/trackId"}. If no + * {@code programNumber} was provided, the {@code trackId} alone is used as + * format id. + */ + public String getFormatId() { + maybeThrowUninitializedError(); + return formatId; + } + + private void maybeThrowUninitializedError() { + if (trackId == ID_UNSET) { + throw new IllegalStateException("generateNewId() must be called before retrieving ids."); + } } } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java b/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java index 19c500202b..ace300c6b1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/TimestampAdjuster.java @@ -34,7 +34,7 @@ public final class TimestampAdjuster { */ private static final long MAX_PTS_PLUS_ONE = 0x200000000L; - private final long firstSampleTimestampUs; + public final long firstSampleTimestampUs; private long timestampOffsetUs; From 74acbe04e35a029b570b2476fbee4135febf110e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 1 Feb 2017 01:25:34 -0800 Subject: [PATCH 063/106] Pass an array of BufferProcessors to the AudioTrack. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146215966 --- .../ext/ffmpeg/FfmpegAudioRenderer.java | 19 +--- .../ext/flac/LibflacAudioRenderer.java | 19 +--- .../ext/opus/LibopusAudioRenderer.java | 33 +++---- .../android/exoplayer2/SimpleExoPlayer.java | 36 ++++--- .../android/exoplayer2/audio/AudioTrack.java | 88 +++++++++++------ .../exoplayer2/audio/BufferProcessor.java | 53 +++++++++-- .../audio/MediaCodecAudioRenderer.java | 16 +++- .../audio/ResamplingBufferProcessor.java | 95 +++++++++++-------- .../audio/SimpleDecoderAudioRenderer.java | 20 ++-- .../mediacodec/MediaCodecRenderer.java | 6 +- 10 files changed, 238 insertions(+), 147 deletions(-) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 0aac601045..6c3ece68a2 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -19,8 +19,8 @@ import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -43,21 +43,12 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - */ - public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); - } - - /** - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, bufferProcessors); } @Override diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index eb7206c9cf..5efaf98512 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.flac; import android.os.Handler; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -38,21 +38,12 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - */ - public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); - } - - /** - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, bufferProcessors); } @Override diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 1850e68229..f31f80f518 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.ext.opus; import android.os.Handler; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -40,35 +40,26 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ - public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) { - super(eventHandler, eventListener); + public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, bufferProcessors); } /** * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. */ public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities) { - super(eventHandler, eventListener, audioCapabilities); - } - - /** - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. - */ - public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - AudioCapabilities audioCapabilities, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { - super(eventHandler, eventListener, audioCapabilities, drmSessionManager, - playClearSamplesWithoutKeys); + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + BufferProcessor... bufferProcessors) { + super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys, + bufferProcessors); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 298e528246..4547ec7e08 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -29,6 +29,7 @@ import android.view.SurfaceView; import android.view.TextureView; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.BufferProcessor; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -624,7 +625,7 @@ public class SimpleExoPlayer implements ExoPlayer { buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, componentListener, allowedVideoJoiningTimeMs, out); buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, - componentListener, out); + componentListener, buildBufferProcessors(), out); buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out); @@ -636,7 +637,7 @@ public class SimpleExoPlayer implements ExoPlayer { * @param context The {@link Context} associated with the player. * @param mainHandler A handler associated with the main thread's looper. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. + * not be used for DRM protected playbacks. * @param extensionRendererMode The extension renderer mode. * @param eventListener An event listener. * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers @@ -681,17 +682,19 @@ public class SimpleExoPlayer implements ExoPlayer { * @param context The {@link Context} associated with the player. * @param mainHandler A handler associated with the main thread's looper. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will - * not be used for DRM protected playbacks. + * not be used for DRM protected playbacks. * @param extensionRendererMode The extension renderer mode. * @param eventListener An event listener. + * @param bufferProcessors An array of {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. May be empty. * @param out An array to which the built renderers should be appended. */ protected void buildAudioRenderers(Context context, Handler mainHandler, DrmSessionManager drmSessionManager, @ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener, - ArrayList out) { + BufferProcessor[] bufferProcessors, ArrayList out) { out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, - mainHandler, eventListener, AudioCapabilities.getCapabilities(context))); + mainHandler, eventListener, AudioCapabilities.getCapabilities(context), bufferProcessors)); if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { return; @@ -705,8 +708,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, BufferProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + bufferProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibopusAudioRenderer."); } catch (ClassNotFoundException e) { @@ -719,8 +723,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, BufferProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + bufferProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded LibflacAudioRenderer."); } catch (ClassNotFoundException e) { @@ -733,8 +738,9 @@ public class SimpleExoPlayer implements ExoPlayer { Class clazz = Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer"); Constructor constructor = clazz.getConstructor(Handler.class, - AudioRendererEventListener.class); - Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); + AudioRendererEventListener.class, BufferProcessor[].class); + Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener, + bufferProcessors); out.add(extensionRendererIndex++, renderer); Log.i(TAG, "Loaded FfmpegAudioRenderer."); } catch (ClassNotFoundException e) { @@ -787,6 +793,14 @@ public class SimpleExoPlayer implements ExoPlayer { // Do nothing. } + /** + * Builds an array of {@link BufferProcessor}s which will process PCM audio buffers before they + * are output. + */ + protected BufferProcessor[] buildBufferProcessors() { + return new BufferProcessor[0]; + } + // Internal methods. private void removeSurfaceCallbacks() { diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 11c388fdab..c0ba7ad3e4 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 @@ -90,6 +90,21 @@ public final class AudioTrack { } + /** + * Thrown when a failure occurs configuring the track. + */ + public static final class ConfigurationException extends Exception { + + public ConfigurationException(Throwable cause) { + super(cause); + } + + public ConfigurationException(String message) { + super(message); + } + + } + /** * Thrown when a failure occurs initializing an {@link android.media.AudioTrack}. */ @@ -254,6 +269,7 @@ public final class AudioTrack { public static boolean failOnSpuriousAudioTimestamp = false; private final AudioCapabilities audioCapabilities; + private final BufferProcessor[] bufferProcessors; private final Listener listener; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; @@ -267,12 +283,12 @@ public final class AudioTrack { private android.media.AudioTrack audioTrack; private int sampleRate; private int channelConfig; - @C.StreamType - private int streamType; @C.Encoding - private int inputEncoding; + private int encoding; @C.Encoding private int outputEncoding; + @C.StreamType + private int streamType; private boolean passthrough; private int pcmFrameSize; private int bufferSize; @@ -303,8 +319,6 @@ public final class AudioTrack { private byte[] preV21OutputBuffer; private int preV21OutputBufferOffset; - private BufferProcessor resampler; - private boolean playing; private int audioSessionId; private boolean tunneling; @@ -312,11 +326,18 @@ public final class AudioTrack { private long lastFeedElapsedRealtimeMs; /** - * @param audioCapabilities The current audio capabilities. + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors An array of {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. May be empty. * @param listener Listener for audio track events. */ - public AudioTrack(AudioCapabilities audioCapabilities, Listener listener) { + public AudioTrack(AudioCapabilities audioCapabilities, BufferProcessor[] bufferProcessors, + Listener listener) { this.audioCapabilities = audioCapabilities; + this.bufferProcessors = new BufferProcessor[bufferProcessors.length + 1]; + this.bufferProcessors[0] = new ResamplingBufferProcessor(); + System.arraycopy(bufferProcessors, 0, this.bufferProcessors, 1, bufferProcessors.length); this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { @@ -413,9 +434,23 @@ public final class AudioTrack { * {@link C#ENCODING_PCM_32BIT}. * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a * suitable buffer size automatically. + * @throws ConfigurationException If an error occurs configuring the track. */ public void configure(String mimeType, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) { + @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException { + boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); + @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding; + if (!passthrough) { + for (BufferProcessor bufferProcessor : bufferProcessors) { + try { + bufferProcessor.configure(sampleRate, channelCount, encoding); + } catch (BufferProcessor.UnhandledFormatException e) { + throw new ConfigurationException(e); + } + encoding = bufferProcessor.getOutputEncoding(); + } + } + int channelConfig; switch (channelCount) { case 1: @@ -443,7 +478,7 @@ public final class AudioTrack { channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; break; default: - throw new IllegalArgumentException("Unsupported channel count: " + channelCount); + throw new ConfigurationException("Unsupported channel count: " + channelCount); } // Workaround for overly strict channel configuration checks on nVidia Shield. @@ -461,25 +496,13 @@ public final class AudioTrack { } } - boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); - // Workaround for Nexus Player not reporting support for mono passthrough. // (See [Internal: b/34268671].) if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) { channelConfig = AudioFormat.CHANNEL_OUT_STEREO; } - @C.Encoding int inputEncoding; - if (passthrough) { - inputEncoding = getEncodingForMimeType(mimeType); - } else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT - || pcmEncoding == C.ENCODING_PCM_24BIT || pcmEncoding == C.ENCODING_PCM_32BIT) { - inputEncoding = pcmEncoding; - } else { - throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding); - } - - if (isInitialized() && this.inputEncoding == inputEncoding && this.sampleRate == sampleRate + if (isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate && this.channelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; @@ -487,15 +510,12 @@ public final class AudioTrack { reset(); - this.inputEncoding = inputEncoding; + this.encoding = encoding; this.passthrough = passthrough; this.sampleRate = sampleRate; this.channelConfig = channelConfig; pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels. - outputEncoding = passthrough ? inputEncoding : C.ENCODING_PCM_16BIT; - - resampler = outputEncoding != inputEncoding ? new ResamplingBufferProcessor(inputEncoding) - : null; + outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT; if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; @@ -684,8 +704,12 @@ public final class AudioTrack { } inputBuffer = buffer; - outputBuffer = resampler != null ? resampler.handleBuffer(inputBuffer, outputBuffer) - : inputBuffer; + if (!passthrough) { + for (BufferProcessor bufferProcessor : bufferProcessors) { + buffer = bufferProcessor.handleBuffer(buffer); + } + } + outputBuffer = buffer; if (Util.SDK_INT < 21) { int bytesRemaining = outputBuffer.remaining(); if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) { @@ -886,6 +910,9 @@ public final class AudioTrack { framesPerEncodedSample = 0; inputBuffer = null; avSyncHeader = null; + for (BufferProcessor bufferProcessor : bufferProcessors) { + bufferProcessor.flush(); + } bytesUntilNextAvSync = 0; startMediaTimeState = START_NOT_SET; latencyUs = 0; @@ -919,6 +946,9 @@ public final class AudioTrack { public void release() { reset(); releaseKeepSessionIdAudioTrack(); + for (BufferProcessor bufferProcessor : bufferProcessors) { + bufferProcessor.release(); + } audioSessionId = C.AUDIO_SESSION_ID_UNSET; playing = false; } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java index a10e8c05af..4f604f1a5d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/BufferProcessor.java @@ -15,23 +15,60 @@ */ package com.google.android.exoplayer2.audio; +import com.google.android.exoplayer2.C; import java.nio.ByteBuffer; /** - * Interface for processors of buffers, for use with {@link AudioTrack}. + * Interface for processors of audio buffers. */ public interface BufferProcessor { /** - * Processes the data in the specified input buffer in its entirety. Populates {@code output} with - * processed data if is not {@code null} and has sufficient capacity. Otherwise a different buffer - * will be populated and returned. + * Exception thrown when a processor can't be configured for a given input format. + */ + final class UnhandledFormatException extends Exception { + + public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) { + super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding " + + encoding); + } + + } + + /** + * Configures this processor to take input buffers with the specified format. + * + * @param sampleRateHz The sample rate of input audio in Hz. + * @param channelCount The number of interleaved channels in input audio. + * @param encoding The encoding of input audio. + * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. + */ + void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException; + + /** + * Returns the encoding used in buffers output by this processor. + */ + @C.Encoding + int getOutputEncoding(); + + /** + * Processes the data in the specified input buffer in its entirety. * * @param input A buffer containing the input data to process. - * @param output A buffer into which the output should be written, if its capacity is sufficient. - * @return The processed output. Different to {@code output} if null was passed, or if its - * capacity was insufficient. + * @return A buffer containing the processed output. This may be the same as the input buffer if + * no processing was required. */ - ByteBuffer handleBuffer(ByteBuffer input, ByteBuffer output); + ByteBuffer handleBuffer(ByteBuffer input); + + /** + * Clears any state in preparation for receiving a new stream of buffers. + */ + void flush(); + + /** + * Releases any resources associated with this instance. + */ + void release(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index f8501c3858..dc7cdf42c8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -121,13 +121,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { + AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, + BufferProcessor... bufferProcessors) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener()); + audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener()); eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -219,14 +222,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + throws ExoPlaybackException { boolean passthrough = passthroughMediaFormat != null; String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME) : MimeTypes.AUDIO_RAW; MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); - audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + try { + audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + } catch (AudioTrack.ConfigurationException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java index f0ea5e60c7..4495cfdbee 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -21,35 +21,47 @@ import com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; /** - * A {@link BufferProcessor} that converts PCM input buffers from a specified input bit depth to - * {@link C#ENCODING_PCM_16BIT} in preparation for writing to an {@link android.media.AudioTrack}. + * A {@link BufferProcessor} that outputs buffers in {@link C#ENCODING_PCM_16BIT}. */ /* package */ final class ResamplingBufferProcessor implements BufferProcessor { @C.PcmEncoding - private final int inputEncoding; + private int encoding; + private ByteBuffer outputBuffer; - /** - * Creates a new buffer processor for resampling input in the specified encoding. - * - * @param inputEncoding The PCM encoding of input buffers. - * @throws IllegalArgumentException Thrown if the input encoding is not PCM or its bit depth is - * not 8, 24 or 32-bits. - */ - public ResamplingBufferProcessor(@C.PcmEncoding int inputEncoding) { - Assertions.checkArgument(inputEncoding == C.ENCODING_PCM_8BIT - || inputEncoding == C.ENCODING_PCM_24BIT || inputEncoding == C.ENCODING_PCM_32BIT); - this.inputEncoding = inputEncoding; + public ResamplingBufferProcessor() { + encoding = C.ENCODING_INVALID; } @Override - public ByteBuffer handleBuffer(ByteBuffer input, ByteBuffer output) { - int offset = input.position(); - int limit = input.limit(); - int size = limit - offset; + public void configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws UnhandledFormatException { + if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT + && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { + throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (encoding == C.ENCODING_PCM_16BIT) { + outputBuffer = null; + } + this.encoding = encoding; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public ByteBuffer handleBuffer(ByteBuffer buffer) { + int position = buffer.position(); + int limit = buffer.limit(); + int size = limit - position; int resampledSize; - switch (inputEncoding) { + switch (encoding) { + case C.ENCODING_PCM_16BIT: + // No processing required. + return buffer; case C.ENCODING_PCM_8BIT: resampledSize = size * 2; break; @@ -59,7 +71,6 @@ import java.nio.ByteBuffer; case C.ENCODING_PCM_32BIT: resampledSize = size / 2; break; - case C.ENCODING_PCM_16BIT: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -67,34 +78,34 @@ import java.nio.ByteBuffer; throw new IllegalStateException(); } - ByteBuffer resampledBuffer = output; - if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) { - resampledBuffer = ByteBuffer.allocateDirect(resampledSize); + if (outputBuffer == null || outputBuffer.capacity() < resampledSize) { + outputBuffer = ByteBuffer.allocateDirect(resampledSize).order(buffer.order()); + } else { + Assertions.checkState(!outputBuffer.hasRemaining()); + outputBuffer.clear(); } - resampledBuffer.position(0); - resampledBuffer.limit(resampledSize); // Samples are little endian. - switch (inputEncoding) { + switch (encoding) { case C.ENCODING_PCM_8BIT: // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. - for (int i = offset; i < limit; i++) { - resampledBuffer.put((byte) 0); - resampledBuffer.put((byte) ((input.get(i) & 0xFF) - 128)); + for (int i = position; i < limit; i++) { + outputBuffer.put((byte) 0); + outputBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128)); } break; case C.ENCODING_PCM_24BIT: // 24->16 bit resampling. Drop the least significant byte. - for (int i = offset; i < limit; i += 3) { - resampledBuffer.put(input.get(i + 1)); - resampledBuffer.put(input.get(i + 2)); + for (int i = position; i < limit; i += 3) { + outputBuffer.put(buffer.get(i + 1)); + outputBuffer.put(buffer.get(i + 2)); } break; case C.ENCODING_PCM_32BIT: // 32->16 bit resampling. Drop the two least significant bytes. - for (int i = offset; i < limit; i += 4) { - resampledBuffer.put(input.get(i + 2)); - resampledBuffer.put(input.get(i + 3)); + for (int i = position; i < limit; i += 4) { + outputBuffer.put(buffer.get(i + 2)); + outputBuffer.put(buffer.get(i + 3)); } break; case C.ENCODING_PCM_16BIT: @@ -105,8 +116,18 @@ import java.nio.ByteBuffer; throw new IllegalStateException(); } - resampledBuffer.position(0); - return resampledBuffer; + outputBuffer.flip(); + return outputBuffer; + } + + @Override + public void flush() { + // Do nothing. + } + + @Override + public void release() { + outputBuffer = null; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index d23ee769dd..9e75145626 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -102,10 +102,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio buffers + * before they are output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener) { - this(eventHandler, eventListener, null); + AudioRendererEventListener eventListener, BufferProcessor... bufferProcessors) { + this(eventHandler, eventListener, null, null, false, bufferProcessors); } /** @@ -133,13 +135,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * begin in parallel with key acquisition. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param bufferProcessors Optional {@link BufferProcessor}s which will process PCM audio + * buffers before they are output. */ public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + BufferProcessor... bufferProcessors) { super(C.TRACK_TYPE_AUDIO); eventDispatcher = new EventDispatcher(eventHandler, eventListener); - audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener()); + audioTrack = new AudioTrack(audioCapabilities, bufferProcessors, new AudioTrackListener()); this.drmSessionManager = drmSessionManager; formatHolder = new FormatHolder(); this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; @@ -193,8 +198,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements while (drainOutputBuffer()) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } catch (AudioTrack.InitializationException | AudioTrack.WriteException - | AudioDecoderException e) { + } catch (AudioDecoderException | AudioTrack.ConfigurationException + | AudioTrack.InitializationException | AudioTrack.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } decoderCounters.ensureUpdated(); @@ -255,7 +260,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException, - AudioTrack.InitializationException, AudioTrack.WriteException { + AudioTrack.ConfigurationException, AudioTrack.InitializationException, + AudioTrack.WriteException { if (outputBuffer == null) { outputBuffer = decoder.dequeueOutputBuffer(); if (outputBuffer == null) { diff --git a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 9be1c59baf..0330b13eb6 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 @@ -779,8 +779,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @param codec The {@link MediaCodec} instance. * @param outputFormat The new output format. + * @throws ExoPlaybackException Thrown if an error occurs handling the new output format. */ - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + throws ExoPlaybackException { // Do nothing. } @@ -918,7 +920,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Processes a new output format. */ - private void processOutputFormat() { + private void processOutputFormat() throws ExoPlaybackException { MediaFormat format = codec.getOutputFormat(); if (codecNeedsAdaptationWorkaround && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT From 025a67cae950f4a8d11b2648b570e2e458bb90e8 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 1 Feb 2017 02:53:32 -0800 Subject: [PATCH 064/106] Enable buffering for CacheDataSink by default ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146221487 --- .../exoplayer2/upstream/cache/CacheDataSink.java | 7 ++++++- .../upstream/cache/CacheDataSinkFactory.java | 11 ++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 71397bd403..33b1ca58b0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -32,6 +32,9 @@ import java.io.OutputStream; */ public final class CacheDataSink implements DataSink { + /** Default buffer size. */ + public static final int DEFAULT_BUFFER_SIZE = 20480; + private final Cache cache; private final long maxCacheFileSize; private final int bufferSize; @@ -56,13 +59,15 @@ public final class CacheDataSink implements DataSink { } /** + * Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}. + * * @param cache The cache into which data should be written. * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into * multiple cache files. */ public CacheDataSink(Cache cache, long maxCacheFileSize) { - this(cache, maxCacheFileSize, 0); + this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE); } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java index 0c8c006e2c..0b9ab66508 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java @@ -24,18 +24,27 @@ public final class CacheDataSinkFactory implements DataSink.Factory { private final Cache cache; private final long maxCacheFileSize; + private final int bufferSize; /** * @see CacheDataSink#CacheDataSink(Cache, long) */ public CacheDataSinkFactory(Cache cache, long maxCacheFileSize) { + this(cache, maxCacheFileSize, CacheDataSink.DEFAULT_BUFFER_SIZE); + } + + /** + * @see CacheDataSink#CacheDataSink(Cache, long, int) + */ + public CacheDataSinkFactory(Cache cache, long maxCacheFileSize, int bufferSize) { this.cache = cache; this.maxCacheFileSize = maxCacheFileSize; + this.bufferSize = bufferSize; } @Override public DataSink createDataSink() { - return new CacheDataSink(cache, maxCacheFileSize); + return new CacheDataSink(cache, maxCacheFileSize, bufferSize); } } From ee3c5f875fe4b09346084b9648f9896cbb7d5329 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 1 Feb 2017 08:00:48 -0800 Subject: [PATCH 065/106] Simplify chunk package ahead of EMSG/608 piping Issue: #2362 Issue: #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146243681 --- .../drm/OfflineLicenseHelperTest.java | 7 +- .../exoplayer2/drm/OfflineLicenseHelper.java | 16 +++-- .../extractor/DefaultTrackOutput.java | 22 ++++--- .../source/chunk/ChunkExtractorWrapper.java | 59 ++++++++--------- .../source/chunk/ContainerMediaChunk.java | 22 ++----- .../source/chunk/InitializationChunk.java | 65 +------------------ .../source/chunk/SingleSampleMediaChunk.java | 3 +- .../source/dash/DefaultDashChunkSource.java | 29 +++------ .../smoothstreaming/DefaultSsChunkSource.java | 4 +- 9 files changed, 75 insertions(+), 152 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 9eed8dfd3a..985e93404a 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.MimeTypes; import java.util.Arrays; import java.util.HashMap; import org.mockito.Mock; @@ -217,7 +218,11 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { } private static Representation newRepresentations(DrmInitData drmInitData) { - Format format = Format.createVideoSampleFormat("", "", "", 0, 0, 0, 0, 0, null, drmInitData); + Format format = Format.createVideoContainerFormat("id", MimeTypes.VIDEO_MP4, + MimeTypes.VIDEO_H264, "", Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, 0); + if (drmInitData != null) { + format = format.copyWithDrmInitData(drmInitData); + } return Representation.newInstance("", 0, format, "", new SingleSegmentBase()); } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index f4a65931b6..0f979d6a4f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -210,11 +210,13 @@ public final class OfflineLicenseHelper { Representation representation = adaptationSet.representations.get(0); DrmInitData drmInitData = representation.format.drmInitData; if (drmInitData == null) { - InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation); + ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format); + InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation, + extractorWrapper); if (initializationChunk == null) { return null; } - Format sampleFormat = initializationChunk.getSampleFormat(); + Format sampleFormat = extractorWrapper.getSampleFormat(); if (sampleFormat != null) { drmInitData = sampleFormat.drmInitData; } @@ -288,8 +290,9 @@ public final class OfflineLicenseHelper { return session; } - private static InitializationChunk loadInitializationChunk(final DataSource dataSource, - final Representation representation) throws IOException, InterruptedException { + private static InitializationChunk loadInitializationChunk(DataSource dataSource, + Representation representation, ChunkExtractorWrapper extractorWrapper) + throws IOException, InterruptedException { RangedUri rangedUri = representation.getInitializationUri(); if (rangedUri == null) { return null; @@ -298,7 +301,7 @@ public final class OfflineLicenseHelper { rangedUri.length, representation.getCacheKey()); InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec, representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */, - newWrappedExtractor(representation.format)); + extractorWrapper); initializationChunk.load(); return initializationChunk; } @@ -308,8 +311,7 @@ public final class OfflineLicenseHelper { final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); - return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */, - false /* resendFormatOnInit */); + return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java index b3bcd97048..460e8d33a8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java @@ -70,6 +70,8 @@ public final class DefaultTrackOutput implements TrackOutput { private Format downstreamFormat; // Accessed only by the loading thread (or the consuming thread when there is no loading thread). + private boolean pendingFormatAdjustment; + private Format lastUnadjustedFormat; private long sampleOffsetUs; private long totalBytesWritten; private Allocation lastAllocation; @@ -445,23 +447,24 @@ public final class DefaultTrackOutput implements TrackOutput { } /** - * Like {@link #format(Format)}, but with an offset that will be added to the timestamps of - * samples subsequently queued to the buffer. The offset is also used to adjust - * {@link Format#subsampleOffsetUs} for both the {@link Format} passed and those subsequently - * passed to {@link #format(Format)}. + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples + * subsequently queued to the buffer. * - * @param format The format. * @param sampleOffsetUs The timestamp offset in microseconds. */ - public void formatWithOffset(Format format, long sampleOffsetUs) { - this.sampleOffsetUs = sampleOffsetUs; - format(format); + public void setSampleOffsetUs(long sampleOffsetUs) { + if (this.sampleOffsetUs != sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + pendingFormatAdjustment = true; + } } @Override public void format(Format format) { Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); boolean formatChanged = infoQueue.format(adjustedFormat); + lastUnadjustedFormat = format; + pendingFormatAdjustment = false; if (upstreamFormatChangeListener != null && formatChanged) { upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); } @@ -518,6 +521,9 @@ public final class DefaultTrackOutput implements TrackOutput { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, byte[] encryptionKey) { + if (pendingFormatAdjustment) { + format(lastUnadjustedFormat); + } if (!startWriteOperation()) { infoQueue.commitSampleTimestamp(timeUs); return; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index 2623d31cef..51db1e2a17 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,33 +30,19 @@ import java.io.IOException; /** * An {@link Extractor} wrapper for loading chunks containing a single track. *

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

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

- * Should be called after loading has completed. - */ - public SeekMap getSeekMap() { - return seekMap; - } - - // SeekMapOutput implementation. - - @Override - public void seekMap(SeekMap seekMap) { - this.seekMap = seekMap; - } - - // TrackOutput implementation. - - @Override - public void format(Format format) { - this.sampleFormat = format; - } - - @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) - throws IOException, InterruptedException { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - - @Override - public void sampleData(ParsableByteArray data, int length) { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - - @Override - public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, - byte[] encryptionKey) { - throw new IllegalStateException("Unexpected sample data in initialization chunk"); - } - // Loadable implementation. @Override @@ -138,8 +78,7 @@ public final class InitializationChunk extends Chunk implements SeekMapOutput, ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); if (bytesLoaded == 0) { - // Set the target to ourselves. - extractorWrapper.init(this, this); + extractorWrapper.init(null); } // Load and decode the initialization data. try { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index d7be74535e..1afce6f2ee 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -88,7 +88,8 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { } ExtractorInput extractorInput = new DefaultExtractorInput(dataSource, bytesLoaded, length); DefaultTrackOutput trackOutput = getTrackOutput(); - trackOutput.formatWithOffset(sampleFormat, 0); + trackOutput.setSampleOffsetUs(0); + trackOutput.format(sampleFormat); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 74d53d3e32..d264283b68 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -176,8 +176,7 @@ public class DefaultDashChunkSource implements DashChunkSource { RangedUri pendingInitializationUri = null; RangedUri pendingIndexUri = null; - Format sampleFormat = representationHolder.sampleFormat; - if (sampleFormat == null) { + if (representationHolder.extractorWrapper.getSampleFormat() == null) { pendingInitializationUri = selectedRepresentation.getInitializationUri(); } if (segmentIndex == null) { @@ -233,8 +232,8 @@ public class DefaultDashChunkSource implements DashChunkSource { int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(), - trackSelection.getSelectionReason(), trackSelection.getSelectionData(), sampleFormat, - segmentNum, maxSegmentCount); + trackSelection.getSelectionReason(), trackSelection.getSelectionData(), segmentNum, + maxSegmentCount); } @Override @@ -243,15 +242,11 @@ public class DefaultDashChunkSource implements DashChunkSource { InitializationChunk initializationChunk = (InitializationChunk) chunk; RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(initializationChunk.trackFormat)]; - Format sampleFormat = initializationChunk.getSampleFormat(); - if (sampleFormat != null) { - representationHolder.setSampleFormat(sampleFormat); - } // The null check avoids overwriting an index obtained from the manifest with one obtained // from the stream. If the manifest defines an index then the stream shouldn't, but in cases // where it does we should ignore it. if (representationHolder.segmentIndex == null) { - SeekMap seekMap = initializationChunk.getSeekMap(); + SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap(); if (seekMap != null) { representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap); } @@ -318,7 +313,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private static Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, Format sampleFormat, int firstSegmentNum, int maxSegmentCount) { + Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) { Representation representation = representationHolder.representation; long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum); @@ -347,7 +342,7 @@ public class DefaultDashChunkSource implements DashChunkSource { long sampleOffsetUs = -representation.presentationTimeOffsetUs; return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, segmentCount, - sampleOffsetUs, representationHolder.extractorWrapper, sampleFormat); + sampleOffsetUs, representationHolder.extractorWrapper); } } @@ -359,7 +354,6 @@ public class DefaultDashChunkSource implements DashChunkSource { public Representation representation; public DashSegmentIndex segmentIndex; - public Format sampleFormat; private long periodDurationUs; private int segmentNumShift; @@ -371,11 +365,9 @@ public class DefaultDashChunkSource implements DashChunkSource { if (mimeTypeIsRawText(containerMimeType)) { extractorWrapper = null; } else { - boolean resendFormatOnInit = false; Extractor extractor; if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { extractor = new RawCcExtractor(representation.format); - resendFormatOnInit = true; } else if (mimeTypeIsWebm(containerMimeType)) { extractor = new MatroskaExtractor(); } else { @@ -383,17 +375,12 @@ public class DefaultDashChunkSource implements DashChunkSource { } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - extractorWrapper = new ChunkExtractorWrapper(extractor, - representation.format, true /* preferManifestDrmInitData */, - resendFormatOnInit); + extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format, + true /* preferManifestDrmInitData */); } segmentIndex = representation.getIndex(); } - public void setSampleFormat(Format sampleFormat) { - this.sampleFormat = sampleFormat; - } - public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) throws BehindLiveWindowException{ DashSegmentIndex oldIndex = representation.getIndex(); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index aa197806e2..2116d852ec 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -102,7 +102,7 @@ public class DefaultSsChunkSource implements SsChunkSource { FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track, null); - extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, false, false); + extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, false); } } @@ -219,7 +219,7 @@ public class DefaultSsChunkSource implements SsChunkSource { long sampleOffsetUs = chunkStartTimeUs; return new ContainerMediaChunk(dataSource, dataSpec, format, trackSelectionReason, trackSelectionData, chunkStartTimeUs, chunkEndTimeUs, chunkIndex, 1, sampleOffsetUs, - extractorWrapper, format); + extractorWrapper); } } From 0402191ace91a783010370249ca017b634518bc0 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 1 Feb 2017 08:01:34 -0800 Subject: [PATCH 066/106] Make SeiReader injectable to H26xReaders This CL is a no-op refactor but allows defining the outputted channels through the TsPayloadReaderFactory. Issue:#2161 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146243736 --- .../extractor/ts/DefaultTsPayloadReaderFactory.java | 7 ++++--- .../android/exoplayer2/extractor/ts/H264Reader.java | 12 ++++++------ .../android/exoplayer2/extractor/ts/H265Reader.java | 13 ++++++++----- .../android/exoplayer2/extractor/ts/SeiReader.java | 13 ++++++++----- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 31aa88d11a..c798494e42 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -74,10 +74,11 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_H262: return new PesReader(new H262Reader()); case TsExtractor.TS_STREAM_TYPE_H264: - return isSet(FLAG_IGNORE_H264_STREAM) ? null : new PesReader( - new H264Reader(isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS))); + return isSet(FLAG_IGNORE_H264_STREAM) ? null + : new PesReader(new H264Reader(new SeiReader(), isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), + isSet(FLAG_DETECT_ACCESS_UNITS))); case TsExtractor.TS_STREAM_TYPE_H265: - return new PesReader(new H265Reader()); + return new PesReader(new H265Reader(new SeiReader())); case TsExtractor.TS_STREAM_TYPE_SPLICE_INFO: return isSet(FLAG_IGNORE_SPLICE_INFO_STREAM) ? null : new SectionReader(new SpliceInfoSectionReader()); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index 0de6bdeaf9..5b9c2cdcea 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -39,6 +39,7 @@ import java.util.List; private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set + private final SeiReader seiReader; private final boolean allowNonIdrKeyframes; private final boolean detectAccessUnits; private final NalUnitTargetBuffer sps; @@ -49,7 +50,6 @@ import java.util.List; private String formatId; private TrackOutput output; - private SeiReader seiReader; private SampleReader sampleReader; // State that should not be reset on seek. @@ -62,15 +62,17 @@ import java.util.List; private final ParsableByteArray seiWrapper; /** + * @param seiReader An SEI reader for consuming closed caption channels. * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as * synchronization samples (key-frames). * @param detectAccessUnits Whether to split the input stream into access units (samples) based on * slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs). */ - public H264Reader(boolean allowNonIdrKeyframes, boolean detectAccessUnits) { - prefixFlags = new boolean[3]; + public H264Reader(SeiReader seiReader, boolean allowNonIdrKeyframes, boolean detectAccessUnits) { + this.seiReader = seiReader; this.allowNonIdrKeyframes = allowNonIdrKeyframes; this.detectAccessUnits = detectAccessUnits; + prefixFlags = new boolean[3]; sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); @@ -93,9 +95,7 @@ import java.util.List; formatId = idGenerator.getFormatId(); output = extractorOutput.track(idGenerator.getTrackId()); sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); - idGenerator.generateNewId(); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getTrackId()), - idGenerator.getFormatId()); + seiReader.createTracks(extractorOutput, idGenerator); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 0f8a7745a5..93cfe9f5cb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -44,10 +44,11 @@ import java.util.Collections; private static final int PREFIX_SEI_NUT = 39; private static final int SUFFIX_SEI_NUT = 40; + private final SeiReader seiReader; + private String formatId; private TrackOutput output; private SampleReader sampleReader; - private SeiReader seiReader; // State that should not be reset on seek. private boolean hasOutputFormat; @@ -67,7 +68,11 @@ import java.util.Collections; // Scratch variables to avoid allocations. private final ParsableByteArray seiWrapper; - public H265Reader() { + /** + * @param seiReader An SEI reader for consuming closed caption channels. + */ + public H265Reader(SeiReader seiReader) { + this.seiReader = seiReader; prefixFlags = new boolean[3]; vps = new NalUnitTargetBuffer(VPS_NUT, 128); sps = new NalUnitTargetBuffer(SPS_NUT, 128); @@ -95,9 +100,7 @@ import java.util.Collections; formatId = idGenerator.getFormatId(); output = extractorOutput.track(idGenerator.getTrackId()); sampleReader = new SampleReader(output); - idGenerator.generateNewId(); - seiReader = new SeiReader(extractorOutput.track(idGenerator.getTrackId()), - idGenerator.getFormatId()); + seiReader.createTracks(extractorOutput, idGenerator); } @Override 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 471c585277..ced28c3b93 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 @@ -16,7 +16,9 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.text.cea.CeaUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -26,12 +28,13 @@ import com.google.android.exoplayer2.util.ParsableByteArray; */ /* package */ final class SeiReader { - private final TrackOutput output; + private TrackOutput output; - public SeiReader(TrackOutput output, String formatId) { - this.output = output; - output.format(Format.createTextSampleFormat(formatId, MimeTypes.APPLICATION_CEA608, null, - Format.NO_VALUE, 0, null, null)); + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId()); + output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), + MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); } public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { From 537a3ab5be3fcf285b17505ac64c28b5f68c8fbb Mon Sep 17 00:00:00 2001 From: cdrolle Date: Thu, 2 Feb 2017 09:40:28 -0800 Subject: [PATCH 067/106] Fixed 2 issues with Cea708Decoder. The first issue occurs when we attempt to process a DtvCcPacket that hasn't been completely filled. In this case we attempted to extract data beyond the length of the packet, instead of dropping the packet as we should have. The other issue occurs when we encountered an invalid cc_data_pkt. In that case we were finalizing the entire DtvCcPacket, instead of just ignoring that particular cc_data_pkt as we should have. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146373074 --- .../google/android/exoplayer2/text/cea/Cea708Decoder.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index fe97dc62a5..e04c246ea0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -211,7 +211,7 @@ public final class Cea708Decoder extends CeaDecoder { } if (!ccValid) { - finalizeCurrentPacket(); + // This byte-pair isn't valid, ignore it and continue. continue; } @@ -259,7 +259,8 @@ public final class Cea708Decoder extends CeaDecoder { if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) { Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1) + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number " - + currentDtvCcPacket.sequenceNumber + ")"); + + currentDtvCcPacket.sequenceNumber + "); ignoring packet"); + return; } serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex); From c82319332fc6acb03a041fa2faf2b1b9486ea1b7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 2 Feb 2017 11:53:28 -0800 Subject: [PATCH 068/106] Omit clipped samples when applying edits for audio tracks. Issue: #2408 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146389955 --- .../android/exoplayer2/extractor/mp4/AtomParsers.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 5288a3e6ba..87a4a62550 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -332,6 +332,9 @@ import java.util.List; return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } + // Omit any sample at the end point of an edit for audio tracks. + boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO; + // Count the number of samples after applying edits. int editedSampleCount = 0; int nextSampleIndex = 0; @@ -342,7 +345,8 @@ import java.util.List; long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale, track.movieTimescale); int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, true, false); + int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample, + false); editedSampleCount += endIndex - startIndex; copyMetadata |= nextSampleIndex != startIndex; nextSampleIndex = endIndex; @@ -365,7 +369,7 @@ import java.util.List; long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale, track.movieTimescale); int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); - int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, true, false); + int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false); if (copyMetadata) { int count = endIndex - startIndex; System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count); From f2d3af7deaf914957cf30f36b2e01821b609cac9 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Feb 2017 05:50:22 -0800 Subject: [PATCH 069/106] Delete dead code and fix javadocs from hls ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146466389 --- .../source/hls/HlsSampleStreamWrapper.java | 16 +++--------- .../hls/playlist/HlsMasterPlaylist.java | 13 ++-------- .../hls/playlist/HlsPlaylistParser.java | 26 +++++++++---------- 3 files changed, 17 insertions(+), 38 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index a9bbddb69c..bee38c59b5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -112,10 +112,9 @@ import java.util.LinkedList; * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. - * @param muxedAudioFormat If HLS master playlist indicates that the stream contains muxed audio, - * this is the audio {@link Format} as defined by the playlist. - * @param muxedCaptionFormat If HLS master playlist indicates that the stream contains muxed - * captions, this is the audio {@link Format} as defined by the playlist. + * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. + * @param muxedCaptionFormat Optional muxed closed caption {@link Format} as defined by the master + * playlist. * @param minLoadableRetryCount The minimum number of times that the source should retry a load * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. @@ -266,15 +265,6 @@ import java.util.LinkedList; released = true; } - public long getLargestQueuedTimestampUs() { - long largestQueuedTimestampUs = Long.MIN_VALUE; - for (int i = 0; i < sampleQueues.size(); i++) { - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, - sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); - } - return largestQueuedTimestampUs; - } - public void setIsTimestampMaster(boolean isTimestampMaster) { chunkSource.setIsTimestampMaster(isTimestampMaster); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index b7426fd03d..ab18fda2f0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -31,27 +31,18 @@ public final class HlsMasterPlaylist extends HlsPlaylist { */ public static final class HlsUrl { - public final String name; public final String url; public final Format format; - public final Format videoFormat; - public final Format audioFormat; - public final Format[] textFormats; public static HlsUrl createMediaPlaylistHlsUrl(String baseUri) { Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null, Format.NO_VALUE, 0, null); - return new HlsUrl(null, baseUri, format, null, null, null); + return new HlsUrl(baseUri, format); } - public HlsUrl(String name, String url, Format format, Format videoFormat, Format audioFormat, - Format[] textFormats) { - this.name = name; + public HlsUrl(String url, Format format) { this.url = url; this.format = format; - this.videoFormat = videoFormat; - this.audioFormat = audioFormat; - this.textFormats = textFormats; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index a211417501..6efd1fecb2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -179,30 +179,28 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Fri, 3 Feb 2017 06:45:17 -0800 Subject: [PATCH 070/106] Fix FLAC extension native part compilation issues Issue: #2352 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146469547 --- extensions/flac/src/main/jni/Android.mk | 2 +- extensions/flac/src/main/jni/flac_parser.cc | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/flac/src/main/jni/Android.mk b/extensions/flac/src/main/jni/Android.mk index e009333633..ff54c1b3c0 100644 --- a/extensions/flac/src/main/jni/Android.mk +++ b/extensions/flac/src/main/jni/Android.mk @@ -31,7 +31,7 @@ LOCAL_C_INCLUDES := \ LOCAL_SRC_FILES := $(FLAC_SOURCES) LOCAL_CFLAGS += '-DVERSION="1.3.1"' -DFLAC__NO_MD5 -DFLAC__INTEGER_ONLY_LIBRARY -DFLAC__NO_ASM -LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC +LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC -DHAVE_SYS_PARAM_H LOCAL_CFLAGS += -O3 -funroll-loops -finline-functions LOCAL_LDLIBS := -llog -lz -lm diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 7d22c7fe79..e4925cb462 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -453,7 +453,8 @@ int64_t FLACParser::getSeekPosition(int64_t timeUs) { } FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points; - for (unsigned i = mSeekTable->num_points - 1; i >= 0; i--) { + for (unsigned i = mSeekTable->num_points; i > 0; ) { + i--; if (points[i].sample_number <= sample) { return firstFrameOffset + points[i].stream_offset; } From d3f4da749c8313b78d0526cacdf4ad593a774506 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 3 Feb 2017 07:06:58 -0800 Subject: [PATCH 071/106] Propagate track type through ExtractorOutput.track This allows binding by track type in ChunkExtractorWrapper, which allows the EMSG and 608 tracks to be enabled on FragmentedMp4Extractor in DefaultDashChunkSource. ChunkExtractorWrapper currently binds these to DummyTrackOutputs. Note: I wanted to pass the mimeType instead, since it's a more specific, but unfortunately there's at least one place where it's not known at the point of invoking track() (FlvExtractor). Issue #2176 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146471082 --- .../exoplayer2/ext/flac/FlacExtractor.java | 2 +- .../extractor/ts/AdtsReaderTest.java | 4 +-- .../extractor/ts/TsExtractorTest.java | 3 ++- .../exoplayer2/drm/OfflineLicenseHelper.java | 8 +++--- .../exoplayer2/extractor/ExtractorOutput.java | 11 ++++---- .../extractor/flv/FlvExtractor.java | 7 +++-- .../extractor/mkv/MatroskaExtractor.java | 7 ++++- .../extractor/mp3/Mp3Extractor.java | 2 +- .../extractor/mp4/FragmentedMp4Extractor.java | 27 ++++++++++++------- .../extractor/mp4/Mp4Extractor.java | 3 ++- .../extractor/ogg/OggExtractor.java | 3 ++- .../extractor/rawcc/RawCcExtractor.java | 2 +- .../exoplayer2/extractor/ts/Ac3Reader.java | 2 +- .../exoplayer2/extractor/ts/AdtsReader.java | 4 +-- .../exoplayer2/extractor/ts/DtsReader.java | 2 +- .../exoplayer2/extractor/ts/H262Reader.java | 2 +- .../exoplayer2/extractor/ts/H264Reader.java | 2 +- .../exoplayer2/extractor/ts/H265Reader.java | 2 +- .../exoplayer2/extractor/ts/Id3Reader.java | 2 +- .../extractor/ts/MpegAudioReader.java | 2 +- .../exoplayer2/extractor/ts/SeiReader.java | 3 ++- .../extractor/ts/SpliceInfoSectionReader.java | 2 +- .../extractor/wav/WavExtractor.java | 2 +- .../source/ExtractorMediaPeriod.java | 2 +- .../source/chunk/ChunkExtractorWrapper.java | 12 +++++++-- .../source/dash/DefaultDashChunkSource.java | 23 ++++++++++------ .../source/hls/HlsSampleStreamWrapper.java | 4 +-- .../source/hls/WebvttExtractor.java | 2 +- .../smoothstreaming/DefaultSsChunkSource.java | 5 ++-- .../testutil/FakeExtractorOutput.java | 6 ++--- 30 files changed, 98 insertions(+), 60 deletions(-) diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 42c5908619..d13194793e 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -67,7 +67,7 @@ public final class FlacExtractor implements Extractor { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = extractorOutput.track(0); + trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); extractorOutput.endTracks(); try { decoderJni = new FlacDecoderJni(); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java index ebb547810b..bcfa90a565 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java @@ -69,8 +69,8 @@ public class AdtsReaderTest extends TestCase { @Override protected void setUp() throws Exception { FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); - adtsOutput = fakeExtractorOutput.track(0); - id3Output = fakeExtractorOutput.track(1); + adtsOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_AUDIO); + id3Output = fakeExtractorOutput.track(1, C.TRACK_TYPE_METADATA); adtsReader = new AdtsReader(true); TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1); adtsReader.createTracks(fakeExtractorOutput, idGenerator); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 74e0748119..9bcb1c2377 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.test.InstrumentationTestCase; import android.util.SparseArray; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -179,7 +180,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_UNKNOWN); output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), "mime", null, 0, 0, language, null, 0)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 0f979d6a4f..8d057230ca 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -210,7 +210,8 @@ public final class OfflineLicenseHelper { Representation representation = adaptationSet.representations.get(0); DrmInitData drmInitData = representation.format.drmInitData; if (drmInitData == null) { - ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format); + ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format, + adaptationSet.type); InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation, extractorWrapper); if (initializationChunk == null) { @@ -306,12 +307,13 @@ public final class OfflineLicenseHelper { return initializationChunk; } - private static ChunkExtractorWrapper newWrappedExtractor(final Format format) { + private static ChunkExtractorWrapper newWrappedExtractor(Format format, int trackType) { final String mimeType = format.containerMimeType; final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); - return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */); + return new ChunkExtractorWrapper(extractor, format, trackType, + false /* preferManifestDrmInitData */); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java index a547f745ca..a59cb1d1f2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java @@ -23,17 +23,18 @@ public interface ExtractorOutput { /** * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track. *

- * The same {@link TrackOutput} is returned if multiple calls are made with the same - * {@code trackId}. + * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. * - * @param trackId A track identifier. + * @param id A track identifier. + * @param type The type of the track. Typically one of the {@link com.google.android.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. * @return The {@link TrackOutput} for the given track identifier. */ - TrackOutput track(int trackId); + TrackOutput track(int id, int type); /** * Called when all tracks have been identified, meaning no new {@code trackId} values will be - * passed to {@link #track(int)}. + * passed to {@link #track(int, int)}. */ void endTracks(); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 5b396749ac..218e6ffd82 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.flv; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -183,10 +184,12 @@ public final class FlvExtractor implements Extractor, SeekMap { boolean hasAudio = (flags & 0x04) != 0; boolean hasVideo = (flags & 0x01) != 0; if (hasAudio && audioReader == null) { - audioReader = new AudioTagPayloadReader(extractorOutput.track(TAG_TYPE_AUDIO)); + audioReader = new AudioTagPayloadReader( + extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO)); } if (hasVideo && videoReader == null) { - videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO)); + videoReader = new VideoTagPayloadReader( + extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO)); } if (metadataReader == null) { metadataReader = new ScriptTagPayloadReader(null); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 970335e9d2..ed1a86e651 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 @@ -1462,6 +1462,7 @@ public final class MatroskaExtractor implements Extractor { throw new ParserException("Unrecognized codec identifier."); } + int type; Format format; @C.SelectionFlags int selectionFlags = 0; selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0; @@ -1469,10 +1470,12 @@ public final class MatroskaExtractor implements Extractor { // TODO: Consider reading the name elements of the tracks and, if present, incorporating them // into the trackId passed when creating the formats. if (MimeTypes.isAudio(mimeType)) { + type = C.TRACK_TYPE_AUDIO; format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding, initializationData, drmInitData, selectionFlags, language); } else if (MimeTypes.isVideo(mimeType)) { + type = C.TRACK_TYPE_VIDEO; if (displayUnit == Track.DISPLAY_UNIT_PIXELS) { displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth; displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight; @@ -1485,17 +1488,19 @@ public final class MatroskaExtractor implements Extractor { Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData, Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData); } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, selectionFlags, language, drmInitData); } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) || MimeTypes.APPLICATION_PGS.equals(mimeType)) { + type = C.TRACK_TYPE_TEXT; format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, initializationData, language, drmInitData); } else { throw new ParserException("Unexpected MIME type."); } - this.output = output.track(number); + this.output = output.track(number, type); this.output.format(format); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 9bdefeceaf..ff84c7da25 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -118,7 +118,7 @@ public final class Mp3Extractor implements Extractor { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = extractorOutput.track(0); + trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); extractorOutput.endTracks(); } 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 f7cc42c48f..d72eb62509 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 @@ -164,7 +164,14 @@ public final class FragmentedMp4Extractor implements Extractor { private boolean haveOutputSeekMap; public FragmentedMp4Extractor() { - this(0, null); + this(0); + } + + /** + * @param flags Flags that control the extractor's behavior. + */ + public FragmentedMp4Extractor(@Flags int flags) { + this(flags, null); } /** @@ -172,20 +179,20 @@ public final class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) { - this(flags, null, timestampAdjuster); + this(flags, timestampAdjuster, null); } /** * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor * will not receive a moov box in the input data. - * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ - public FragmentedMp4Extractor(@Flags int flags, Track sideloadedTrack, - TimestampAdjuster timestampAdjuster) { - this.sideloadedTrack = sideloadedTrack; + public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, + Track sideloadedTrack) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; + this.sideloadedTrack = sideloadedTrack; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); @@ -209,7 +216,7 @@ public final class FragmentedMp4Extractor implements Extractor { public void init(ExtractorOutput output) { extractorOutput = output; if (sideloadedTrack != null) { - TrackBundle bundle = new TrackBundle(output.track(0)); + TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type)); bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); trackBundles.put(0, bundle); maybeInitExtraTracks(); @@ -420,7 +427,7 @@ 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); - TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i)); + TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); trackBundle.init(track, defaultSampleValuesArray.get(track.id)); trackBundles.put(track.id, trackBundle); durationUs = Math.max(durationUs, track.durationUs); @@ -449,12 +456,12 @@ public final class FragmentedMp4Extractor implements Extractor { private void maybeInitExtraTracks() { if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) { - eventMessageTrackOutput = extractorOutput.track(trackBundles.size()); + eventMessageTrackOutput = extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE)); } if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutput == null) { - cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1); + cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, C.TRACK_TYPE_TEXT); cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 3759a80fd4..0c990f5747 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -344,7 +344,8 @@ public final class Mp4Extractor implements Extractor, SeekMap { continue; } - Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i)); + Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, + extractorOutput.track(i, track.type)); // Each sample has up to three bytes of overhead for the start code that replaces its length. // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java index 5f41126737..cc3c5de311 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -75,7 +76,7 @@ public class OggExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - TrackOutput trackOutput = output.track(0); + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); output.endTracks(); // TODO: fix the case if sniff() isn't called streamReader.init(output, trackOutput); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index f9957aebe5..7840eafce6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -65,7 +65,7 @@ public final class RawCcExtractor implements Extractor { @Override public void init(ExtractorOutput output) { output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); - trackOutput = output.track(0); + trackOutput = output.track(0, C.TRACK_TYPE_TEXT); output.endTracks(); trackOutput.format(format); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index afef154ed4..790c036f1d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -87,7 +87,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator generator) { generator.generateNewId(); trackFormatId = generator.getFormatId(); - output = extractorOutput.track(generator.getTrackId()); + output = extractorOutput.track(generator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 56793119e4..58318ea78d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -111,10 +111,10 @@ import java.util.Collections; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); if (exposeId3) { idGenerator.generateNewId(); - id3Output = extractorOutput.track(idGenerator.getTrackId()); + id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); } else { diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 50be258ae5..874de83b68 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -82,7 +82,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index df6ba208c3..ba515d31ed 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -81,7 +81,7 @@ import java.util.Collections; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index 5b9c2cdcea..c1d24b7a33 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -93,7 +93,7 @@ import java.util.List; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); seiReader.createTracks(extractorOutput, idGenerator); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 93cfe9f5cb..30a5bdc1fd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -98,7 +98,7 @@ import java.util.Collections; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_VIDEO); sampleReader = new SampleReader(output); seiReader.createTracks(extractorOutput, idGenerator); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index 27eb2a1bb4..7d2ecc4e74 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -57,7 +57,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index ae7edc51e4..6301716286 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -79,7 +79,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); } @Override 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 ced28c3b93..a3f4deffcb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -32,7 +33,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT); output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE, 0, null, null)); } 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 625bb70560..27838d4c25 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java @@ -37,7 +37,7 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader { TsPayloadReader.TrackIdGenerator idGenerator) { this.timestampAdjuster = timestampAdjuster; idGenerator.generateNewId(); - output = extractorOutput.track(idGenerator.getTrackId()); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_SCTE35, null, Format.NO_VALUE, null)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 3d9f8166ab..cb46aa5519 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -60,7 +60,7 @@ public final class WavExtractor implements Extractor, SeekMap { @Override public void init(ExtractorOutput output) { extractorOutput = output; - trackOutput = output.track(0); + trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); wavHeader = null; output.endTracks(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 5226043593..dc189058a6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -381,7 +381,7 @@ import java.io.IOException; // ExtractorOutput implementation. Called by the loading thread. @Override - public TrackOutput track(int id) { + public TrackOutput track(int id, int type) { DefaultTrackOutput trackOutput = sampleQueues.get(id); if (trackOutput == null) { trackOutput = new DefaultTrackOutput(allocator); 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 51db1e2a17..4984ed0ff0 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.chunk; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -37,6 +38,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput public final Extractor extractor; private final Format manifestFormat; + private final int primaryTrackType; private final boolean preferManifestDrmInitData; private boolean extractorInitialized; @@ -52,13 +54,16 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput * @param extractor The extractor to wrap. * @param manifestFormat A manifest defined {@link Format} whose data should be merged into any * sample {@link Format} output from the {@link Extractor}. + * @param primaryTrackType The type of the primary track. Typically one of the {@link C} + * {@code TRACK_TYPE_*} constants. * @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat} * should be preferred when the sample and manifest {@link Format}s are merged. */ - public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, + public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, int primaryTrackType, boolean preferManifestDrmInitData) { this.extractor = extractor; this.manifestFormat = manifestFormat; + this.primaryTrackType = primaryTrackType; this.preferManifestDrmInitData = preferManifestDrmInitData; } @@ -98,7 +103,10 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput // ExtractorOutput implementation. @Override - public TrackOutput track(int id) { + public TrackOutput track(int id, int type) { + if (primaryTrackType != C.TRACK_TYPE_UNKNOWN && primaryTrackType != type) { + return new DummyTrackOutput(); + } Assertions.checkState(!seenTrack || seenTrackId == id); seenTrack = true; seenTrackId = id; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index d264283b68..88dcdd50be 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; import com.google.android.exoplayer2.source.chunk.InitializationChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; @@ -119,11 +120,13 @@ public class DefaultDashChunkSource implements DashChunkSource { this.maxSegmentsPerLoad = maxSegmentsPerLoad; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - List representations = getRepresentations(); + AdaptationSet adaptationSet = getAdaptationSet(); + List representations = adaptationSet.representations; representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); - representationHolders[i] = new RepresentationHolder(periodDurationUs, representation); + representationHolders[i] = new RepresentationHolder(periodDurationUs, representation, + adaptationSet.type); } } @@ -133,7 +136,7 @@ public class DefaultDashChunkSource implements DashChunkSource { manifest = newManifest; periodIndex = newPeriodIndex; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); - List representations = getRepresentations(); + List representations = getAdaptationSet().representations; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); representationHolders[i].updateRepresentation(periodDurationUs, representation); @@ -278,8 +281,8 @@ public class DefaultDashChunkSource implements DashChunkSource { // Private methods. - private List getRepresentations() { - return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex).representations; + private AdaptationSet getAdaptationSet() { + return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex); } private long getNowUnixTimeUs() { @@ -350,6 +353,7 @@ public class DefaultDashChunkSource implements DashChunkSource { protected static final class RepresentationHolder { + public final int trackType; public final ChunkExtractorWrapper extractorWrapper; public Representation representation; @@ -358,9 +362,11 @@ public class DefaultDashChunkSource implements DashChunkSource { private long periodDurationUs; private int segmentNumShift; - public RepresentationHolder(long periodDurationUs, Representation representation) { + public RepresentationHolder(long periodDurationUs, Representation representation, + int trackType) { this.periodDurationUs = periodDurationUs; this.representation = representation; + this.trackType = trackType; String containerMimeType = representation.format.containerMimeType; if (mimeTypeIsRawText(containerMimeType)) { extractorWrapper = null; @@ -371,12 +377,13 @@ public class DefaultDashChunkSource implements DashChunkSource { } else if (mimeTypeIsWebm(containerMimeType)) { extractor = new MatroskaExtractor(); } else { - extractor = new FragmentedMp4Extractor(); + extractor = new FragmentedMp4Extractor(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK + | FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK); } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format, - true /* preferManifestDrmInitData */); + trackType, true /* preferManifestDrmInitData */); } segmentIndex = representation.getIndex(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index bee38c59b5..538acbeabf 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -156,7 +156,7 @@ import java.util.LinkedList; * prepare. */ public void prepareSingleTrack(Format format) { - track(0).format(format); + track(0, C.TRACK_TYPE_UNKNOWN).format(format); sampleQueuesBuilt = true; maybeFinishPrepare(); } @@ -456,7 +456,7 @@ import java.util.LinkedList; // ExtractorOutput implementation. Called by the loading thread. @Override - public DefaultTrackOutput track(int id) { + public DefaultTrackOutput track(int id, int type) { if (sampleQueues.indexOfKey(id) >= 0) { return sampleQueues.get(id); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java index c8928ce65d..12ea2c16c7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java @@ -167,7 +167,7 @@ import java.util.regex.Pattern; } private TrackOutput buildTrackOutput(long subsampleOffsetUs) { - TrackOutput trackOutput = output.track(0); + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_TEXT); trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, language, null, subsampleOffsetUs)); output.endTracks(); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index 2116d852ec..b0a583e8e5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -101,8 +101,9 @@ public class DefaultSsChunkSource implements SsChunkSource { trackEncryptionBoxes, nalUnitLengthFieldLength, null, null); FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME - | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track, null); - extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, false); + | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track); + extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, streamElement.type, + false); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java index 3716c6d37f..ee8927ea21 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java @@ -47,13 +47,13 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab } @Override - public FakeTrackOutput track(int trackId) { - FakeTrackOutput output = trackOutputs.get(trackId); + public FakeTrackOutput track(int id, int type) { + FakeTrackOutput output = trackOutputs.get(id); if (output == null) { Assert.assertFalse(tracksEnded); numberOfTracks++; output = new FakeTrackOutput(); - trackOutputs.put(trackId, output); + trackOutputs.put(id, output); } return output; } From 7c8a3d006d7192bc69084972396327409c28ee94 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Feb 2017 09:01:31 -0800 Subject: [PATCH 072/106] Flexibilize mp4 extensions detection for HLS chunks This CL adds support mp4 extensions of the form .m4_. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146479870 --- .../google/android/exoplayer2/source/hls/HlsMediaChunk.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 924d3d3ece..a3e3559724 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 @@ -56,6 +56,7 @@ import java.util.concurrent.atomic.AtomicInteger; private static final String EC3_FILE_EXTENSION = ".ec3"; private static final String MP3_FILE_EXTENSION = ".mp3"; private static final String MP4_FILE_EXTENSION = ".mp4"; + private static final String M4_FILE_EXTENSION_PREFIX = ".m4"; private static final String VTT_FILE_EXTENSION = ".vtt"; private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; @@ -341,7 +342,8 @@ import java.util.concurrent.atomic.AtomicInteger; // Only reuse TS and fMP4 extractors. usingNewExtractor = false; extractor = previousExtractor; - } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) { + } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) + || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { extractor = new FragmentedMp4Extractor(0, timestampAdjuster); } else { // MPEG-2 TS segments, but we need a new extractor. From de46ed7fb90f9ec421c77e5888bb8ae4572bff83 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Tue, 7 Feb 2017 07:15:39 -0800 Subject: [PATCH 073/106] Fixed an issue with Cea608Decoder in which tab commands were not being used correctly. Tab commands were being used cumulatively (i.e. moving the cursor farther and farther over) resulting in the text eventually trying to write beyond the bounds of the screen and throwing an exception. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146783284 --- .../google/android/exoplayer2/text/cea/Cea608Decoder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 7324c94288..261f9d0e3e 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 @@ -364,7 +364,7 @@ public final class Cea608Decoder extends CeaDecoder { } else if (isPreambleAddressCode(cc1, cc2)) { handlePreambleAddressCode(cc1, cc2); } else if (isTabCtrlCode(cc1, cc2)) { - currentCueBuilder.tab(cc2 - 0x20); + currentCueBuilder.setTab(cc2 - 0x20); } else if (isMiscCode(cc1, cc2)) { handleMiscCode(cc2); } @@ -646,8 +646,8 @@ public final class Cea608Decoder extends CeaDecoder { this.indent = indent; } - public void tab(int tabs) { - tabOffset += tabs; + public void setTab(int tabs) { + tabOffset = tabs; } public void setPreambleStyle(CharacterStyle style) { From ef41303a04077dd323014ec3295950490a4ba635 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 8 Feb 2017 06:57:39 -0800 Subject: [PATCH 074/106] Keep FlacStreamInfo unobfuscated as it is accessed from native methods Issue: #2427 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=146905629 --- extensions/flac/proguard-rules.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/flac/proguard-rules.txt b/extensions/flac/proguard-rules.txt index 8e7f5e17d5..ee0a9fa5b5 100644 --- a/extensions/flac/proguard-rules.txt +++ b/extensions/flac/proguard-rules.txt @@ -5,7 +5,10 @@ native ; } -# Some members of this class are being accessed from native methods. Keep them unobfuscated. +# Some members of these classes are being accessed from native methods. Keep them unobfuscated. -keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni { *; } +-keep class com.google.android.exoplayer2.util.FlacStreamInfo { + *; +} From 9e07cf7c73ad70ec9d5c2fea56c58ef8871d2142 Mon Sep 17 00:00:00 2001 From: maxjulian Date: Thu, 9 Feb 2017 11:41:20 -0800 Subject: [PATCH 075/106] Update exoplayer 1 and 2 to support stereo mesh layout. Reference spec: https://github.com/google/spatial-media/blob/master/docs/spherical-video-v2-rfc.md#semantics ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147060701 --- .../main/java/com/google/android/exoplayer2/C.java | 13 ++++++++++++- .../java/com/google/android/exoplayer2/Format.java | 2 +- .../exoplayer2/extractor/mkv/MatroskaExtractor.java | 3 +++ .../exoplayer2/extractor/mp4/AtomParsers.java | 3 +++ 4 files changed, 19 insertions(+), 2 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 0b1c33bfc9..7e9fe46c10 100644 --- a/library/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/src/main/java/com/google/android/exoplayer2/C.java @@ -515,7 +515,13 @@ public final class C { * The stereo mode for 360/3D/VR videos. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({Format.NO_VALUE, STEREO_MODE_MONO, STEREO_MODE_TOP_BOTTOM, STEREO_MODE_LEFT_RIGHT}) + @IntDef({ + Format.NO_VALUE, + STEREO_MODE_MONO, + STEREO_MODE_TOP_BOTTOM, + STEREO_MODE_LEFT_RIGHT, + STEREO_MODE_STEREO_MESH + }) public @interface StereoMode {} /** * Indicates Monoscopic stereo layout, used with 360/3D/VR videos. @@ -529,6 +535,11 @@ public final class C { * Indicates Left-Right stereo layout, used with 360/3D/VR videos. */ public static final int STEREO_MODE_LEFT_RIGHT = 2; + /** + * Indicates a stereo layout where the left and right eyes have separate meshes, + * used with 360/3D/VR videos. + */ + public static final int STEREO_MODE_STEREO_MESH = 3; /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index bf113119a6..f001feec10 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -120,7 +120,7 @@ public final class Format implements Parcelable { /** * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo * modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link - * C#STEREO_MODE_LEFT_RIGHT}. + * C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}. */ @C.StereoMode public final int stereoMode; 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 ed1a86e651..51ce819282 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 @@ -673,6 +673,9 @@ public final class MatroskaExtractor implements Extractor { case 3: currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM; break; + case 15: + currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH; + break; default: break; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 87a4a62550..54141f2545 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -720,6 +720,9 @@ import java.util.List; case 2: stereoMode = C.STEREO_MODE_LEFT_RIGHT; break; + case 3: + stereoMode = C.STEREO_MODE_STEREO_MESH; + break; default: break; } From ef475eb9c9c8c1224043a61504f296c39db38778 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Thu, 9 Feb 2017 12:31:16 -0800 Subject: [PATCH 076/106] Fixed potential bug in which old paint-on captions could be drawn overtop by pop-on captions as they weren't being cleared properly. This fix was taken from YouTube's Cea608Decoder ([] ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147066662 --- .../google/android/exoplayer2/text/cea/Cea608Decoder.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 261f9d0e3e..fe9a5fbc5c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -503,11 +503,14 @@ public final class Cea608Decoder extends CeaDecoder { return; } + int oldCaptionMode = this.captionMode; this.captionMode = captionMode; + // Clear the working memory. resetCueBuilders(); - if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) { - // When switching to roll-up or unknown, we also need to clear the caption. + if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP + || captionMode == CC_MODE_UNKNOWN) { + // When switching from paint-on or to roll-up or unknown, we also need to clear the caption. cues = null; } } From f7fbbe993e90ffe60c6a3ec151529bd32bf8bd55 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 10 Feb 2017 09:49:15 -0800 Subject: [PATCH 077/106] Fix ArrayIndexOutOfBoundsException while reading SEI NAL unit ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147165453 --- .../exoplayer2/extractor/mp4/FragmentedMp4Extractor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 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 d72eb62509..6c3b86c19b 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 @@ -1087,12 +1087,12 @@ public final class FragmentedMp4Extractor implements Extractor { 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) { + input.peekFully(nalPayload.data, 0, 1); + if ((nalPayload.data[0] & 0x1F) == NAL_UNIT_TYPE_SEI) { // Read the whole SEI NAL unit into nalWrapper, including the NAL unit type byte. nalPayload.reset(sampleCurrentNalBytesRemaining); + byte[] nalPayloadData = nalPayload.data; input.readFully(nalPayloadData, 0, sampleCurrentNalBytesRemaining); // Write the SEI unit straight to the output. output.sampleData(nalPayload, sampleCurrentNalBytesRemaining); From 5bfad5d99b86a43fe70357b12fbe94aa185906f0 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Sun, 12 Feb 2017 19:22:20 -0800 Subject: [PATCH 078/106] Added sample mime type to the track list descriptions. The addition of sample mime types can make it easier to identify tracks in the case of mixed media (e.g. CEA-608 and CEA-708 caption tracks). This change appends the mime type to the end of the track description for all media types. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147303187 --- .../exoplayer2/demo/TrackSelectionHelper.java | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java index 936cdf90f8..338544b1ed 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java @@ -301,15 +301,18 @@ import java.util.Locale; private static String buildTrackName(Format format) { String trackName; if (MimeTypes.isVideo(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(buildResolutionString(format), - buildBitrateString(format)), buildTrackIdString(format)); + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator( + buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)), + buildSampleMimeTypeString(format)); } else if (MimeTypes.isAudio(format.sampleMimeType)) { - trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), - buildAudioPropertyString(format)), buildBitrateString(format)), - buildTrackIdString(format)); + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator( + buildLanguageString(format), buildAudioPropertyString(format)), + buildBitrateString(format)), buildTrackIdString(format)), + buildSampleMimeTypeString(format)); } else { - trackName = joinWithSeparator(joinWithSeparator(buildLanguageString(format), - buildBitrateString(format)), buildTrackIdString(format)); + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), + buildBitrateString(format)), buildTrackIdString(format)), + buildSampleMimeTypeString(format)); } return trackName.length() == 0 ? "unknown" : trackName; } @@ -342,4 +345,8 @@ import java.util.Locale; return format.id == null ? "" : ("id:" + format.id); } + private static String buildSampleMimeTypeString(Format format) { + return format.sampleMimeType == null ? "" : format.sampleMimeType; + } + } From 3bc320faaf2991913a838b881bf59069543e351e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Feb 2017 02:44:00 -0800 Subject: [PATCH 079/106] Fix misleading method names. Issue: #2414 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147325759 --- .../com/google/android/exoplayer2/ext/opus/OpusDecoder.java | 2 +- .../java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index 6d0deb44ae..83e461d279 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -213,7 +213,7 @@ import java.util.List; SimpleOutputBuffer outputBuffer, int sampleRate); private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate, - ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv, + ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); private native void opusClose(long decoder); private native void opusReset(long decoder); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 0d7547d125..73ec7c2f96 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -141,7 +141,7 @@ import java.nio.ByteBuffer; private native long vpxClose(long context); private native long vpxDecode(long context, ByteBuffer encoded, int length); private native long vpxSecureDecode(long context, ByteBuffer encoded, int length, - ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv, + ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer); private native int vpxGetErrorCode(long context); From 0316ab80dfbab737283c6a38b4f7dfd98d1747e1 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Feb 2017 09:35:24 -0800 Subject: [PATCH 080/106] Fix broken Javadoc Issue: #2433 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147355544 --- .../java/com/google/android/exoplayer2/audio/AudioTrack.java | 2 +- .../google/android/exoplayer2/drm/DefaultDrmSessionManager.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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 c0ba7ad3e4..cc3f91bc0a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -840,7 +840,7 @@ public final class AudioTrack { * audio session id has changed. Enabling tunneling requires platform API version 21 onwards. * * @param tunnelingAudioSessionId The audio session id to use. - * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. + * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. */ public void enableTunnelingV21(int tunnelingAudioSessionId) { Assertions.checkState(Util.SDK_INT >= 21); 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 1cd8d8464d..3af0f8a5c0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -280,6 +280,7 @@ public class DefaultDrmSessionManager implements DrmSe * required. * *

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

    *
  • {@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is * requested otherwise the offline license is restored. *
  • {@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license @@ -288,6 +289,7 @@ public class DefaultDrmSessionManager implements DrmSe * requested otherwise the offline license is renewed. *
  • {@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license * is released. + *
* * @param mode The mode to be set. * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. From 8cb3b6ed07ba95cd2f9297b9c351d02c090e9dda Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Feb 2017 10:03:50 -0800 Subject: [PATCH 081/106] SmoothStreaming: Replace variant bitrate/start_time placeholders Issue: #2447 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147358615 --- .../smoothstreaming/manifest/SsManifest.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index 844ffc45e6..1bb877eb59 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -128,8 +128,10 @@ public class SsManifest { */ public static class StreamElement { - private static final String URL_PLACEHOLDER_START_TIME = "{start time}"; - private static final String URL_PLACEHOLDER_BITRATE = "{bitrate}"; + private static final String URL_PLACEHOLDER_START_TIME_1 = "{start time}"; + private static final String URL_PLACEHOLDER_START_TIME_2 = "{start_time}"; + private static final String URL_PLACEHOLDER_BITRATE_1 = "{bitrate}"; + private static final String URL_PLACEHOLDER_BITRATE_2 = "{Bitrate}"; public final int type; public final String subType; @@ -216,9 +218,13 @@ public class SsManifest { Assertions.checkState(formats != null); Assertions.checkState(chunkStartTimes != null); Assertions.checkState(chunkIndex < chunkStartTimes.size()); + String bitrateString = Integer.toString(formats[track].bitrate); + String startTimeString = chunkStartTimes.get(chunkIndex).toString(); String chunkUrl = chunkTemplate - .replace(URL_PLACEHOLDER_BITRATE, Integer.toString(formats[track].bitrate)) - .replace(URL_PLACEHOLDER_START_TIME, chunkStartTimes.get(chunkIndex).toString()); + .replace(URL_PLACEHOLDER_BITRATE_1, bitrateString) + .replace(URL_PLACEHOLDER_BITRATE_2, bitrateString) + .replace(URL_PLACEHOLDER_START_TIME_1, startTimeString) + .replace(URL_PLACEHOLDER_START_TIME_2, startTimeString); return UriUtil.resolveToUri(baseUri, chunkUrl); } From 7625c1b862db7a0dc860832214a2beba79c22c17 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 13 Feb 2017 13:27:17 -0800 Subject: [PATCH 082/106] Remove unnecessary configuration parameter. - It's always fine to prefer the manifest drm init data. In DASH we always do this anyway. In SS the manifest and sample formats are identical. - Optimized the case where the manifest and sample formats are identical by avoiding the format copy. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147385282 --- .../java/com/google/android/exoplayer2/Format.java | 11 +++++++---- .../android/exoplayer2/drm/OfflineLicenseHelper.java | 3 +-- .../source/chunk/ChunkExtractorWrapper.java | 10 ++-------- .../source/dash/DefaultDashChunkSource.java | 3 +-- .../source/smoothstreaming/DefaultSsChunkSource.java | 3 +-- 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/Format.java b/library/src/main/java/com/google/android/exoplayer2/Format.java index f001feec10..866e512288 100644 --- a/library/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/src/main/java/com/google/android/exoplayer2/Format.java @@ -438,16 +438,19 @@ public final class Format implements Parcelable { drmInitData, metadata); } - public Format copyWithManifestFormatInfo(Format manifestFormat, - boolean preferManifestDrmInitData) { + public Format copyWithManifestFormatInfo(Format manifestFormat) { + if (this == manifestFormat) { + // No need to copy from ourselves. + return this; + } String id = manifestFormat.id; String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs; int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate; float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate; @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; String language = this.language == null ? manifestFormat.language : this.language; - DrmInitData drmInitData = (preferManifestDrmInitData && manifestFormat.drmInitData != null) - || this.drmInitData == null ? manifestFormat.drmInitData : this.drmInitData; + DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData + : this.drmInitData; return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 8d057230ca..b3729c2377 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -312,8 +312,7 @@ public final class OfflineLicenseHelper { final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); - return new ChunkExtractorWrapper(extractor, format, trackType, - false /* preferManifestDrmInitData */); + return new ChunkExtractorWrapper(extractor, format, trackType); } } 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 4984ed0ff0..489f63be2b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source.chunk; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -39,7 +38,6 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput private final Format manifestFormat; private final int primaryTrackType; - private final boolean preferManifestDrmInitData; private boolean extractorInitialized; private TrackOutput trackOutput; @@ -56,15 +54,11 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput * sample {@link Format} output from the {@link Extractor}. * @param primaryTrackType The type of the primary track. Typically one of the {@link C} * {@code TRACK_TYPE_*} constants. - * @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat} - * should be preferred when the sample and manifest {@link Format}s are merged. */ - public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, int primaryTrackType, - boolean preferManifestDrmInitData) { + public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, int primaryTrackType) { this.extractor = extractor; this.manifestFormat = manifestFormat; this.primaryTrackType = primaryTrackType; - this.preferManifestDrmInitData = preferManifestDrmInitData; } /** @@ -127,7 +121,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput @Override public void format(Format format) { - sampleFormat = format.copyWithManifestFormatInfo(manifestFormat, preferManifestDrmInitData); + sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); if (trackOutput != null) { trackOutput.format(sampleFormat); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 88dcdd50be..c553e4eb40 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -382,8 +382,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format, - trackType, true /* preferManifestDrmInitData */); + extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format, trackType); } segmentIndex = representation.getIndex(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index b0a583e8e5..e17d72ab37 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -102,8 +102,7 @@ public class DefaultSsChunkSource implements SsChunkSource { FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track); - extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, streamElement.type, - false); + extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, streamElement.type); } } From 3691454b82506e23d0663e84becd6faa1265c772 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 14 Feb 2017 05:22:09 -0800 Subject: [PATCH 083/106] Fix resuming after error for CEA-608 SEI in fMP4. Also test SEI parsing in FragmentedMp4ExtractorTest. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147460699 --- .../assets/mp4/sample_fragmented_sei.mp4 | Bin 0 -> 106093 bytes .../mp4/sample_fragmented_sei.mp4.0.dump | 382 ++++++++++++++++++ .../mp4/FragmentedMp4ExtractorTest.java | 31 +- .../extractor/mp4/FragmentedMp4Extractor.java | 72 ++-- .../android/exoplayer2/util/NalUnitUtil.java | 17 +- 5 files changed, 453 insertions(+), 49 deletions(-) create mode 100644 library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 create mode 100644 library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump diff --git a/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 b/library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..16907fdd987223e0996d3cfc65ba346cb4123752 GIT binary patch literal 106093 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 @@ -1066,49 +1066,47 @@ public final class FragmentedMp4Extractor implements Extractor { if (track.nalUnitLengthFieldLength != 0) { // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. - byte[] nalLengthData = nalLength.data; - nalLengthData[0] = 0; - nalLengthData[1] = 0; - nalLengthData[2] = 0; - int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength; + byte[] nalPrefixData = nalPrefix.data; + nalPrefixData[0] = 0; + nalPrefixData[1] = 0; + nalPrefixData[2] = 0; + int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1; int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength; // NAL units are length delimited, but the decoder requires start code delimited units. // Loop until we've written the sample to the track output, replacing length delimiters with // start codes as we encounter them. while (sampleBytesWritten < sampleSize) { if (sampleCurrentNalBytesRemaining == 0) { - // Read the NAL length so that we know where we find the next one. - input.readFully(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); - nalLength.setPosition(0); - sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); + // Read the NAL length so that we know where we find the next one, and its type. + input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength); + nalPrefix.setPosition(0); + sampleCurrentNalBytesRemaining = nalPrefix.readUnsignedIntToInt() - 1; // Write a start code for the current NAL unit. nalStartCode.setPosition(0); output.sampleData(nalStartCode, 4); - sampleBytesWritten += 4; + // Write the NAL unit type byte. + output.sampleData(nalPrefix, 1); + processSeiNalUnitPayload = cea608TrackOutput != null + && NalUnitUtil.isNalUnitSei(nalPrefixData[4]); + sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; - if (cea608TrackOutput != null) { - // Peek the NAL unit type byte. - input.peekFully(nalPayload.data, 0, 1); - if ((nalPayload.data[0] & 0x1F) == NAL_UNIT_TYPE_SEI) { - // Read the whole SEI NAL unit into nalWrapper, including the NAL unit type byte. - nalPayload.reset(sampleCurrentNalBytesRemaining); - byte[] nalPayloadData = nalPayload.data; - 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); + int writtenBytes; + if (processSeiNalUnitPayload) { + // Read and write the payload of the SEI NAL unit. + nalBuffer.reset(sampleCurrentNalBytesRemaining); + input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining); + output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining); + writtenBytes = sampleCurrentNalBytesRemaining; + // Unescape and process the SEI NAL unit. + int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit()); + nalBuffer.reset(unescapedLength); + CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer, + cea608TrackOutput); + } else { + // Write the payload of the NAL unit. + writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); + } sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java index a452871afc..a2643d5177 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java @@ -103,7 +103,8 @@ public final class NalUnitUtil { 2f }; - private static final int NAL_UNIT_TYPE_SPS = 7; + private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information + private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set private static final Object scratchEscapePositionsLock = new Object(); @@ -197,6 +198,17 @@ public final class NalUnitUtil { data.clear(); } + /** + * Returns whether the NAL unit with the specified header contains supplemental enhancement + * information. + * + * @param nalUnitHeader The header of the NAL unit (first byte of nal_unit()). + * @return Whether the NAL unit with the specified header is an SEI NAL unit. + */ + public static boolean isNalUnitSei(byte nalUnitHeader) { + return (nalUnitHeader & 0x1F) == NAL_UNIT_TYPE_SEI; + } + /** * Returns the type of the NAL unit in {@code data} that starts at {@code offset}. * @@ -297,7 +309,8 @@ public final class NalUnitUtil { int frameCropRightOffset = data.readUnsignedExpGolombCodedInt(); int frameCropTopOffset = data.readUnsignedExpGolombCodedInt(); int frameCropBottomOffset = data.readUnsignedExpGolombCodedInt(); - int cropUnitX, cropUnitY; + int cropUnitX; + int cropUnitY; if (chromaFormatIdc == 0) { cropUnitX = 1; cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0); From fd6012a72722a42c78cf0742c3986feaf76a8829 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 14 Feb 2017 17:04:48 -0800 Subject: [PATCH 084/106] Remove outputBuffer assertion in ResamplingBufferProcessor. The outputBuffer is not necessarily empty after a flush, so the assertion could fail in normal usage. The assertion can just be removed as the output buffer is rewritten in full on every call to handleBuffer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147541016 --- .../android/exoplayer2/audio/ResamplingBufferProcessor.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java index 4495cfdbee..507cdbcdd1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java +++ b/library/src/main/java/com/google/android/exoplayer2/audio/ResamplingBufferProcessor.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.audio; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; /** @@ -81,7 +80,6 @@ import java.nio.ByteBuffer; if (outputBuffer == null || outputBuffer.capacity() < resampledSize) { outputBuffer = ByteBuffer.allocateDirect(resampledSize).order(buffer.order()); } else { - Assertions.checkState(!outputBuffer.hasRemaining()); outputBuffer.clear(); } From 5c571e6e9d24086abede757f5be5d275cd67a515 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 15 Feb 2017 09:58:44 -0800 Subject: [PATCH 085/106] Handle H.265/HEVC SEI NAL units in FragmentedMp4Extractor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147609330 --- .../extractor/mp4/FragmentedMp4Extractor.java | 6 ++++-- .../android/exoplayer2/util/NalUnitUtil.java | 17 +++++++++++------ 2 files changed, 15 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 bc9b0fcad6..8144880338 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 @@ -1087,7 +1087,7 @@ public final class FragmentedMp4Extractor implements Extractor { // Write the NAL unit type byte. output.sampleData(nalPrefix, 1); processSeiNalUnitPayload = cea608TrackOutput != null - && NalUnitUtil.isNalUnitSei(nalPrefixData[4]); + && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; } else { @@ -1100,7 +1100,9 @@ public final class FragmentedMp4Extractor implements Extractor { writtenBytes = sampleCurrentNalBytesRemaining; // Unescape and process the SEI NAL unit. int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit()); - nalBuffer.reset(unescapedLength); + // If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte. + nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0); + nalBuffer.setLimit(unescapedLength); CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer, cea608TrackOutput); } else { diff --git a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java index a2643d5177..ab2fec0db7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java @@ -103,8 +103,9 @@ public final class NalUnitUtil { 2f }; - private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information - private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set + private static final int H264_NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information + private static final int H264_NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set + private static final int H265_NAL_UNIT_TYPE_PREFIX_SEI = 39; private static final Object scratchEscapePositionsLock = new Object(); @@ -177,7 +178,7 @@ public final class NalUnitUtil { while (offset + 1 < length) { int value = data.get(offset) & 0xFF; if (consecutiveZeros == 3) { - if (value == 1 && (data.get(offset + 1) & 0x1F) == NAL_UNIT_TYPE_SPS) { + if (value == 1 && (data.get(offset + 1) & 0x1F) == H264_NAL_UNIT_TYPE_SPS) { // Copy from this NAL unit onwards to the start of the buffer. ByteBuffer offsetData = data.duplicate(); offsetData.position(offset - 3); @@ -202,11 +203,15 @@ public final class NalUnitUtil { * Returns whether the NAL unit with the specified header contains supplemental enhancement * information. * - * @param nalUnitHeader The header of the NAL unit (first byte of nal_unit()). + * @param mimeType The sample MIME type. + * @param nalUnitHeaderFirstByte The first byte of nal_unit(). * @return Whether the NAL unit with the specified header is an SEI NAL unit. */ - public static boolean isNalUnitSei(byte nalUnitHeader) { - return (nalUnitHeader & 0x1F) == NAL_UNIT_TYPE_SEI; + public static boolean isNalUnitSei(String mimeType, byte nalUnitHeaderFirstByte) { + return (MimeTypes.VIDEO_H264.equals(mimeType) + && (nalUnitHeaderFirstByte & 0x1F) == H264_NAL_UNIT_TYPE_SEI) + || (MimeTypes.VIDEO_H265.equals(mimeType) + && ((nalUnitHeaderFirstByte & 0x7E) >> 1) == H265_NAL_UNIT_TYPE_PREFIX_SEI); } /** From d6e15b79538b39c6ddf976bf18a951394816819c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Feb 2017 10:27:26 -0800 Subject: [PATCH 086/106] DASH: Correctly handle empty segment indices Issue: #1865 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147613244 --- .../source/dash/DashMediaSource.java | 30 ++++++++----- .../source/dash/DashSegmentIndex.java | 16 +++---- .../source/dash/DashWrappingSegmentIndex.java | 4 +- .../source/dash/DefaultDashChunkSource.java | 44 ++++++++++++------- .../source/dash/manifest/Representation.java | 4 +- .../source/dash/manifest/SegmentBase.java | 40 ++++++++++------- .../dash/manifest/SingleSegmentIndex.java | 4 +- 7 files changed, 82 insertions(+), 60 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 99845c057e..eec99521f1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -572,22 +572,28 @@ public final class DashMediaSource implements MediaSource { long availableStartTimeUs = 0; long availableEndTimeUs = Long.MAX_VALUE; boolean isIndexExplicit = false; + boolean seenEmptyIndex = false; for (int i = 0; i < adaptationSetCount; i++) { DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex(); if (index == null) { return new PeriodSeekInfo(true, 0, durationUs); } - int firstSegmentNum = index.getFirstSegmentNum(); - int lastSegmentNum = index.getLastSegmentNum(durationUs); isIndexExplicit |= index.isExplicit(); - long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); - availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); - if (lastSegmentNum != DashSegmentIndex.INDEX_UNBOUNDED) { - long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) - + index.getDurationUs(lastSegmentNum, durationUs); - availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); - } else { - // The available end time is unmodified, because this index is unbounded. + int segmentCount = index.getSegmentCount(durationUs); + if (segmentCount == 0) { + seenEmptyIndex = true; + availableStartTimeUs = 0; + availableEndTimeUs = 0; + } else if (!seenEmptyIndex) { + int firstSegmentNum = index.getFirstSegmentNum(); + long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum); + availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); + if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED) { + int lastSegmentNum = firstSegmentNum + segmentCount - 1; + long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum) + + index.getDurationUs(lastSegmentNum, durationUs); + availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); + } } } return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs); @@ -704,8 +710,8 @@ public final class DashMediaSource implements MediaSource { // not correspond to the start of a segment in both, but this is an edge case. DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex) .representations.get(0).getIndex(); - if (snapIndex == null) { - // Video adaptation set does not include an index for snapping. + if (snapIndex == null || snapIndex.getSegmentCount(periodDurationUs) == 0) { + // Video adaptation set does not include a non-empty index for snapping. return windowDefaultStartPositionUs; } int segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java index d002831c4f..2ddc7f4f80 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java @@ -26,12 +26,10 @@ public interface DashSegmentIndex { int INDEX_UNBOUNDED = -1; /** - * Returns the segment number of the segment containing a given media time. - *

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

* An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a * SegmentTimeline element, and if the period duration is not yet known. In this case the caller @@ -82,9 +80,9 @@ public interface DashSegmentIndex { * * @param periodDurationUs The duration of the enclosing period in microseconds, or * {@link C#TIME_UNSET} if the period's duration is not yet known. - * @return The segment number of the last segment, or {@link #INDEX_UNBOUNDED}. + * @return The number of segments in the index, or {@link #INDEX_UNBOUNDED}. */ - int getLastSegmentNum(long periodDurationUs); + int getSegmentCount(long periodDurationUs); /** * Returns true if segments are defined explicitly by the index. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java index 56ea626120..40f3448f6a 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java @@ -39,8 +39,8 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri; } @Override - public int getLastSegmentNum(long periodDurationUs) { - return chunkIndex.length - 1; + public int getSegmentCount(long periodDurationUs) { + return chunkIndex.length; } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index c553e4eb40..4548bc75f8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -194,10 +194,16 @@ public class DefaultDashChunkSource implements DashChunkSource { } long nowUs = getNowUnixTimeUs(); + int availableSegmentCount = representationHolder.getSegmentCount(); + if (availableSegmentCount == 0) { + // The index doesn't define any segments. + out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1); + return; + } + int firstAvailableSegmentNum = representationHolder.getFirstSegmentNum(); - int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; - if (indexUnbounded) { + int lastAvailableSegmentNum; + if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { // The index is itself unbounded. We need to use the current time to calculate the range of // available segments. long liveEdgeTimeUs = nowUs - manifest.availabilityStartTime * 1000; @@ -211,6 +217,8 @@ public class DefaultDashChunkSource implements DashChunkSource { // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the // index of the last completed segment. lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs) - 1; + } else { + lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1; } int segmentNum; @@ -268,10 +276,13 @@ public class DefaultDashChunkSource implements DashChunkSource { && ((InvalidResponseCodeException) e).responseCode == 404) { RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(chunk.trackFormat)]; - int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { - missingLastSegment = true; - return true; + int segmentCount = representationHolder.getSegmentCount(); + if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) { + int lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1; + if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { + missingLastSegment = true; + return true; + } } } // Blacklist if appropriate. @@ -405,15 +416,20 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(periodDurationUs); + int oldIndexSegmentCount = oldIndex.getSegmentCount(periodDurationUs); + if (oldIndexSegmentCount == 0) { + // Segment numbers cannot shift if the old index was empty. + return; + } + + int oldIndexLastSegmentNum = oldIndex.getFirstSegmentNum() + oldIndexSegmentCount - 1; long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs); int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); if (oldIndexEndTimeUs == newIndexStartTimeUs) { // The new index continues where the old one ended, with no overlap. - segmentNumShift += oldIndex.getLastSegmentNum(periodDurationUs) + 1 - - newIndexFirstSegmentNum; + segmentNumShift += oldIndexLastSegmentNum + 1 - newIndexFirstSegmentNum; } else if (oldIndexEndTimeUs < newIndexStartTimeUs) { // There's a gap between the old index and the new one which means we've slipped behind the // live window and can't proceed. @@ -429,12 +445,8 @@ public class DefaultDashChunkSource implements DashChunkSource { return segmentIndex.getFirstSegmentNum() + segmentNumShift; } - public int getLastSegmentNum() { - int lastSegmentNum = segmentIndex.getLastSegmentNum(periodDurationUs); - if (lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) { - return DashSegmentIndex.INDEX_UNBOUNDED; - } - return lastSegmentNum + segmentNumShift; + public int getSegmentCount() { + return segmentIndex.getSegmentCount(periodDurationUs); } public long getSegmentStartTimeUs(int segmentNum) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 4146037e1c..5960d4d7ba 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -318,8 +318,8 @@ public abstract class Representation { } @Override - public int getLastSegmentNum(long periodDurationUs) { - return segmentBase.getLastSegmentNum(periodDurationUs); + public int getSegmentCount(long periodDurationUs) { + return segmentBase.getSegmentCount(periodDurationUs); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index 70a65e932a..4f7dc81fc5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -130,18 +130,22 @@ public abstract class SegmentBase { */ public int getSegmentNum(long timeUs, long periodDurationUs) { final int firstSegmentNum = getFirstSegmentNum(); - int lowIndex = firstSegmentNum; - int highIndex = getLastSegmentNum(periodDurationUs); + final int segmentCount = getSegmentCount(periodDurationUs); + if (segmentCount == 0) { + return firstSegmentNum; + } if (segmentTimeline == null) { // All segments are of equal duration (with the possible exception of the last one). long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; int segmentNum = startNumber + (int) (timeUs / durationUs); // Ensure we stay within bounds. - return segmentNum < lowIndex ? lowIndex - : highIndex != DashSegmentIndex.INDEX_UNBOUNDED && segmentNum > highIndex ? highIndex - : segmentNum; + return segmentNum < firstSegmentNum ? firstSegmentNum + : segmentCount == DashSegmentIndex.INDEX_UNBOUNDED ? segmentNum + : Math.min(segmentNum, firstSegmentNum + segmentCount - 1); } else { - // The high index cannot be unbounded. Identify the segment using binary search. + // The index cannot be unbounded. Identify the segment using binary search. + int lowIndex = firstSegmentNum; + int highIndex = firstSegmentNum + segmentCount - 1; while (lowIndex <= highIndex) { int midIndex = lowIndex + (highIndex - lowIndex) / 2; long midTimeUs = getSegmentTimeUs(midIndex); @@ -165,7 +169,9 @@ public abstract class SegmentBase { long duration = segmentTimeline.get(sequenceNumber - startNumber).duration; return (duration * C.MICROS_PER_SECOND) / timescale; } else { - return sequenceNumber == getLastSegmentNum(periodDurationUs) + int segmentCount = getSegmentCount(periodDurationUs); + return segmentCount != DashSegmentIndex.INDEX_UNBOUNDED + && sequenceNumber == (getFirstSegmentNum() + segmentCount - 1) ? (periodDurationUs - getSegmentTimeUs(sequenceNumber)) : ((duration * C.MICROS_PER_SECOND) / timescale); } @@ -201,9 +207,9 @@ public abstract class SegmentBase { } /** - * @see DashSegmentIndex#getLastSegmentNum(long) + * @see DashSegmentIndex#getSegmentCount(long) */ - public abstract int getLastSegmentNum(long periodDurationUs); + public abstract int getSegmentCount(long periodDurationUs); /** * @see DashSegmentIndex#isExplicit() @@ -250,8 +256,8 @@ public abstract class SegmentBase { } @Override - public int getLastSegmentNum(long periodDurationUs) { - return startNumber + mediaSegments.size() - 1; + public int getSegmentCount(long periodDurationUs) { + return mediaSegments.size(); } @Override @@ -322,14 +328,14 @@ public abstract class SegmentBase { } @Override - public int getLastSegmentNum(long periodDurationUs) { + public int getSegmentCount(long periodDurationUs) { if (segmentTimeline != null) { - return segmentTimeline.size() + startNumber - 1; - } else if (periodDurationUs == C.TIME_UNSET) { - return DashSegmentIndex.INDEX_UNBOUNDED; - } else { + return segmentTimeline.size(); + } else if (periodDurationUs != C.TIME_UNSET) { long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; - return startNumber + (int) Util.ceilDivide(periodDurationUs, durationUs) - 1; + return (int) Util.ceilDivide(periodDurationUs, durationUs); + } else { + return DashSegmentIndex.INDEX_UNBOUNDED; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java index 083046d073..4ce49c5ffe 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java @@ -57,8 +57,8 @@ import com.google.android.exoplayer2.source.dash.DashSegmentIndex; } @Override - public int getLastSegmentNum(long periodDurationUs) { - return 0; + public int getSegmentCount(long periodDurationUs) { + return 1; } @Override From ec98bd9ea1d230c8b641c1d0dcff405b5677dfbd Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 15 Feb 2017 10:30:40 -0800 Subject: [PATCH 087/106] Work around broken AAC decoder EoS handling on L. SoftAAC2 would cause an exception to be thrown from dequeueOutputBuffer/releaseOutputBuffer after queueing an end-of-stream buffer for certain streams. The bug was introduced in L and fixed in L MR1, so the workaround is targeted to API 21. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147613659 --- .../mediacodec/MediaCodecRenderer.java | 63 +++++++++++++++++-- 1 file changed, 58 insertions(+), 5 deletions(-) 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 0330b13eb6..9baf974b37 100644 --- a/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -183,6 +183,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean codecNeedsAdaptationWorkaround; private boolean codecNeedsEosPropagationWorkaround; private boolean codecNeedsEosFlushWorkaround; + private boolean codecNeedsEosOutputExceptionWorkaround; private boolean codecNeedsMonoChannelCountWorkaround; private boolean codecNeedsAdaptationWorkaroundBuffer; private boolean shouldSkipAdaptationWorkaroundOutputBuffer; @@ -342,6 +343,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); + codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format); try { long codecInitializingTimestamp = SystemClock.elapsedRealtime(); @@ -513,7 +515,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; if (codecNeedsFlushWorkaround || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { - // Workaround framework bugs. See [Internal: b/8347958, b/8578467, b/8543366, b/23361053]. releaseCodec(); maybeInitCodec(); } else if (codecReinitializationState != REINITIALIZATION_STATE_NONE) { @@ -867,7 +868,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputIndex < 0) { - outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); + if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { + try { + outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, + getDequeueOutputBufferTimeoutUs()); + } catch (IllegalStateException e) { + processEndOfStream(); + if (outputStreamEnded) { + // Release the codec, as it's in an error state. + releaseCodec(); + } + return false; + } + } else { + outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, + getDequeueOutputBufferTimeoutUs()); + } if (outputIndex >= 0) { // We've dequeued a buffer. if (shouldSkipAdaptationWorkaroundOutputBuffer) { @@ -906,9 +922,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } - if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex], - outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer)) { + boolean processedOutputBuffer; + if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) { + try { + processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec, + outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer); + } catch (IllegalStateException e) { + processEndOfStream(); + if (outputStreamEnded) { + // Release the codec, as it's in an error state. + releaseCodec(); + } + return false; + } + } else { + processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec, + outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags, + outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer); + } + + if (processedOutputBuffer) { onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs); outputIndex = C.INDEX_UNSET; return true; @@ -1010,6 +1044,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

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

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

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

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

+ * See [Internal: b/17933838]. + * + * @param name The name of the decoder. + * @return True if the decoder may throw an exception after receiving an end-of-stream buffer. + */ + private static boolean codecNeedsEosOutputExceptionWorkaround(String name) { + return Util.SDK_INT == 21 && "OMX.google.aac.decoder".equals(name); + } + /** * Returns whether the decoder is known to set the number of audio channels in the output format * to 2 for the given input format, whilst only actually outputting a single channel. From 65d4b1cf5c75bf6416acb4836564745f5f57048e Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Feb 2017 10:33:16 -0800 Subject: [PATCH 088/106] Make CeaUtil robust against malformed SEI data I've also added a TODO to not even bother trying to parse CEA from SEI NAL units if they're fully or partially encrypted, which it's possible to determine in the FMP4 extractor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147613979 --- .../extractor/mp4/FragmentedMp4Extractor.java | 1 + .../android/exoplayer2/text/cea/CeaUtil.java | 45 +++++++++++++------ 2 files changed, 32 insertions(+), 14 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 8144880338..0beb644ff6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -1086,6 +1086,7 @@ public final class FragmentedMp4Extractor implements Extractor { output.sampleData(nalStartCode, 4); // Write the NAL unit type byte. output.sampleData(nalPrefix, 1); + // TODO: Don't try and process the SEI NAL unit if the payload is encrypted. processSeiNalUnitPayload = cea608TrackOutput != null && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); sampleBytesWritten += 5; 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 index 3053debfcf..a39c8c8669 100644 --- 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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.cea; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -24,6 +25,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; */ public final class CeaUtil { + private static final String TAG = "CeaUtil"; + private static final int PAYLOAD_TYPE_CC = 4; private static final int COUNTRY_CODE = 0xB5; private static final int PROVIDER_CODE = 0x31; @@ -40,22 +43,15 @@ public final class CeaUtil { */ 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); + int payloadType = readNon255TerminatedValue(seiBuffer); + int payloadSize = readNon255TerminatedValue(seiBuffer); // Process the payload. - if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) { + if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) { + // This might occur if we're trying to read an encrypted SEI NAL unit. + Log.w(TAG, "Skipping remainder of malformed SEI NAL unit."); + seiBuffer.setPosition(seiBuffer.limit()); + } else if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) { // Ignore country_code (1) + provider_code (2) + user_identifier (4) // + user_data_type_code (1). seiBuffer.skipBytes(8); @@ -76,6 +72,27 @@ public final class CeaUtil { } } + /** + * Reads a value from the provided buffer consisting of zero or more 0xFF bytes followed by a + * terminating byte not equal to 0xFF. The returned value is ((0xFF * N) + T), where N is the + * number of 0xFF bytes and T is the value of the terminating byte. + * + * @param buffer The buffer from which to read the value. + * @returns The read value, or -1 if the end of the buffer is reached before a value is read. + */ + private static int readNon255TerminatedValue(ParsableByteArray buffer) { + int b; + int value = 0; + do { + if (buffer.bytesLeft() == 0) { + return -1; + } + b = buffer.readUnsignedByte(); + value += b; + } while (b == 0xFF); + return value; + } + /** * Inspects an sei message to determine whether it contains CEA-608. *

From e74b729952a9b3018ee63f7dc4a8320ab8578de4 Mon Sep 17 00:00:00 2001 From: Sungmin Kim Date: Thu, 16 Feb 2017 21:23:28 +0900 Subject: [PATCH 089/106] fixed a typing error --- .../android/exoplayer2/trackselection/BaseTrackSelection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index c81ffb441f..054ee7973f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -148,7 +148,7 @@ public abstract class BaseTrackSelection implements TrackSelection { } /** - * Returns whether the track at the specified index in the selection is blaclisted. + * Returns whether the track at the specified index in the selection is blacklisted. * * @param index The index of the track in the selection. * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}. From f8bb329ef2a668498262111157e90ebc91a82519 Mon Sep 17 00:00:00 2001 From: Johannes Schamburger Date: Thu, 16 Feb 2017 17:05:12 +0100 Subject: [PATCH 090/106] Make DrmSessionException constructor public to enable creating custom DrmSessionManager implementations. --- .../main/java/com/google/android/exoplayer2/drm/DrmSession.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 4d64187a8b..df9b1fffa0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -31,7 +31,7 @@ public interface DrmSession { /** Wraps the exception which is the cause of the error state. */ class DrmSessionException extends Exception { - DrmSessionException(Exception e) { + public DrmSessionException(Exception e) { super(e); } From 0d468ca7bf7382537f5540854dc268e8d1a4fa1b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Feb 2017 08:52:50 -0800 Subject: [PATCH 091/106] DASH: Support mspr:pro element Issue: #2386 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147725616 --- .../dash/manifest/DashManifestParser.java | 31 +++++++++++-------- 1 file changed, 18 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 1917399282..5cd0e593be 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 @@ -335,30 +335,35 @@ public class DashManifestParser extends DefaultHandler */ protected SchemeData parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { + String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); + boolean isPlayReady = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95".equals(schemeIdUri); byte[] data = null; UUID uuid = null; - boolean seenPsshElement = false; boolean requiresSecureDecoder = false; do { xpp.next(); - // The cenc:pssh element is defined in 23001-7:2015. - if (XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { - seenPsshElement = true; - data = Base64.decode(xpp.getText(), Base64.DEFAULT); + if (data == null && XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") + && xpp.next() == XmlPullParser.TEXT) { + // The cenc:pssh element is defined in 23001-7:2015. uuid = PsshAtomUtil.parseUuid(data); + if (uuid == null) { + Log.w(TAG, "Skipping malformed cenc:pssh data"); + } else { + data = Base64.decode(xpp.getText(), Base64.DEFAULT); + } + } else if (data == null && isPlayReady && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") + && xpp.next() == XmlPullParser.TEXT) { + // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. + uuid = C.PLAYREADY_UUID; + data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, + Base64.decode(xpp.getText(), Base64.DEFAULT)); } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); - if (!seenPsshElement) { - return null; - } else if (uuid != null) { - return new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder); - } else { - Log.w(TAG, "Skipped unsupported ContentProtection element"); - return null; - } + return data != null ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) + : null; } /** From 4cca2f2d0a7edc4b05d94fa6c71559bfa2a910e1 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Feb 2017 09:49:27 -0800 Subject: [PATCH 092/106] Remove unnecessary null check. Issue #2462 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147731351 --- .../java/com/google/android/exoplayer2/demo/EventLogger.java | 3 --- 1 file changed, 3 deletions(-) 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 edc268ddb9..e39cd16743 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 @@ -101,9 +101,6 @@ import java.util.Locale; @Override public void onTimelineChanged(Timeline timeline, Object manifest) { - if (timeline == null) { - return; - } int periodCount = timeline.getPeriodCount(); int windowCount = timeline.getWindowCount(); Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount); From 636eecf4e78f78323206375443934db40966d7a1 Mon Sep 17 00:00:00 2001 From: twisstosin Date: Thu, 16 Feb 2017 21:30:37 +0100 Subject: [PATCH 093/106] Reverted Font Size Change --- library/src/main/res/layout/exo_playback_control_view.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/res/layout/exo_playback_control_view.xml b/library/src/main/res/layout/exo_playback_control_view.xml index 531ee4c6fa..f8ef5a6fdd 100644 --- a/library/src/main/res/layout/exo_playback_control_view.xml +++ b/library/src/main/res/layout/exo_playback_control_view.xml @@ -58,7 +58,7 @@ Date: Thu, 16 Feb 2017 18:26:32 -0800 Subject: [PATCH 094/106] Use execute instead of submit ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147794314 --- .../google/android/exoplayer2/extractor/Extractor.java | 1 + .../com/google/android/exoplayer2/upstream/Loader.java | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java index 38b0325cba..de3dfd5266 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java @@ -102,4 +102,5 @@ public interface Extractor { * Releases all kept resources. */ void release(); + } diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index 64836dae4c..c9173d3756 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -199,7 +199,7 @@ public final class Loader implements LoaderErrorThrower { currentTask.cancel(true); } if (postLoadAction != null) { - downloadExecutorService.submit(postLoadAction); + downloadExecutorService.execute(postLoadAction); } downloadExecutorService.shutdown(); } @@ -260,7 +260,7 @@ public final class Loader implements LoaderErrorThrower { if (delayMillis > 0) { sendEmptyMessageDelayed(MSG_START, delayMillis); } else { - submitToExecutor(); + execute(); } } @@ -367,9 +367,9 @@ public final class Loader implements LoaderErrorThrower { } } - private void submitToExecutor() { + private void execute() { currentError = null; - downloadExecutorService.submit(currentTask); + downloadExecutorService.execute(currentTask); } private void finish() { From bc9dfa813911a5e9ffd4309c3cfb8bfdeac3a166 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Feb 2017 19:00:00 -0800 Subject: [PATCH 095/106] Fix build ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147796398 --- .../java/com/google/android/exoplayer2/upstream/Loader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index c9173d3756..bca90ddc5c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -334,7 +334,7 @@ public final class Loader implements LoaderErrorThrower { return; } if (msg.what == MSG_START) { - submitToExecutor(); + execute(); return; } if (msg.what == MSG_FATAL_ERROR) { From 14507c45036bb4057f4c3d35bb5beb790cdfee22 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 17 Feb 2017 06:51:29 -0800 Subject: [PATCH 096/106] Fix NPE parsing ContentProtection element ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147833328 --- .../exoplayer2/source/dash/manifest/DashManifestParser.java | 6 +++--- 1 file changed, 3 insertions(+), 3 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 5cd0e593be..d4338fd812 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -345,18 +345,18 @@ public class DashManifestParser extends DefaultHandler if (data == null && XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { // The cenc:pssh element is defined in 23001-7:2015. + data = Base64.decode(xpp.getText(), Base64.DEFAULT); uuid = PsshAtomUtil.parseUuid(data); if (uuid == null) { Log.w(TAG, "Skipping malformed cenc:pssh data"); - } else { - data = Base64.decode(xpp.getText(), Base64.DEFAULT); + data = null; } } else if (data == null && isPlayReady && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") && xpp.next() == XmlPullParser.TEXT) { // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. - uuid = C.PLAYREADY_UUID; data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, Base64.decode(xpp.getText(), Base64.DEFAULT)); + uuid = C.PLAYREADY_UUID; } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); From c3d7eecd1f64ae6cdb120cb543d4994a10c2d02c Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 17 Feb 2017 07:57:49 -0800 Subject: [PATCH 097/106] Fix ChunkExtractorWrapper.init(TrackOutput) calls with null TrackOutput after extractor initialized InitializationChunk calls init(null). When the initialization and index data is separate they need to be loaded separately which results to two init(null) calls. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=147837985 --- .../android/exoplayer2/source/chunk/ChunkExtractorWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 489f63be2b..2a641b80a6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -88,7 +88,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput extractorInitialized = true; } else { extractor.seek(0, 0); - if (sampleFormat != null) { + if (sampleFormat != null && trackOutput != null) { trackOutput.format(sampleFormat); } } From e9399f8684363cf0fc7e2a425513341b9d0b77f0 Mon Sep 17 00:00:00 2001 From: Daniel Santiago Date: Fri, 17 Feb 2017 12:44:13 -0800 Subject: [PATCH 098/106] Added flags to the mp3 extractor to control behavior of the extractor. Added FLAG_ENABLE_CONSTANT_BITRATE_SEEKING to let the extractor know that the CBR seeker is desired in the case where the seeker has determine the track is not seekable For example, in the case where the track has a Xing Header but no content table. --- .../extractor/mp3/Mp3Extractor.java | 39 +++++++++++++++++-- .../exoplayer2/source/hls/HlsMediaChunk.java | 2 +- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index ff84c7da25..e38ae148bd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -31,8 +31,13 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; + +import android.support.annotation.IntDef; + import java.io.EOFException; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Extracts data from an MP3 file. @@ -51,6 +56,19 @@ public final class Mp3Extractor implements Extractor { }; + /** + * Flags controlling the behavior of the extractor. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) + public @interface Flags {} + + /** + * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would + * otherwise not be possible. + */ + public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; + /** * The maximum number of bytes to search when synchronizing, before giving up. */ @@ -72,6 +90,9 @@ public final class Mp3Extractor implements Extractor { private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); + @Flags + private final int flags; + private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; @@ -93,16 +114,27 @@ public final class Mp3Extractor implements Extractor { * Constructs a new {@link Mp3Extractor}. */ public Mp3Extractor() { - this(C.TIME_UNSET); + this(0); } /** * Constructs a new {@link Mp3Extractor}. * + * @param flags Flags that control the extractor's behavior. + */ + public Mp3Extractor(@Flags int flags) { + this(flags, C.TIME_UNSET); + } + + /** + * Constructs a new {@link Mp3Extractor}. + * + * @param flags Flags that control the extractor's behavior. * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or * {@link C#TIME_UNSET} if forcing is not required. */ - public Mp3Extractor(long forcedFirstSampleTimestampUs) { + public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) { + this.flags = flags; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; scratch = new ParsableByteArray(SCRATCH_LENGTH); synchronizedHeader = new MpegAudioHeader(); @@ -350,7 +382,8 @@ public final class Mp3Extractor implements Extractor { } } - if (seeker == null) { + if (seeker == null || (!seeker.isSeekable() + && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { // Repopulate the synchronized header in case we had to skip an invalid seeking header, which // would give an invalid CBR bitrate. input.resetPeekPosition(); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index a3e3559724..5885797896 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -379,7 +379,7 @@ import java.util.concurrent.atomic.AtomicInteger; || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { extractor = new Ac3Extractor(startTimeUs); } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { - extractor = new Mp3Extractor(startTimeUs); + extractor = new Mp3Extractor(0, startTimeUs); } else { throw new IllegalArgumentException("Unkown extension for audio file: " + lastPathSegment); } From 37c15e7ceeeddeadf9d8b7afba991fa074f9f94c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 11:33:43 +0000 Subject: [PATCH 099/106] Minor stylistic tweaks --- .../android/exoplayer2/extractor/mp3/Mp3Extractor.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index e38ae148bd..ef0fdd9393 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -62,7 +62,6 @@ public final class Mp3Extractor implements Extractor { @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}) public @interface Flags {} - /** * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would * otherwise not be possible. @@ -90,9 +89,7 @@ public final class Mp3Extractor implements Extractor { private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); - @Flags - private final int flags; - + @Flags private final int flags; private final long forcedFirstSampleTimestampUs; private final ParsableByteArray scratch; private final MpegAudioHeader synchronizedHeader; From edae29dff754fe4bd410fdb6fcedd60ed2266b9c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 11:35:06 +0000 Subject: [PATCH 100/106] Fix import order --- .../google/android/exoplayer2/extractor/mp3/Mp3Extractor.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index ef0fdd9393..00394f7912 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -31,9 +32,6 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; - -import android.support.annotation.IntDef; - import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Retention; From f16058422df7f42f7a4a20ec9fa4a4128f55fd88 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 12:51:56 +0000 Subject: [PATCH 101/106] Fix import order --- .../src/main/java/com/google/android/exoplayer2/text/Cue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 84b67928bb..3210ffd9a4 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,8 +15,8 @@ */ package com.google.android.exoplayer2.text; -import android.graphics.Color; import android.graphics.Bitmap; +import android.graphics.Color; import android.support.annotation.IntDef; import android.text.Layout.Alignment; import java.lang.annotation.Retention; From 21923ae1faa1db85256a016721bc238a20d2d2b8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 12:55:07 +0000 Subject: [PATCH 102/106] m --- .../com/google/android/exoplayer2/ui/SubtitlePainter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 3e9fb3e68b..555aa84531 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 @@ -157,6 +157,7 @@ import com.google.android.exoplayer2.util.Util; if (!applyEmbeddedStyles) { // Strip out any embedded styling. cueText = cueText.toString(); + windowColor = style.windowColor; } } else { cueBitmap = cue.bitmap; @@ -190,7 +191,7 @@ import com.google.android.exoplayer2.util.Util; this.cueText = cueText; this.cueTextAlignment = cue.textAlignment; - this.cueBitmap = cue.bitmap; + this.cueBitmap = cueBitmap; this.cueLine = cue.line; this.cueLineType = cue.lineType; this.cueLineAnchor = cue.lineAnchor; @@ -200,7 +201,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 31513202df8c186764ad5d2d1d14a8a7fa49a7f6 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 13:01:00 +0000 Subject: [PATCH 103/106] Fix subtitle painter issues --- .../android/exoplayer2/ui/SubtitlePainter.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 555aa84531..04a6bafd3d 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 @@ -65,9 +65,9 @@ import com.google.android.exoplayer2.util.Util; private final Paint paint; // Previous input variables. - private Bitmap cueBitmap; private CharSequence cueText; private Alignment cueTextAlignment; + private Bitmap cueBitmap; private float cueLine; @Cue.LineType private int cueLineType; @@ -148,12 +148,14 @@ import com.google.android.exoplayer2.util.Util; boolean isTextCue = cue.bitmap == null; CharSequence cueText = null; Bitmap cueBitmap = null; + int windowColor = Color.BLACK; if (isTextCue) { cueText = cue.text; if (TextUtils.isEmpty(cueText)) { // Nothing to draw. return; } + windowColor = cue.windowColorSet ? cue.windowColor : style.windowColor; if (!applyEmbeddedStyles) { // Strip out any embedded styling. cueText = cueText.toString(); @@ -174,7 +176,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) @@ -275,7 +277,7 @@ import com.google.android.exoplayer2.util.Util; if (cueLine >= 0) { anchorPosition = Math.round(cueLine * firstLineHeight) + parentTop; } else { - anchorPosition = Math.round(cueLine * firstLineHeight) + parentBottom; + anchorPosition = Math.round((cueLine + 1) * firstLineHeight) + parentBottom; } } textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight @@ -309,8 +311,8 @@ import com.google.android.exoplayer2.util.Util; int height = (int) (width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); int x = (int) (cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); - int y = (int) (cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - width) - : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (width / 2)) : anchorY); + int y = (int) (cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) + : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); bitmapRect = new Rect(x, y, x + width, y + height); } From 5fbf10969453d823ebe85e39573712669695ef41 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 13:03:03 +0000 Subject: [PATCH 104/106] Use Math.round instead of floor. --- .../com/google/android/exoplayer2/ui/SubtitlePainter.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 04a6bafd3d..6a1f31d270 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 @@ -307,11 +307,11 @@ import com.google.android.exoplayer2.util.Util; int parentHeight = parentBottom - parentTop; float anchorX = parentLeft + (parentWidth * cuePosition); float anchorY = parentTop + (parentHeight * cueLine); - int width = (int) (parentWidth * cueSize); - int height = (int) (width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); - int x = (int) (cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) + int width = Math.round(parentWidth * cueSize); + int height = Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth())); + int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END ? (anchorX - width) : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2)) : anchorX); - int y = (int) (cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) + int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END ? (anchorY - height) : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2)) : anchorY); bitmapRect = new Rect(x, y, x + width, y + height); } From 539072dbf4236610be56224c836b6b0d1677fdb3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 13:09:40 +0000 Subject: [PATCH 105/106] Remove useless Cue constructor --- .../java/com/google/android/exoplayer2/text/Cue.java | 10 ---------- 1 file changed, 10 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 3210ffd9a4..96cd76a957 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 @@ -177,16 +177,6 @@ public class Cue { */ public final int windowColor; - /** - * Constructs an image cue whose type parameters are set to {@link #TYPE_UNSET} and whose - * dimension parameters are set to {@link #DIMEN_UNSET}. - * - * @param bitmap See {@link #bitmap}. - */ - public Cue(Bitmap bitmap) { - this(bitmap, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET); - } - /** * Creates an image cue. * From 11c16d83fdf65c6f35d8852e08baa7b75ffe1a47 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 20 Feb 2017 13:13:13 +0000 Subject: [PATCH 106/106] Final nit fixes for Cue/SubtitlePainter --- .../src/main/java/com/google/android/exoplayer2/text/Cue.java | 3 ++- .../java/com/google/android/exoplayer2/ui/SubtitlePainter.java | 2 +- 2 files changed, 3 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 96cd76a957..176b8ea815 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 @@ -180,6 +180,7 @@ public class Cue { /** * Creates an image cue. * + * @param bitmap See {@link #bitmap}. * @param horizontalPosition The position of the horizontal anchor within the viewport, expressed * as a fraction of the viewport width. * @param horizontalPositionAnchor The horizontal anchor. One of {@link #ANCHOR_TYPE_START}, @@ -197,7 +198,7 @@ public class Cue { } /** - * Constructs a text cue whose {@link #textAlignment} is null, whose type parameters are set to + * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}. * * @param text See {@link #text}. 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 6a1f31d270..5ca97403f1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -325,7 +325,7 @@ import com.google.android.exoplayer2.util.Util; } private void drawTextLayout(Canvas canvas) { - final StaticLayout layout = textLayout; + StaticLayout layout = textLayout; if (layout == null) { // Nothing to draw. return;