Commit 8cbcfd66 by ojw28 Committed by GitHub

Merge pull request #4556 from google/dev-v2-r2.8.3

r2.8.3
parents f7ed789f 563f13a8
Showing with 906 additions and 378 deletions
# Release notes # # Release notes #
### 2.8.3 ###
* IMA:
* Fix behavior when creating/releasing the player then releasing
`ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)).
* Add support for setting slots for companion ads.
* Captions:
* TTML: Fix an issue with TTML using font size as % of cell resolution that
makes `SubtitleView.setApplyEmbeddedFontSizes()` not work correctly.
([#4491](https://github.com/google/ExoPlayer/issues/4491)).
* CEA-608: Improve handling of embedded styles
([#4321](https://github.com/google/ExoPlayer/issues/4321)).
* DASH:
* Exclude text streams from duration calculations
([#4029](https://github.com/google/ExoPlayer/issues/4029)).
* Fix freezing when playing multi-period manifests with `EventStream`s
([#4492](https://github.com/google/ExoPlayer/issues/4492)).
* DRM: Allow DrmInitData to carry a license server URL
([#3393](https://github.com/google/ExoPlayer/issues/3393)).
* MPEG-TS: Fix bug preventing SCTE-35 cues from being output
([#4573](https://github.com/google/ExoPlayer/issues/4573)).
* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using
CommentFrame to InternalFrame for frames with gapless metadata in MP4.
* Add `PlayerView.isControllerVisible`
([#4385](https://github.com/google/ExoPlayer/issues/4385)).
* Fix issue playing DRM protected streams on Asus Zenfone 2
([#4403](https://github.com/google/ExoPlayer/issues/4413)).
* Add support for multiple audio and video tracks in MPEG-PS streams
([#4406](https://github.com/google/ExoPlayer/issues/4406)).
* Add workaround for track index mismatches between trex and tkhd boxes in
fragmented MP4 files
([#4477](https://github.com/google/ExoPlayer/issues/4477)).
* Add workaround for track index mismatches between tfhd and tkhd boxes in
fragmented MP4 files
([#4083](https://github.com/google/ExoPlayer/issues/4083)).
* Ignore all MP4 edit lists if one edit list couldn't be handled
([#4348](https://github.com/google/ExoPlayer/issues/4348)).
* Fix issue when switching track selection from an embedded track to a primary
track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)).
* Fix accessibility class name for `DefaultTimeBar`
([#4611](https://github.com/google/ExoPlayer/issues/4611)).
* Improved compatibility with FireOS devices.
### 2.8.2 ### ### 2.8.2 ###
* IMA: Don't advertise support for video/mpeg ad media, as we don't have an * IMA: Don't advertise support for video/mpeg ad media, as we don't have an
......
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.8.2' releaseVersion = '2.8.3'
releaseVersionCode = 2802 releaseVersionCode = 2803
// Important: ExoPlayer specifies a minSdkVersion of 14 because various // Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices. // components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided // However, please note that the core media playback functionality provided
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.castdemo; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.castdemo;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
...@@ -282,7 +283,7 @@ import java.util.ArrayList; ...@@ -282,7 +283,7 @@ import java.util.ArrayList;
@Override @Override
public void onTimelineChanged( public void onTimelineChanged(
Timeline timeline, Object manifest, @TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
updateCurrentItemIndex(); updateCurrentItemIndex();
if (timeline.isEmpty()) { if (timeline.isEmpty()) {
castMediaQueueCreationPending = true; castMediaQueueCreationPending = true;
......
...@@ -38,6 +38,7 @@ import com.google.ads.interactivemedia.v3.api.AdsManager; ...@@ -38,6 +38,7 @@ import com.google.ads.interactivemedia.v3.api.AdsManager;
import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest; import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.CompanionAdSlot;
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
...@@ -62,6 +63,7 @@ import java.lang.annotation.Retention; ...@@ -62,6 +63,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -267,13 +269,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -267,13 +269,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
/** The expected ad group index that IMA should load next. */ /** The expected ad group index that IMA should load next. */
private int expectedAdGroupIndex; private int expectedAdGroupIndex;
/** /** The index of the current ad group that IMA is loading. */
* The index of the current ad group that IMA is loading.
*/
private int adGroupIndex; private int adGroupIndex;
/** /** Whether IMA has sent an ad event to pause content since the last resume content event. */
* Whether IMA has sent an ad event to pause content since the last resume content event.
*/
private boolean imaPausedContent; private boolean imaPausedContent;
/** The current ad playback state. */ /** The current ad playback state. */
private @ImaAdState int imaAdState; private @ImaAdState int imaAdState;
...@@ -285,9 +283,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -285,9 +283,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Fields tracking the player/loader state. // Fields tracking the player/loader state.
/** /** Whether the player is playing an ad. */
* Whether the player is playing an ad.
*/
private boolean playingAd; private boolean playingAd;
/** /**
* If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET}
...@@ -310,13 +306,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -310,13 +306,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
* content progress should increase. {@link C#TIME_UNSET} otherwise. * content progress should increase. {@link C#TIME_UNSET} otherwise.
*/ */
private long fakeContentProgressOffsetMs; private long fakeContentProgressOffsetMs;
/** /** Stores the pending content position when a seek operation was intercepted to play an ad. */
* Stores the pending content position when a seek operation was intercepted to play an ad.
*/
private long pendingContentPositionMs; private long pendingContentPositionMs;
/** /** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */
* Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA.
*/
private boolean sentPendingContentPositionMs; private boolean sentPendingContentPositionMs;
/** /**
...@@ -406,6 +398,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -406,6 +398,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
} }
/** /**
* Sets the slots for displaying companion ads. Individual slots can be created using {@link
* ImaSdkFactory#createCompanionAdSlot()}.
*
* @param companionSlots Slots for displaying companion ads.
* @see AdDisplayContainer#setCompanionSlots(Collection)
*/
public void setCompanionSlots(Collection<CompanionAdSlot> companionSlots) {
adDisplayContainer.setCompanionSlots(companionSlots);
}
/**
* Requests ads, if they have not already been requested. Must be called on the main thread. * Requests ads, if they have not already been requested. Must be called on the main thread.
* *
* <p>Ads will be requested automatically when the player is prepared if this method has not been * <p>Ads will be requested automatically when the player is prepared if this method has not been
...@@ -509,6 +512,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -509,6 +512,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adsManager.destroy(); adsManager.destroy();
adsManager = null; adsManager = null;
} }
imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE;
pendingAdLoadError = null;
adPlaybackState = AdPlaybackState.NONE;
updateAdPlaybackState();
} }
@Override @Override
...@@ -558,7 +566,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -558,7 +566,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(TAG, "onAdEvent: " + adEventType); Log.d(TAG, "onAdEvent: " + adEventType);
} }
if (adsManager == null) { if (adsManager == null) {
Log.w(TAG, "Dropping ad event after release: " + adEvent); Log.w(TAG, "Ignoring AdEvent after release: " + adEvent);
return; return;
} }
try { try {
...@@ -654,6 +662,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -654,6 +662,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override @Override
public void loadAd(String adUriString) { public void loadAd(String adUriString) {
try { try {
if (DEBUG) {
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
}
if (adsManager == null) {
Log.w(TAG, "Ignoring loadAd after release");
return;
}
if (adGroupIndex == C.INDEX_UNSET) { if (adGroupIndex == C.INDEX_UNSET) {
Log.w( Log.w(
TAG, TAG,
...@@ -662,9 +677,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -662,9 +677,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adGroupIndex = expectedAdGroupIndex; adGroupIndex = expectedAdGroupIndex;
adsManager.start(); adsManager.start();
} }
if (DEBUG) {
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
}
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
if (adIndexInAdGroup == C.INDEX_UNSET) { if (adIndexInAdGroup == C.INDEX_UNSET) {
Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads");
...@@ -693,6 +705,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -693,6 +705,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "playAd"); Log.d(TAG, "playAd");
} }
if (adsManager == null) {
Log.w(TAG, "Ignoring playAd after release");
return;
}
switch (imaAdState) { switch (imaAdState) {
case IMA_AD_STATE_PLAYING: case IMA_AD_STATE_PLAYING:
// IMA does not always call stopAd before resuming content. // IMA does not always call stopAd before resuming content.
...@@ -736,6 +752,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -736,6 +752,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "stopAd"); Log.d(TAG, "stopAd");
} }
if (adsManager == null) {
Log.w(TAG, "Ignoring stopAd after release");
return;
}
if (player == null) { if (player == null) {
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
Log.w(TAG, "Unexpected stopAd while detached"); Log.w(TAG, "Unexpected stopAd while detached");
...@@ -775,8 +795,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -775,8 +795,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Player.EventListener implementation. // Player.EventListener implementation.
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, public void onTimelineChanged(
@Player.TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
if (reason == Player.TIMELINE_CHANGE_REASON_RESET) { if (reason == Player.TIMELINE_CHANGE_REASON_RESET) {
// The player is being reset and this source will be released. // The player is being reset and this source will be released.
return; return;
...@@ -1083,6 +1103,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -1083,6 +1103,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d( Log.d(
TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
} }
if (adsManager == null) {
Log.w(TAG, "Ignoring ad prepare error after release");
return;
}
if (imaAdState == IMA_AD_STATE_NONE) { if (imaAdState == IMA_AD_STATE_NONE) {
// Send IMA a content position at the ad group so that it will try to play it, at which point // Send IMA a content position at the ad group so that it will try to play it, at which point
// we can notify that it failed to load. // we can notify that it failed to load.
...@@ -1165,7 +1189,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ...@@ -1165,7 +1189,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.e(TAG, message, cause); Log.e(TAG, message, cause);
// We can't recover from an unexpected error in general, so skip all remaining ads. // We can't recover from an unexpected error in general, so skip all remaining ads.
if (adPlaybackState == null) { if (adPlaybackState == null) {
adPlaybackState = new AdPlaybackState(); adPlaybackState = AdPlaybackState.NONE;
} else { } else {
for (int i = 0; i < adPlaybackState.adGroupCount; i++) { for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i); adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
......
...@@ -281,8 +281,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { ...@@ -281,8 +281,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
} }
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, public void onTimelineChanged(
@TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
Callback callback = getCallback(); Callback callback = getCallback();
callback.onDurationChanged(LeanbackPlayerAdapter.this); callback.onDurationChanged(LeanbackPlayerAdapter.this);
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
......
...@@ -674,8 +674,8 @@ public final class MediaSessionConnector { ...@@ -674,8 +674,8 @@ public final class MediaSessionConnector {
private int currentWindowCount; private int currentWindowCount;
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, public void onTimelineChanged(
@Player.TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
int windowCount = player.getCurrentTimeline().getWindowCount(); int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex(); int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) { if (queueNavigator != null) {
......
...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.8.2"; public static final String VERSION = "2.8.3";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.2"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.3";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2008002; public static final int VERSION_INT = 2008003;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -80,6 +80,13 @@ public final class Format implements Parcelable { ...@@ -80,6 +80,13 @@ public final class Format implements Parcelable {
/** DRM initialization data if the stream is protected, or null otherwise. */ /** DRM initialization data if the stream is protected, or null otherwise. */
public final @Nullable DrmInitData drmInitData; public final @Nullable DrmInitData drmInitData;
/**
* For samples that contain subsamples, this is an offset that should be added to subsample
* timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are
* relative to the timestamps of their parent samples.
*/
public final long subsampleOffsetUs;
// Video specific. // Video specific.
/** /**
...@@ -141,15 +148,6 @@ public final class Format implements Parcelable { ...@@ -141,15 +148,6 @@ public final class Format implements Parcelable {
*/ */
public final int encoderPadding; public final int encoderPadding;
// Text specific.
/**
* For samples that contain subsamples, this is an offset that should be added to subsample
* timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are
* relative to the timestamps of their parent samples.
*/
public final long subsampleOffsetUs;
// Audio and text specific. // Audio and text specific.
/** /**
......
...@@ -228,11 +228,13 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -228,11 +228,13 @@ import com.google.android.exoplayer2.util.Assertions;
reading = playing.next; reading = playing.next;
} }
playing.release(); playing.release();
playing = playing.next;
length--; length--;
if (length == 0) { if (length == 0) {
loading = null; loading = null;
oldFrontPeriodUid = playing.uid;
oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber;
} }
playing = playing.next;
} else { } else {
playing = loading; playing = loading;
reading = loading; reading = loading;
......
...@@ -191,7 +191,8 @@ public interface Player { ...@@ -191,7 +191,8 @@ public interface Player {
* @param manifest The latest manifest. May be null. * @param manifest The latest manifest. May be null.
* @param reason The {@link TimelineChangeReason} responsible for this timeline change. * @param reason The {@link TimelineChangeReason} responsible for this timeline change.
*/ */
void onTimelineChanged(Timeline timeline, Object manifest, @TimelineChangeReason int reason); void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason);
/** /**
* Called when the available or selected tracks change. * Called when the available or selected tracks change.
...@@ -281,8 +282,8 @@ public interface Player { ...@@ -281,8 +282,8 @@ public interface Player {
abstract class DefaultEventListener implements EventListener { abstract class DefaultEventListener implements EventListener {
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, public void onTimelineChanged(
@TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
// Call deprecated version. Otherwise, do nothing. // Call deprecated version. Otherwise, do nothing.
onTimelineChanged(timeline, manifest); onTimelineChanged(timeline, manifest);
} }
...@@ -337,7 +338,7 @@ public interface Player { ...@@ -337,7 +338,7 @@ public interface Player {
* instead. * instead.
*/ */
@Deprecated @Deprecated
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) {
// Do nothing. // Do nothing.
} }
......
...@@ -420,7 +420,7 @@ public class AnalyticsCollector ...@@ -420,7 +420,7 @@ public class AnalyticsCollector
@Override @Override
public final void onTimelineChanged( public final void onTimelineChanged(
Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
mediaPeriodQueueTracker.onTimelineChanged(timeline); mediaPeriodQueueTracker.onTimelineChanged(timeline);
EventTime eventTime = generatePlayingMediaPeriodEventTime(); EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) { for (AnalyticsListener listener : listeners) {
......
...@@ -33,12 +33,12 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor { ...@@ -33,12 +33,12 @@ public final class SilenceSkippingAudioProcessor implements AudioProcessor {
* The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify * The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify
* that part of audio as silent, in microseconds. * that part of audio as silent, in microseconds.
*/ */
private static final long MINIMUM_SILENCE_DURATION_US = 100_000; private static final long MINIMUM_SILENCE_DURATION_US = 150_000;
/** /**
* The duration of silence by which to extend non-silent sections, in microseconds. The value must * The duration of silence by which to extend non-silent sections, in microseconds. The value must
* not exceed {@link #MINIMUM_SILENCE_DURATION_US}. * not exceed {@link #MINIMUM_SILENCE_DURATION_US}.
*/ */
private static final long PADDING_SILENCE_US = 10_000; private static final long PADDING_SILENCE_US = 20_000;
/** /**
* The absolute level below which an individual PCM sample is classified as silent. Note: the * The absolute level below which an individual PCM sample is classified as silent. Note: the
* specified value will be rounded so that the threshold check only depends on the more * specified value will be rounded so that the threshold check only depends on the more
......
...@@ -32,7 +32,6 @@ import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; ...@@ -32,7 +32,6 @@ import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
...@@ -89,7 +88,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -89,7 +88,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3;
private static final String TAG = "DefaultDrmSessionMgr"; private static final String TAG = "DefaultDrmSessionMgr";
private static final String CENC_SCHEME_MIME_TYPE = "cenc";
private final UUID uuid; private final UUID uuid;
private final ExoMediaDrm<T> mediaDrm; private final ExoMediaDrm<T> mediaDrm;
...@@ -509,17 +507,14 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -509,17 +507,14 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
} }
} }
byte[] initData = null; SchemeData schemeData = null;
String mimeType = null;
if (offlineLicenseKeySetId == null) { if (offlineLicenseKeySetId == null) {
SchemeData data = getSchemeData(drmInitData, uuid, false); schemeData = getSchemeData(drmInitData, uuid, false);
if (data == null) { if (schemeData == null) {
final MissingSchemeDataException error = new MissingSchemeDataException(uuid); final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
eventDispatcher.drmSessionManagerError(error); eventDispatcher.drmSessionManagerError(error);
return new ErrorStateDrmSession<>(new DrmSessionException(error)); return new ErrorStateDrmSession<>(new DrmSessionException(error));
} }
initData = getSchemeInitData(data, uuid);
mimeType = getSchemeMimeType(data, uuid);
} }
DefaultDrmSession<T> session; DefaultDrmSession<T> session;
...@@ -528,6 +523,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -528,6 +523,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
} else { } else {
// Only use an existing session if it has matching init data. // Only use an existing session if it has matching init data.
session = null; session = null;
byte[] initData = schemeData != null ? schemeData.data : null;
for (DefaultDrmSession<T> existingSession : sessions) { for (DefaultDrmSession<T> existingSession : sessions) {
if (existingSession.hasInitData(initData)) { if (existingSession.hasInitData(initData)) {
session = existingSession; session = existingSession;
...@@ -543,8 +539,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -543,8 +539,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
uuid, uuid,
mediaDrm, mediaDrm,
this, this,
initData, schemeData,
mimeType,
mode, mode,
offlineLicenseKeySetId, offlineLicenseKeySetId,
optionalKeyRequestParameters, optionalKeyRequestParameters,
...@@ -650,31 +645,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -650,31 +645,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
return matchingSchemeDatas.get(0); return matchingSchemeDatas.get(0);
} }
private static byte[] getSchemeInitData(SchemeData data, UUID uuid) {
byte[] schemeInitData = data.data;
if (Util.SDK_INT < 21) {
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, uuid);
if (psshData == null) {
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
} else {
schemeInitData = psshData;
}
}
return schemeInitData;
}
private static String getSchemeMimeType(SchemeData data, UUID uuid) {
String schemeMimeType = data.mimeType;
if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid)
&& (MimeTypes.VIDEO_MP4.equals(schemeMimeType)
|| MimeTypes.AUDIO_MP4.equals(schemeMimeType))) {
// Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
schemeMimeType = CENC_SCHEME_MIME_TYPE;
}
return schemeMimeType;
}
@SuppressLint("HandlerLeak") @SuppressLint("HandlerLeak")
private class MediaDrmHandler extends Handler { private class MediaDrmHandler extends Handler {
......
...@@ -266,9 +266,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -266,9 +266,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* applies to all schemes). * applies to all schemes).
*/ */
private final UUID uuid; private final UUID uuid;
/** /** The URL of the server to which license requests should be made. May be null if unknown. */
* The mimeType of {@link #data}. public final @Nullable String licenseServerUrl;
*/ /** The mimeType of {@link #data}. */
public final String mimeType; public final String mimeType;
/** /**
* The initialization data. May be null for scheme support checks only. * The initialization data. May be null for scheme support checks only.
...@@ -297,7 +297,25 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -297,7 +297,25 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* @param requiresSecureDecryption See {@link #requiresSecureDecryption}. * @param requiresSecureDecryption See {@link #requiresSecureDecryption}.
*/ */
public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) { public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) {
this(uuid, /* licenseServerUrl= */ null, mimeType, data, requiresSecureDecryption);
}
/**
* @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is
* universal (i.e. applies to all schemes).
* @param licenseServerUrl See {@link #licenseServerUrl}.
* @param mimeType See {@link #mimeType}.
* @param data See {@link #data}.
* @param requiresSecureDecryption See {@link #requiresSecureDecryption}.
*/
public SchemeData(
UUID uuid,
@Nullable String licenseServerUrl,
String mimeType,
byte[] data,
boolean requiresSecureDecryption) {
this.uuid = Assertions.checkNotNull(uuid); this.uuid = Assertions.checkNotNull(uuid);
this.licenseServerUrl = licenseServerUrl;
this.mimeType = Assertions.checkNotNull(mimeType); this.mimeType = Assertions.checkNotNull(mimeType);
this.data = data; this.data = data;
this.requiresSecureDecryption = requiresSecureDecryption; this.requiresSecureDecryption = requiresSecureDecryption;
...@@ -305,6 +323,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -305,6 +323,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
/* package */ SchemeData(Parcel in) { /* package */ SchemeData(Parcel in) {
uuid = new UUID(in.readLong(), in.readLong()); uuid = new UUID(in.readLong(), in.readLong());
licenseServerUrl = in.readString();
mimeType = in.readString(); mimeType = in.readString();
data = in.createByteArray(); data = in.createByteArray();
requiresSecureDecryption = in.readByte() != 0; requiresSecureDecryption = in.readByte() != 0;
...@@ -346,7 +365,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -346,7 +365,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
return true; return true;
} }
SchemeData other = (SchemeData) obj; SchemeData other = (SchemeData) obj;
return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid) return Util.areEqual(licenseServerUrl, other.licenseServerUrl)
&& Util.areEqual(mimeType, other.mimeType)
&& Util.areEqual(uuid, other.uuid)
&& Arrays.equals(data, other.data); && Arrays.equals(data, other.data);
} }
...@@ -354,6 +375,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -354,6 +375,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
public int hashCode() { public int hashCode() {
if (hashCode == 0) { if (hashCode == 0) {
int result = uuid.hashCode(); int result = uuid.hashCode();
result = 31 * result + (licenseServerUrl == null ? 0 : licenseServerUrl.hashCode());
result = 31 * result + mimeType.hashCode(); result = 31 * result + mimeType.hashCode();
result = 31 * result + Arrays.hashCode(data); result = 31 * result + Arrays.hashCode(data);
hashCode = result; hashCode = result;
...@@ -372,6 +394,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -372,6 +394,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(uuid.getMostSignificantBits()); dest.writeLong(uuid.getMostSignificantBits());
dest.writeLong(uuid.getLeastSignificantBits()); dest.writeLong(uuid.getLeastSignificantBits());
dest.writeString(licenseServerUrl);
dest.writeString(mimeType); dest.writeString(mimeType);
dest.writeByteArray(data); dest.writeByteArray(data);
dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0)); dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0));
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.DeniedByServerException; import android.media.DeniedByServerException;
import android.media.MediaCrypto; import android.media.MediaCrypto;
...@@ -26,7 +27,9 @@ import android.media.UnsupportedSchemeException; ...@@ -26,7 +27,9 @@ import android.media.UnsupportedSchemeException;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
...@@ -40,6 +43,8 @@ import java.util.UUID; ...@@ -40,6 +43,8 @@ import java.util.UUID;
@TargetApi(23) @TargetApi(23)
public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> { public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> {
private static final String CENC_SCHEME_MIME_TYPE = "cenc";
private final UUID uuid; private final UUID uuid;
private final MediaDrm mediaDrm; private final MediaDrm mediaDrm;
...@@ -60,6 +65,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -60,6 +65,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
} }
} }
@SuppressLint("WrongConstant")
private FrameworkMediaDrm(UUID uuid) throws UnsupportedSchemeException { private FrameworkMediaDrm(UUID uuid) throws UnsupportedSchemeException {
Assertions.checkNotNull(uuid); Assertions.checkNotNull(uuid);
Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
...@@ -67,6 +73,9 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -67,6 +73,9 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
uuid = Util.SDK_INT < 27 && C.CLEARKEY_UUID.equals(uuid) ? C.COMMON_PSSH_UUID : uuid; uuid = Util.SDK_INT < 27 && C.CLEARKEY_UUID.equals(uuid) ? C.COMMON_PSSH_UUID : uuid;
this.uuid = uuid; this.uuid = uuid;
this.mediaDrm = new MediaDrm(uuid); this.mediaDrm = new MediaDrm(uuid);
if (C.WIDEVINE_UUID.equals(uuid) && needsForceL3Workaround()) {
mediaDrm.setPropertyString("securityLevel", "L3");
}
} }
@Override @Override
...@@ -116,14 +125,49 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -116,14 +125,49 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
@Override @Override
public KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType, public KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType,
HashMap<String, String> optionalParameters) throws NotProvisionedException { HashMap<String, String> optionalParameters) throws NotProvisionedException {
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom. Some Amazon
// devices also required data to be extracted from the PSSH atom for PlayReady.
if ((Util.SDK_INT < 21 && C.WIDEVINE_UUID.equals(uuid))
|| (C.PLAYREADY_UUID.equals(uuid)
&& "Amazon".equals(Util.MANUFACTURER)
&& ("AFTB".equals(Util.MODEL) // Fire TV Gen 1
|| "AFTS".equals(Util.MODEL) // Fire TV Gen 2
|| "AFTM".equals(Util.MODEL)))) { // Fire TV Stick Gen 1
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(init, uuid);
if (psshData == null) {
// Extraction failed. schemeData isn't a PSSH atom, so leave it unchanged.
} else {
init = psshData;
}
}
// Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
if (Util.SDK_INT < 26
&& C.CLEARKEY_UUID.equals(uuid)
&& (MimeTypes.VIDEO_MP4.equals(mimeType) || MimeTypes.AUDIO_MP4.equals(mimeType))) {
mimeType = CENC_SCHEME_MIME_TYPE;
}
final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType, final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType,
optionalParameters); optionalParameters);
return new DefaultKeyRequest(request.getData(), request.getDefaultUrl());
byte[] requestData = request.getData();
if (C.CLEARKEY_UUID.equals(uuid)) {
requestData = ClearKeyUtil.adjustRequestData(requestData);
}
return new DefaultKeyRequest(requestData, request.getDefaultUrl());
} }
@Override @Override
public byte[] provideKeyResponse(byte[] scope, byte[] response) public byte[] provideKeyResponse(byte[] scope, byte[] response)
throws NotProvisionedException, DeniedByServerException { throws NotProvisionedException, DeniedByServerException {
if (C.CLEARKEY_UUID.equals(uuid)) {
response = ClearKeyUtil.adjustResponseData(response);
}
return mediaDrm.provideKeyResponse(scope, response); return mediaDrm.provideKeyResponse(scope, response);
} }
...@@ -183,4 +227,12 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -183,4 +227,12 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
forceAllowInsecureDecoderComponents); forceAllowInsecureDecoderComponents);
} }
/**
* Returns whether the device codec is known to fail if security level L1 is used.
*
* <p>See <a href="https://github.com/google/ExoPlayer/issues/4413">GitHub issue #4413</a>.
*/
private static boolean needsForceL3Workaround() {
return "ASUS_Z00AD".equals(Util.MODEL);
}
} }
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
...@@ -114,8 +115,13 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { ...@@ -114,8 +115,13 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
} }
@Override @Override
public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { public byte[] executeKeyRequest(
UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl)
throws Exception {
String url = request.getDefaultUrl(); String url = request.getDefaultUrl();
if (TextUtils.isEmpty(url)) {
url = mediaProvidedLicenseServerUrl;
}
if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) { if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
url = defaultLicenseUrl; url = defaultLicenseUrl;
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -44,7 +45,9 @@ public final class LocalMediaDrmCallback implements MediaDrmCallback { ...@@ -44,7 +45,9 @@ public final class LocalMediaDrmCallback implements MediaDrmCallback {
} }
@Override @Override
public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { public byte[] executeKeyRequest(
UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl)
throws Exception {
return keyResponse; return keyResponse;
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import java.util.UUID; import java.util.UUID;
...@@ -38,10 +39,13 @@ public interface MediaDrmCallback { ...@@ -38,10 +39,13 @@ public interface MediaDrmCallback {
* Executes a key request. * Executes a key request.
* *
* @param uuid The UUID of the content protection scheme. * @param uuid The UUID of the content protection scheme.
* @param request The request. * @param request The request generated by the content decryption module.
* @param mediaProvidedLicenseServerUrl A license server URL provided by the media, or null if the
* media does not include any license server URL.
* @return The response data. * @return The response data.
* @throws Exception If an error occurred executing the request. * @throws Exception If an error occurred executing the request.
*/ */
byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception; byte[] executeKeyRequest(
UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl)
throws Exception;
} }
...@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format; ...@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
import com.google.android.exoplayer2.metadata.id3.InternalFrame;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
...@@ -39,7 +40,8 @@ public final class GaplessInfoHolder { ...@@ -39,7 +40,8 @@ public final class GaplessInfoHolder {
} }
}; };
private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; private static final String GAPLESS_DOMAIN = "com.apple.iTunes";
private static final String GAPLESS_DESCRIPTION = "iTunSMPB";
private static final Pattern GAPLESS_COMMENT_PATTERN = private static final Pattern GAPLESS_COMMENT_PATTERN =
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
...@@ -91,7 +93,15 @@ public final class GaplessInfoHolder { ...@@ -91,7 +93,15 @@ public final class GaplessInfoHolder {
Metadata.Entry entry = metadata.get(i); Metadata.Entry entry = metadata.get(i);
if (entry instanceof CommentFrame) { if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry; CommentFrame commentFrame = (CommentFrame) entry;
if (setFromComment(commentFrame.description, commentFrame.text)) { if (GAPLESS_DESCRIPTION.equals(commentFrame.description)
&& setFromComment(commentFrame.text)) {
return true;
}
} else if (entry instanceof InternalFrame) {
InternalFrame internalFrame = (InternalFrame) entry;
if (GAPLESS_DOMAIN.equals(internalFrame.domain)
&& GAPLESS_DESCRIPTION.equals(internalFrame.description)
&& setFromComment(internalFrame.text)) {
return true; return true;
} }
} }
...@@ -103,14 +113,10 @@ public final class GaplessInfoHolder { ...@@ -103,14 +113,10 @@ public final class GaplessInfoHolder {
* Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header
* or MPEG 4 user data), if valid and non-zero. * or MPEG 4 user data), if valid and non-zero.
* *
* @param name The comment's identifier.
* @param data The comment's payload data. * @param data The comment's payload data.
* @return Whether the holder was populated. * @return Whether the holder was populated.
*/ */
private boolean setFromComment(String name, String data) { private boolean setFromComment(String data) {
if (!GAPLESS_COMMENT_ID.equals(name)) {
return false;
}
Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
if (matcher.find()) { if (matcher.find()) {
try { try {
......
...@@ -616,10 +616,10 @@ public final class MatroskaExtractor implements Extractor { ...@@ -616,10 +616,10 @@ public final class MatroskaExtractor implements Extractor {
currentTrack.number = (int) value; currentTrack.number = (int) value;
break; break;
case ID_FLAG_DEFAULT: case ID_FLAG_DEFAULT:
currentTrack.flagForced = value == 1; currentTrack.flagDefault = value == 1;
break; break;
case ID_FLAG_FORCED: case ID_FLAG_FORCED:
currentTrack.flagDefault = value == 1; currentTrack.flagForced = value == 1;
break; break;
case ID_TRACK_TYPE: case ID_TRACK_TYPE:
currentTrack.type = (int) value; currentTrack.type = (int) value;
......
...@@ -43,6 +43,9 @@ import java.util.List; ...@@ -43,6 +43,9 @@ import java.util.List;
*/ */
/* package */ final class AtomParsers { /* package */ final class AtomParsers {
/** Thrown if an edit list couldn't be applied. */
public static final class UnhandledEditListException extends ParserException {}
private static final String TAG = "AtomParsers"; private static final String TAG = "AtomParsers";
private static final int TYPE_vide = Util.getIntegerCodeForString("vide"); private static final int TYPE_vide = Util.getIntegerCodeForString("vide");
...@@ -117,10 +120,12 @@ import java.util.List; ...@@ -117,10 +120,12 @@ import java.util.List;
* @param stblAtom stbl (sample table) atom to decode. * @param stblAtom stbl (sample table) atom to decode.
* @param gaplessInfoHolder Holder to populate with gapless playback information. * @param gaplessInfoHolder Holder to populate with gapless playback information.
* @return Sample table described by the stbl atom. * @return Sample table described by the stbl atom.
* @throws ParserException If the resulting sample sequence does not contain a sync sample. * @throws UnhandledEditListException Thrown if the edit list can't be applied.
* @throws ParserException Thrown if the stbl atom can't be parsed.
*/ */
public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom, public static TrackSampleTable parseStbl(
GaplessInfoHolder gaplessInfoHolder) throws ParserException { Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder)
throws ParserException {
SampleSizeBox sampleSizeBox; SampleSizeBox sampleSizeBox;
Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz);
if (stszAtom != null) { if (stszAtom != null) {
...@@ -136,7 +141,13 @@ import java.util.List; ...@@ -136,7 +141,13 @@ import java.util.List;
int sampleCount = sampleSizeBox.getSampleCount(); int sampleCount = sampleSizeBox.getSampleCount();
if (sampleCount == 0) { if (sampleCount == 0) {
return new TrackSampleTable( return new TrackSampleTable(
new long[0], new int[0], 0, new long[0], new int[0], C.TIME_UNSET); track,
/* offsets= */ new long[0],
/* sizes= */ new int[0],
/* maximumSize= */ 0,
/* timestampsUs= */ new long[0],
/* flags= */ new int[0],
/* durationUs= */ C.TIME_UNSET);
} }
// Entries are byte offsets of chunks. // Entries are byte offsets of chunks.
...@@ -315,7 +326,8 @@ import java.util.List; ...@@ -315,7 +326,8 @@ import java.util.List;
// There is no edit list, or we are ignoring it as we already have gapless metadata to apply. // There is no edit list, or we are ignoring it as we already have gapless metadata to apply.
// This implementation does not support applying both gapless metadata and an edit list. // This implementation does not support applying both gapless metadata and an edit list.
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); return new TrackSampleTable(
track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
} }
// See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a
...@@ -342,7 +354,8 @@ import java.util.List; ...@@ -342,7 +354,8 @@ import java.util.List;
gaplessInfoHolder.encoderDelay = (int) encoderDelay; gaplessInfoHolder.encoderDelay = (int) encoderDelay;
gaplessInfoHolder.encoderPadding = (int) encoderPadding; gaplessInfoHolder.encoderPadding = (int) encoderPadding;
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); return new TrackSampleTable(
track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
} }
} }
} }
...@@ -359,7 +372,8 @@ import java.util.List; ...@@ -359,7 +372,8 @@ import java.util.List;
} }
durationUs = durationUs =
Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale); Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); return new TrackSampleTable(
track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
} }
// Omit any sample at the end point of an edit for audio tracks. // Omit any sample at the end point of an edit for audio tracks.
...@@ -409,6 +423,11 @@ import java.util.List; ...@@ -409,6 +423,11 @@ import java.util.List;
System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count); System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count);
System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count); System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count);
} }
if (startIndex < endIndex && (editedFlags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) == 0) {
// Applying the edit list would require prerolling from a sync sample.
Log.w(TAG, "Ignoring edit list: edit does not start with a sync sample.");
throw new UnhandledEditListException();
}
for (int j = startIndex; j < endIndex; j++) { for (int j = startIndex; j < endIndex; j++) {
long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
long timeInSegmentUs = long timeInSegmentUs =
...@@ -424,20 +443,8 @@ import java.util.List; ...@@ -424,20 +443,8 @@ import java.util.List;
pts += editDuration; pts += editDuration;
} }
long editedDurationUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.timescale); long editedDurationUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.timescale);
boolean hasSyncSample = false;
for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) {
hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0;
}
if (!hasSyncSample) {
// We don't support edit lists where the edited sample sequence doesn't contain a sync sample.
// Such edit lists are often (although not always) broken, so we ignore it and continue.
Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample.");
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs);
}
return new TrackSampleTable( return new TrackSampleTable(
track,
editedOffsets, editedOffsets,
editedSizes, editedSizes,
editedMaximumSize, editedMaximumSize,
......
...@@ -499,7 +499,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -499,7 +499,7 @@ public final class FragmentedMp4Extractor implements Extractor {
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
Track track = tracks.valueAt(i); Track track = tracks.valueAt(i);
TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type));
trackBundle.init(track, defaultSampleValuesArray.get(track.id)); trackBundle.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id));
trackBundles.put(track.id, trackBundle); trackBundles.put(track.id, trackBundle);
durationUs = Math.max(durationUs, track.durationUs); durationUs = Math.max(durationUs, track.durationUs);
} }
...@@ -509,11 +509,23 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -509,11 +509,23 @@ public final class FragmentedMp4Extractor implements Extractor {
Assertions.checkState(trackBundles.size() == trackCount); Assertions.checkState(trackBundles.size() == trackCount);
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
Track track = tracks.valueAt(i); Track track = tracks.valueAt(i);
trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id)); trackBundles
.get(track.id)
.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id));
} }
} }
} }
private DefaultSampleValues getDefaultSampleValues(
SparseArray<DefaultSampleValues> defaultSampleValuesArray, int trackId) {
if (defaultSampleValuesArray.size() == 1) {
// Ignore track id if there is only one track to cope with non-matching track indices.
// See https://github.com/google/ExoPlayer/issues/4477.
return defaultSampleValuesArray.valueAt(/* index= */ 0);
}
return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId));
}
private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException {
parseMoof(moof, trackBundles, flags, extendedTypeScratch); parseMoof(moof, trackBundles, flags, extendedTypeScratch);
// If drm init data is sideloaded, we ignore pssh boxes. // If drm init data is sideloaded, we ignore pssh boxes.
...@@ -642,7 +654,7 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -642,7 +654,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray, private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray,
@Flags int flags, byte[] extendedTypeScratch) throws ParserException { @Flags int flags, byte[] extendedTypeScratch) throws ParserException {
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray, flags); TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray);
if (trackBundle == null) { if (trackBundle == null) {
return; return;
} }
...@@ -793,13 +805,13 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -793,13 +805,13 @@ public final class FragmentedMp4Extractor implements Extractor {
* @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd
* does not refer to any {@link TrackBundle}. * does not refer to any {@link TrackBundle}.
*/ */
private static TrackBundle parseTfhd(ParsableByteArray tfhd, private static TrackBundle parseTfhd(
SparseArray<TrackBundle> trackBundles, int flags) { ParsableByteArray tfhd, SparseArray<TrackBundle> trackBundles) {
tfhd.setPosition(Atom.HEADER_SIZE); tfhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = tfhd.readInt(); int fullAtom = tfhd.readInt();
int atomFlags = Atom.parseFullAtomFlags(fullAtom); int atomFlags = Atom.parseFullAtomFlags(fullAtom);
int trackId = tfhd.readInt(); int trackId = tfhd.readInt();
TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0); TrackBundle trackBundle = getTrackBundle(trackBundles, trackId);
if (trackBundle == null) { if (trackBundle == null) {
return null; return null;
} }
...@@ -824,6 +836,17 @@ public final class FragmentedMp4Extractor implements Extractor { ...@@ -824,6 +836,17 @@ public final class FragmentedMp4Extractor implements Extractor {
return trackBundle; return trackBundle;
} }
private static @Nullable TrackBundle getTrackBundle(
SparseArray<TrackBundle> trackBundles, int trackId) {
if (trackBundles.size() == 1) {
// Ignore track id if there is only one track. This is either because we have a side-loaded
// track (flag FLAG_SIDELOADED) or to cope with non-matching track indices (see
// https://github.com/google/ExoPlayer/issues/4083).
return trackBundles.valueAt(/* index= */ 0);
}
return trackBundles.get(trackId);
}
/** /**
* Parses a tfdt atom (defined in 14496-12). * Parses a tfdt atom (defined in 14496-12).
* *
......
...@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.metadata.Metadata; ...@@ -20,6 +20,7 @@ import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.metadata.id3.InternalFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -293,14 +294,13 @@ import com.google.android.exoplayer2.util.Util; ...@@ -293,14 +294,13 @@ import com.google.android.exoplayer2.util.Util;
data.skipBytes(atomSize - 12); data.skipBytes(atomSize - 12);
} }
} }
if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) { if (domain == null || name == null || dataAtomPosition == -1) {
// We're only interested in iTunSMPB.
return null; return null;
} }
data.setPosition(dataAtomPosition); data.setPosition(dataAtomPosition);
data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4) data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4)
String value = data.readNullTerminatedString(dataAtomSize - 16); String value = data.readNullTerminatedString(dataAtomSize - 16);
return new CommentFrame(LANGUAGE_UNDEFINED, name, value); return new InternalFrame(domain, name, value);
} }
private static int parseUint8AttributeValue(ParsableByteArray data) { private static int parseUint8AttributeValue(ParsableByteArray data) {
......
...@@ -391,25 +391,21 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -391,25 +391,21 @@ public final class Mp4Extractor implements Extractor, SeekMap {
} }
} }
for (int i = 0; i < moov.containerChildren.size(); i++) { boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
Atom.ContainerAtom atom = moov.containerChildren.get(i); ArrayList<TrackSampleTable> trackSampleTables;
if (atom.type != Atom.TYPE_trak) { try {
continue; trackSampleTables = getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists);
} } catch (AtomParsers.UnhandledEditListException e) {
// Discard gapless info as we aren't able to handle corresponding edits.
Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), gaplessInfoHolder = new GaplessInfoHolder();
C.TIME_UNSET, null, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, isQuickTime); trackSampleTables =
if (track == null) { getTrackSampleTables(moov, gaplessInfoHolder, /* ignoreEditLists= */ true);
continue; }
}
Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia)
.getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl);
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
if (trackSampleTable.sampleCount == 0) {
continue;
}
int trackCount = trackSampleTables.size();
for (int i = 0; i < trackCount; i++) {
TrackSampleTable trackSampleTable = trackSampleTables.get(i);
Track track = trackSampleTable.track;
Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, Mp4Track mp4Track = new Mp4Track(track, trackSampleTable,
extractorOutput.track(i, track.type)); extractorOutput.track(i, track.type));
// Each sample has up to three bytes of overhead for the start code that replaces its length. // Each sample has up to three bytes of overhead for the start code that replaces its length.
...@@ -445,6 +441,39 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -445,6 +441,39 @@ public final class Mp4Extractor implements Extractor, SeekMap {
extractorOutput.seekMap(this); extractorOutput.seekMap(this);
} }
private ArrayList<TrackSampleTable> getTrackSampleTables(
ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists)
throws ParserException {
ArrayList<TrackSampleTable> trackSampleTables = new ArrayList<>();
for (int i = 0; i < moov.containerChildren.size(); i++) {
Atom.ContainerAtom atom = moov.containerChildren.get(i);
if (atom.type != Atom.TYPE_trak) {
continue;
}
Track track =
AtomParsers.parseTrak(
atom,
moov.getLeafAtomOfType(Atom.TYPE_mvhd),
/* duration= */ C.TIME_UNSET,
/* drmInitData= */ null,
ignoreEditLists,
isQuickTime);
if (track == null) {
continue;
}
Atom.ContainerAtom stblAtom =
atom.getContainerAtomOfType(Atom.TYPE_mdia)
.getContainerAtomOfType(Atom.TYPE_minf)
.getContainerAtomOfType(Atom.TYPE_stbl);
TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder);
if (trackSampleTable.sampleCount == 0) {
continue;
}
trackSampleTables.add(trackSampleTable);
}
return trackSampleTables;
}
/** /**
* Attempts to extract the next sample in the current mdat atom for the specified track. * Attempts to extract the next sample in the current mdat atom for the specified track.
* <p> * <p>
......
...@@ -24,29 +24,19 @@ import com.google.android.exoplayer2.util.Util; ...@@ -24,29 +24,19 @@ import com.google.android.exoplayer2.util.Util;
*/ */
/* package */ final class TrackSampleTable { /* package */ final class TrackSampleTable {
/** /** The track corresponding to this sample table. */
* Number of samples. public final Track track;
*/ /** Number of samples. */
public final int sampleCount; public final int sampleCount;
/** /** Sample offsets in bytes. */
* Sample offsets in bytes.
*/
public final long[] offsets; public final long[] offsets;
/** /** Sample sizes in bytes. */
* Sample sizes in bytes.
*/
public final int[] sizes; public final int[] sizes;
/** /** Maximum sample size in {@link #sizes}. */
* Maximum sample size in {@link #sizes}.
*/
public final int maximumSize; public final int maximumSize;
/** /** Sample timestamps in microseconds. */
* Sample timestamps in microseconds.
*/
public final long[] timestampsUs; public final long[] timestampsUs;
/** /** Sample flags. */
* Sample flags.
*/
public final int[] flags; public final int[] flags;
/** /**
* The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample * The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample
...@@ -55,6 +45,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -55,6 +45,7 @@ import com.google.android.exoplayer2.util.Util;
public final long durationUs; public final long durationUs;
public TrackSampleTable( public TrackSampleTable(
Track track,
long[] offsets, long[] offsets,
int[] sizes, int[] sizes,
int maximumSize, int maximumSize,
...@@ -65,6 +56,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -65,6 +56,7 @@ import com.google.android.exoplayer2.util.Util;
Assertions.checkArgument(offsets.length == timestampsUs.length); Assertions.checkArgument(offsets.length == timestampsUs.length);
Assertions.checkArgument(flags.length == timestampsUs.length); Assertions.checkArgument(flags.length == timestampsUs.length);
this.track = track;
this.offsets = offsets; this.offsets = offsets;
this.sizes = sizes; this.sizes = sizes;
this.maximumSize = maximumSize; this.maximumSize = maximumSize;
......
...@@ -52,7 +52,12 @@ public final class PsExtractor implements Extractor { ...@@ -52,7 +52,12 @@ public final class PsExtractor implements Extractor {
private static final int PACKET_START_CODE_PREFIX = 0x000001; private static final int PACKET_START_CODE_PREFIX = 0x000001;
private static final int MPEG_PROGRAM_END_CODE = 0x000001B9; private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
private static final int MAX_STREAM_ID_PLUS_ONE = 0x100; private static final int MAX_STREAM_ID_PLUS_ONE = 0x100;
// Max search length for first audio and video track in input data.
private static final long MAX_SEARCH_LENGTH = 1024 * 1024; private static final long MAX_SEARCH_LENGTH = 1024 * 1024;
// Max search length for additional audio and video tracks in input data after at least one audio
// and video track has been found.
private static final long MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND = 8 * 1024;
public static final int PRIVATE_STREAM_1 = 0xBD; public static final int PRIVATE_STREAM_1 = 0xBD;
public static final int AUDIO_STREAM = 0xC0; public static final int AUDIO_STREAM = 0xC0;
...@@ -66,6 +71,7 @@ public final class PsExtractor implements Extractor { ...@@ -66,6 +71,7 @@ public final class PsExtractor implements Extractor {
private boolean foundAllTracks; private boolean foundAllTracks;
private boolean foundAudioTrack; private boolean foundAudioTrack;
private boolean foundVideoTrack; private boolean foundVideoTrack;
private long lastTrackPosition;
// Accessed only by the loading thread. // Accessed only by the loading thread.
private ExtractorOutput output; private ExtractorOutput output;
...@@ -188,18 +194,21 @@ public final class PsExtractor implements Extractor { ...@@ -188,18 +194,21 @@ public final class PsExtractor implements Extractor {
if (!foundAllTracks) { if (!foundAllTracks) {
if (payloadReader == null) { if (payloadReader == null) {
ElementaryStreamReader elementaryStreamReader = null; ElementaryStreamReader elementaryStreamReader = null;
if (!foundAudioTrack && streamId == PRIVATE_STREAM_1) { if (streamId == PRIVATE_STREAM_1) {
// Private stream, used for AC3 audio. // Private stream, used for AC3 audio.
// NOTE: This may need further parsing to determine if its DTS, but that's likely only // NOTE: This may need further parsing to determine if its DTS, but that's likely only
// valid for DVDs. // valid for DVDs.
elementaryStreamReader = new Ac3Reader(); elementaryStreamReader = new Ac3Reader();
foundAudioTrack = true; foundAudioTrack = true;
} else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) { lastTrackPosition = input.getPosition();
} else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
elementaryStreamReader = new MpegAudioReader(); elementaryStreamReader = new MpegAudioReader();
foundAudioTrack = true; foundAudioTrack = true;
} else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) { lastTrackPosition = input.getPosition();
} else if ((streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
elementaryStreamReader = new H262Reader(); elementaryStreamReader = new H262Reader();
foundVideoTrack = true; foundVideoTrack = true;
lastTrackPosition = input.getPosition();
} }
if (elementaryStreamReader != null) { if (elementaryStreamReader != null) {
TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE); TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE);
...@@ -208,7 +217,11 @@ public final class PsExtractor implements Extractor { ...@@ -208,7 +217,11 @@ public final class PsExtractor implements Extractor {
psPayloadReaders.put(streamId, payloadReader); psPayloadReaders.put(streamId, payloadReader);
} }
} }
if ((foundAudioTrack && foundVideoTrack) || input.getPosition() > MAX_SEARCH_LENGTH) { long maxSearchPosition =
foundAudioTrack && foundVideoTrack
? lastTrackPosition + MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND
: MAX_SEARCH_LENGTH;
if (input.getPosition() > maxSearchPosition) {
foundAllTracks = true; foundAllTracks = true;
output.endTracks(); output.endTracks();
} }
......
...@@ -369,6 +369,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -369,6 +369,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto(); wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType); drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
} }
if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
} else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
// Wait for keys.
return;
}
}
} }
if (codecInfo == null) { if (codecInfo == null) {
...@@ -405,7 +414,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -405,7 +414,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName); codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName);
codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format);
codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecInfo);
codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);
codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName);
codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format); codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format);
...@@ -1210,6 +1219,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1210,6 +1219,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
/** /**
* Returns whether the device needs keys to have been loaded into the {@link DrmSession} before
* codec configuration.
*/
private boolean deviceNeedsDrmKeysToConfigureCodecWorkaround() {
return "Amazon".equals(Util.MANUFACTURER)
&& ("AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1
|| "AFTB".equals(Util.MODEL)); // Fire TV Gen 1
}
/**
* Returns whether the decoder is known to fail when flushed. * Returns whether the decoder is known to fail when flushed.
* <p> * <p>
* If true is returned, the renderer will work around the issue by releasing the decoder and * If true is returned, the renderer will work around the issue by releasing the decoder and
...@@ -1272,20 +1291,23 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1272,20 +1291,23 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
/** /**
* Returns whether the decoder is known to handle the propagation of the * Returns whether the decoder is known to handle the propagation of the {@link
* {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device. * MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device.
* <p> *
* If true is returned, the renderer will work around the issue by approximating end of stream * <p>If true is returned, the renderer will work around the issue by approximating end of stream
* behavior without relying on the flag being propagated through to an output buffer by the * behavior without relying on the flag being propagated through to an output buffer by the
* underlying decoder. * underlying decoder.
* *
* @param name The name of the decoder. * @param codecInfo Information about the {@link MediaCodec}.
* @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} * @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM}
* propagation incorrectly on the host device. False otherwise. * propagation incorrectly on the host device. False otherwise.
*/ */
private static boolean codecNeedsEosPropagationWorkaround(String name) { private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) {
return Util.SDK_INT <= 17 && ("OMX.rk.video_decoder.avc".equals(name) String name = codecInfo.name;
|| "OMX.allwinner.video.decoder.avc".equals(name)); return (Util.SDK_INT <= 17
&& ("OMX.rk.video_decoder.avc".equals(name)
|| "OMX.allwinner.video.decoder.avc".equals(name)))
|| ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure);
} }
/** /**
......
/*
* Copyright (C) 2018 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.id3;
import android.os.Parcel;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
/** Internal ID3 frame that is intended for use by the player. */
public final class InternalFrame extends Id3Frame {
public static final String ID = "----";
public final String domain;
public final String description;
public final String text;
public InternalFrame(String domain, String description, String text) {
super(ID);
this.domain = domain;
this.description = description;
this.text = text;
}
/* package */ InternalFrame(Parcel in) {
super(ID);
domain = Assertions.checkNotNull(in.readString());
description = Assertions.checkNotNull(in.readString());
text = Assertions.checkNotNull(in.readString());
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
InternalFrame other = (InternalFrame) obj;
return Util.areEqual(description, other.description)
&& Util.areEqual(domain, other.domain)
&& Util.areEqual(text, other.text);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (domain != null ? domain.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (text != null ? text.hashCode() : 0);
return result;
}
@Override
public String toString() {
return id + ": domain=" + domain + ", description=" + description;
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(domain);
dest.writeString(text);
}
public static final Creator<InternalFrame> CREATOR =
new Creator<InternalFrame>() {
@Override
public InternalFrame createFromParcel(Parcel in) {
return new InternalFrame(in);
}
@Override
public InternalFrame[] newArray(int size) {
return new InternalFrame[size];
}
};
}
...@@ -86,6 +86,7 @@ public abstract class DownloadService extends Service { ...@@ -86,6 +86,7 @@ public abstract class DownloadService extends Service {
private DownloadManagerListener downloadManagerListener; private DownloadManagerListener downloadManagerListener;
private int lastStartId; private int lastStartId;
private boolean startedInForeground; private boolean startedInForeground;
private boolean taskRemoved;
/** /**
* Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. * Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}.
...@@ -219,12 +220,17 @@ public abstract class DownloadService extends Service { ...@@ -219,12 +220,17 @@ public abstract class DownloadService extends Service {
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
lastStartId = startId; lastStartId = startId;
taskRemoved = false;
String intentAction = null; String intentAction = null;
if (intent != null) { if (intent != null) {
intentAction = intent.getAction(); intentAction = intent.getAction();
startedInForeground |= startedInForeground |=
intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction); intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction);
} }
// intentAction is null if the service is restarted or no action is specified.
if (intentAction == null) {
intentAction = ACTION_INIT;
}
logd("onStartCommand action: " + intentAction + " startId: " + startId); logd("onStartCommand action: " + intentAction + " startId: " + startId);
switch (intentAction) { switch (intentAction) {
case ACTION_INIT: case ACTION_INIT:
...@@ -261,6 +267,12 @@ public abstract class DownloadService extends Service { ...@@ -261,6 +267,12 @@ public abstract class DownloadService extends Service {
} }
@Override @Override
public void onTaskRemoved(Intent rootIntent) {
logd("onTaskRemoved rootIntent: " + rootIntent);
taskRemoved = true;
}
@Override
public void onDestroy() { public void onDestroy() {
logd("onDestroy"); logd("onDestroy");
foregroundNotificationUpdater.stopPeriodicUpdates(); foregroundNotificationUpdater.stopPeriodicUpdates();
...@@ -353,8 +365,13 @@ public abstract class DownloadService extends Service { ...@@ -353,8 +365,13 @@ public abstract class DownloadService extends Service {
if (startedInForeground && Util.SDK_INT >= 26) { if (startedInForeground && Util.SDK_INT >= 26) {
foregroundNotificationUpdater.showNotificationIfNotAlready(); foregroundNotificationUpdater.showNotificationIfNotAlready();
} }
boolean stopSelfResult = stopSelfResult(lastStartId); if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644].
logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult); stopSelf();
logd("stopSelf()");
} else {
boolean stopSelfResult = stopSelfResult(lastStartId);
logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult);
}
} }
private void logd(String message) { private void logd(String message) {
......
...@@ -344,6 +344,14 @@ public final class AdPlaybackState { ...@@ -344,6 +344,14 @@ public final class AdPlaybackState {
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
} }
/** Returns an instance with the specified ad marked as skipped. */
@CheckResult
public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) {
AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup);
return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
}
/** Returns an instance with the specified ad marked as having a load error. */ /** Returns an instance with the specified ad marked as having a load error. */
@CheckResult @CheckResult
public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) { public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) {
......
...@@ -30,7 +30,8 @@ public final class TimestampAdjuster { ...@@ -30,7 +30,8 @@ public final class TimestampAdjuster {
public static final long DO_NOT_OFFSET = Long.MAX_VALUE; public static final long DO_NOT_OFFSET = Long.MAX_VALUE;
/** /**
* The value one greater than the largest representable (33 bit) MPEG-2 TS presentation timestamp. * The value one greater than the largest representable (33 bit) MPEG-2 TS 90 kHz clock
* presentation timestamp.
*/ */
private static final long MAX_PTS_PLUS_ONE = 0x200000000L; private static final long MAX_PTS_PLUS_ONE = 0x200000000L;
...@@ -38,13 +39,13 @@ public final class TimestampAdjuster { ...@@ -38,13 +39,13 @@ public final class TimestampAdjuster {
private long timestampOffsetUs; private long timestampOffsetUs;
// Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp. // Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp.
private volatile long lastSampleTimestamp; private volatile long lastSampleTimestampUs;
/** /**
* @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}. * @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}.
*/ */
public TimestampAdjuster(long firstSampleTimestampUs) { public TimestampAdjuster(long firstSampleTimestampUs) {
lastSampleTimestamp = C.TIME_UNSET; lastSampleTimestampUs = C.TIME_UNSET;
setFirstSampleTimestampUs(firstSampleTimestampUs); setFirstSampleTimestampUs(firstSampleTimestampUs);
} }
...@@ -56,30 +57,24 @@ public final class TimestampAdjuster { ...@@ -56,30 +57,24 @@ public final class TimestampAdjuster {
* {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset. * {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset.
*/ */
public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) { public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) {
Assertions.checkState(lastSampleTimestamp == C.TIME_UNSET); Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET);
this.firstSampleTimestampUs = firstSampleTimestampUs; this.firstSampleTimestampUs = firstSampleTimestampUs;
} }
/** /** Returns the last value passed to {@link #setFirstSampleTimestampUs(long)}. */
* Returns the first adjusted sample timestamp in microseconds.
*
* @return The first adjusted sample timestamp in microseconds.
*/
public long getFirstSampleTimestampUs() { public long getFirstSampleTimestampUs() {
return firstSampleTimestampUs; return firstSampleTimestampUs;
} }
/** /**
* Returns the last adjusted timestamp. If no timestamp has been adjusted, returns * Returns the last value obtained from {@link #adjustSampleTimestamp}. If {@link
* {@code firstSampleTimestampUs} as provided to the constructor. If this value is * #adjustSampleTimestamp} has not been called, returns the result of calling {@link
* {@link #DO_NOT_OFFSET}, returns {@link C#TIME_UNSET}. * #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link
* * C#TIME_UNSET}.
* @return The last adjusted timestamp. If not present, {@code firstSampleTimestampUs} is
* returned unless equal to {@link #DO_NOT_OFFSET}, in which case {@link C#TIME_UNSET} is
* returned.
*/ */
public long getLastAdjustedTimestampUs() { public long getLastAdjustedTimestampUs() {
return lastSampleTimestamp != C.TIME_UNSET ? lastSampleTimestamp return lastSampleTimestampUs != C.TIME_UNSET
? (lastSampleTimestampUs + timestampOffsetUs)
: firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET; : firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
} }
...@@ -93,44 +88,47 @@ public final class TimestampAdjuster { ...@@ -93,44 +88,47 @@ public final class TimestampAdjuster {
* be offset. * be offset.
*/ */
public long getTimestampOffsetUs() { public long getTimestampOffsetUs() {
return firstSampleTimestampUs == DO_NOT_OFFSET ? 0 return firstSampleTimestampUs == DO_NOT_OFFSET
: lastSampleTimestamp == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs; ? 0
: lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
} }
/** /**
* Resets the instance to its initial state. * Resets the instance to its initial state.
*/ */
public void reset() { public void reset() {
lastSampleTimestamp = C.TIME_UNSET; lastSampleTimestampUs = C.TIME_UNSET;
} }
/** /**
* Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound. * Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound.
* *
* @param pts The MPEG-2 TS presentation timestamp. * @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp.
* @return The adjusted timestamp in microseconds. * @return The adjusted timestamp in microseconds.
*/ */
public long adjustTsTimestamp(long pts) { public long adjustTsTimestamp(long pts90Khz) {
if (pts == C.TIME_UNSET) { if (pts90Khz == C.TIME_UNSET) {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
if (lastSampleTimestamp != C.TIME_UNSET) { if (lastSampleTimestampUs != C.TIME_UNSET) {
// The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1),
// and we need to snap to the one closest to lastSampleTimestamp. // and we need to snap to the one closest to lastSampleTimestampUs.
long lastPts = usToPts(lastSampleTimestamp); long lastPts = usToPts(lastSampleTimestampUs);
long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE; long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE;
long ptsWrapBelow = pts + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1)); long ptsWrapBelow = pts90Khz + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1));
long ptsWrapAbove = pts + (MAX_PTS_PLUS_ONE * closestWrapCount); long ptsWrapAbove = pts90Khz + (MAX_PTS_PLUS_ONE * closestWrapCount);
pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts) pts90Khz =
? ptsWrapBelow : ptsWrapAbove; Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts)
? ptsWrapBelow
: ptsWrapAbove;
} }
return adjustSampleTimestamp(ptsToUs(pts)); return adjustSampleTimestamp(ptsToUs(pts90Khz));
} }
/** /**
* Offsets a sample timestamp in microseconds. * Offsets a timestamp in microseconds.
* *
* @param timeUs The timestamp of a sample to adjust. * @param timeUs The timestamp to adjust in microseconds.
* @return The adjusted timestamp in microseconds. * @return The adjusted timestamp in microseconds.
*/ */
public long adjustSampleTimestamp(long timeUs) { public long adjustSampleTimestamp(long timeUs) {
...@@ -138,15 +136,15 @@ public final class TimestampAdjuster { ...@@ -138,15 +136,15 @@ public final class TimestampAdjuster {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
// Record the adjusted PTS to adjust for wraparound next time. // Record the adjusted PTS to adjust for wraparound next time.
if (lastSampleTimestamp != C.TIME_UNSET) { if (lastSampleTimestampUs != C.TIME_UNSET) {
lastSampleTimestamp = timeUs; lastSampleTimestampUs = timeUs;
} else { } else {
if (firstSampleTimestampUs != DO_NOT_OFFSET) { if (firstSampleTimestampUs != DO_NOT_OFFSET) {
// Calculate the timestamp offset. // Calculate the timestamp offset.
timestampOffsetUs = firstSampleTimestampUs - timeUs; timestampOffsetUs = firstSampleTimestampUs - timeUs;
} }
synchronized (this) { synchronized (this) {
lastSampleTimestamp = timeUs; lastSampleTimestampUs = timeUs;
// Notify threads waiting for this adjuster to be initialized. // Notify threads waiting for this adjuster to be initialized.
notifyAll(); notifyAll();
} }
...@@ -160,15 +158,15 @@ public final class TimestampAdjuster { ...@@ -160,15 +158,15 @@ public final class TimestampAdjuster {
* @throws InterruptedException If the thread was interrupted. * @throws InterruptedException If the thread was interrupted.
*/ */
public synchronized void waitUntilInitialized() throws InterruptedException { public synchronized void waitUntilInitialized() throws InterruptedException {
while (lastSampleTimestamp == C.TIME_UNSET) { while (lastSampleTimestampUs == C.TIME_UNSET) {
wait(); wait();
} }
} }
/** /**
* Converts a value in MPEG-2 timestamp units to the corresponding value in microseconds. * Converts a 90 kHz clock timestamp to a timestamp in microseconds.
* *
* @param pts A value in MPEG-2 timestamp units. * @param pts A 90 kHz clock timestamp.
* @return The corresponding value in microseconds. * @return The corresponding value in microseconds.
*/ */
public static long ptsToUs(long pts) { public static long ptsToUs(long pts) {
...@@ -176,10 +174,10 @@ public final class TimestampAdjuster { ...@@ -176,10 +174,10 @@ public final class TimestampAdjuster {
} }
/** /**
* Converts a value in microseconds to the corresponding values in MPEG-2 timestamp units. * Converts a timestamp in microseconds to a 90 kHz clock timestamp.
* *
* @param us A value in microseconds. * @param us A value in microseconds.
* @return The corresponding value in MPEG-2 timestamp units. * @return The corresponding value as a 90 kHz clock timestamp.
*/ */
public static long usToPts(long us) { public static long usToPts(long us) {
return (us * 90000) / C.MICROS_PER_SECOND; return (us * 90000) / C.MICROS_PER_SECOND;
......
...@@ -56,8 +56,7 @@ public final class XmlPullParserUtil { ...@@ -56,8 +56,7 @@ public final class XmlPullParserUtil {
* @return Whether the current event is a start tag with the specified name. * @return Whether the current event is a start tag with the specified name.
* @throws XmlPullParserException If an error occurs querying the parser. * @throws XmlPullParserException If an error occurs querying the parser.
*/ */
public static boolean isStartTag(XmlPullParser xpp, String name) public static boolean isStartTag(XmlPullParser xpp, String name) throws XmlPullParserException {
throws XmlPullParserException {
return isStartTag(xpp) && xpp.getName().equals(name); return isStartTag(xpp) && xpp.getName().equals(name);
} }
...@@ -73,21 +72,58 @@ public final class XmlPullParserUtil { ...@@ -73,21 +72,58 @@ public final class XmlPullParserUtil {
} }
/** /**
* Returns whether the current event is a start tag with the specified name. If the current event
* has a raw name then its prefix is stripped before matching.
*
* @param xpp The {@link XmlPullParser} to query.
* @param name The specified name.
* @return Whether the current event is a start tag with the specified name.
* @throws XmlPullParserException If an error occurs querying the parser.
*/
public static boolean isStartTagIgnorePrefix(XmlPullParser xpp, String name)
throws XmlPullParserException {
return isStartTag(xpp) && stripPrefix(xpp.getName()).equals(name);
}
/**
* Returns the value of an attribute of the current start tag. * Returns the value of an attribute of the current start tag.
* *
* @param xpp The {@link XmlPullParser} to query. * @param xpp The {@link XmlPullParser} to query.
* @param attributeName The name of the attribute. * @param attributeName The name of the attribute.
* @return The value of the attribute, or null if the current event is not a start tag or if no * @return The value of the attribute, or null if the current event is not a start tag or if no
* no such attribute was found. * such attribute was found.
*/ */
public static String getAttributeValue(XmlPullParser xpp, String attributeName) { public static String getAttributeValue(XmlPullParser xpp, String attributeName) {
int attributeCount = xpp.getAttributeCount(); int attributeCount = xpp.getAttributeCount();
for (int i = 0; i < attributeCount; i++) { for (int i = 0; i < attributeCount; i++) {
if (attributeName.equals(xpp.getAttributeName(i))) { if (xpp.getAttributeName(i).equals(attributeName)) {
return xpp.getAttributeValue(i);
}
}
return null;
}
/**
* Returns the value of an attribute of the current start tag. Any raw attribute names in the
* current start tag have their prefixes stripped before matching.
*
* @param xpp The {@link XmlPullParser} to query.
* @param attributeName The name of the attribute.
* @return The value of the attribute, or null if the current event is not a start tag or if no
* such attribute was found.
*/
public static String getAttributeValueIgnorePrefix(XmlPullParser xpp, String attributeName) {
int attributeCount = xpp.getAttributeCount();
for (int i = 0; i < attributeCount; i++) {
if (stripPrefix(xpp.getAttributeName(i)).equals(attributeName)) {
return xpp.getAttributeValue(i); return xpp.getAttributeValue(i);
} }
} }
return null; return null;
} }
private static String stripPrefix(String name) {
int prefixSeparatorIndex = name.indexOf(':');
return prefixSeparatorIndex == -1 ? name : name.substring(prefixSeparatorIndex + 1);
}
} }
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import android.support.annotation.Nullable;
import android.view.Surface; import android.view.Surface;
import com.google.android.exoplayer2.Player.DefaultEventListener; import com.google.android.exoplayer2.Player.DefaultEventListener;
import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.Player.EventListener;
...@@ -1981,6 +1982,44 @@ public final class ExoPlayerTest { ...@@ -1981,6 +1982,44 @@ public final class ExoPlayerTest {
} }
@Test @Test
public void testRepeatedSeeksToUnpreparedPeriodInSameWindowKeepsWindowSequenceNumber()
throws Exception {
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 2,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND));
FakeMediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null);
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testSeekToUnpreparedPeriod")
.pause()
.waitForPlaybackState(Player.STATE_READY)
.seek(/* windowIndex= */ 0, /* positionMs= */ 9999)
.seek(/* windowIndex= */ 0, /* positionMs= */ 1)
.seek(/* windowIndex= */ 0, /* positionMs= */ 9999)
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlayedPeriodIndices(0, 1, 0, 1);
assertThat(mediaSource.getCreatedMediaPeriods())
.containsAllOf(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0),
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0));
assertThat(mediaSource.getCreatedMediaPeriods())
.doesNotContain(new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1));
}
@Test
public void testRecursivePlayerChangesReportConsistentValuesForAllListeners() throws Exception { public void testRecursivePlayerChangesReportConsistentValuesForAllListeners() throws Exception {
// We add two listeners to the player. The first stops the player as soon as it's ready and both // We add two listeners to the player. The first stops the player as soon as it's ready and both
// record the state change events they receive. // record the state change events they receive.
...@@ -2040,7 +2079,7 @@ public final class ExoPlayerTest { ...@@ -2040,7 +2079,7 @@ public final class ExoPlayerTest {
final EventListener eventListener = final EventListener eventListener =
new DefaultEventListener() { new DefaultEventListener() {
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
if (timeline.isEmpty()) { if (timeline.isEmpty()) {
playerReference.get().setPlayWhenReady(/* playWhenReady= */ false); playerReference.get().setPlayWhenReady(/* playWhenReady= */ false);
} }
......
...@@ -90,6 +90,19 @@ public final class AdPlaybackStateTest { ...@@ -90,6 +90,19 @@ public final class AdPlaybackStateTest {
} }
@Test @Test
public void testGetFirstAdIndexToPlaySkipsSkippedAd() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(1);
assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
assertThat(state.adGroups[0].states[2]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE);
}
@Test
public void testGetFirstAdIndexToPlaySkipsErrorAds() { public void testGetFirstAdIndexToPlaySkipsErrorAds() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3); state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI); state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);
......
...@@ -37,6 +37,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat ...@@ -37,6 +37,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat
import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SequenceableLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback;
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.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
...@@ -974,8 +975,25 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -974,8 +975,25 @@ public final class DashMediaSource extends BaseMediaSource {
long availableEndTimeUs = Long.MAX_VALUE; long availableEndTimeUs = Long.MAX_VALUE;
boolean isIndexExplicit = false; boolean isIndexExplicit = false;
boolean seenEmptyIndex = false; boolean seenEmptyIndex = false;
boolean haveAudioVideoAdaptationSets = false;
for (int i = 0; i < adaptationSetCount; i++) { for (int i = 0; i < adaptationSetCount; i++) {
DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex(); int type = period.adaptationSets.get(i).type;
if (type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO) {
haveAudioVideoAdaptationSets = true;
break;
}
}
for (int i = 0; i < adaptationSetCount; i++) {
AdaptationSet adaptationSet = period.adaptationSets.get(i);
// Exclude text adaptation sets from duration calculations, if we have at least one audio
// or video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029
if (haveAudioVideoAdaptationSets && adaptationSet.type == C.TRACK_TYPE_TEXT) {
continue;
}
DashSegmentIndex index = adaptationSet.representations.get(0).getIndex();
if (index == null) { if (index == null) {
return new PeriodSeekInfo(true, 0, durationUs); return new PeriodSeekInfo(true, 0, durationUs);
} }
......
...@@ -36,37 +36,53 @@ import java.io.IOException; ...@@ -36,37 +36,53 @@ import java.io.IOException;
private final EventMessageEncoder eventMessageEncoder; private final EventMessageEncoder eventMessageEncoder;
private long[] eventTimesUs; private long[] eventTimesUs;
private boolean eventStreamUpdatable; private boolean eventStreamAppendable;
private EventStream eventStream; private EventStream eventStream;
private boolean isFormatSentDownstream; private boolean isFormatSentDownstream;
private int currentIndex; private int currentIndex;
private long pendingSeekPositionUs; private long pendingSeekPositionUs;
EventSampleStream(EventStream eventStream, Format upstreamFormat, boolean eventStreamUpdatable) { public EventSampleStream(
EventStream eventStream, Format upstreamFormat, boolean eventStreamAppendable) {
this.upstreamFormat = upstreamFormat; this.upstreamFormat = upstreamFormat;
this.eventStream = eventStream; this.eventStream = eventStream;
eventMessageEncoder = new EventMessageEncoder(); eventMessageEncoder = new EventMessageEncoder();
pendingSeekPositionUs = C.TIME_UNSET; pendingSeekPositionUs = C.TIME_UNSET;
eventTimesUs = eventStream.presentationTimesUs; eventTimesUs = eventStream.presentationTimesUs;
updateEventStream(eventStream, eventStreamUpdatable); updateEventStream(eventStream, eventStreamAppendable);
} }
void updateEventStream(EventStream eventStream, boolean eventStreamUpdatable) { public String eventStreamId() {
return eventStream.id();
}
public void updateEventStream(EventStream eventStream, boolean eventStreamAppendable) {
long lastReadPositionUs = currentIndex == 0 ? C.TIME_UNSET : eventTimesUs[currentIndex - 1]; long lastReadPositionUs = currentIndex == 0 ? C.TIME_UNSET : eventTimesUs[currentIndex - 1];
this.eventStreamUpdatable = eventStreamUpdatable; this.eventStreamAppendable = eventStreamAppendable;
this.eventStream = eventStream; this.eventStream = eventStream;
this.eventTimesUs = eventStream.presentationTimesUs; this.eventTimesUs = eventStream.presentationTimesUs;
if (pendingSeekPositionUs != C.TIME_UNSET) { if (pendingSeekPositionUs != C.TIME_UNSET) {
seekToUs(pendingSeekPositionUs); seekToUs(pendingSeekPositionUs);
} else if (lastReadPositionUs != C.TIME_UNSET) { } else if (lastReadPositionUs != C.TIME_UNSET) {
currentIndex = Util.binarySearchCeil(eventTimesUs, lastReadPositionUs, false, false); currentIndex =
Util.binarySearchCeil(
eventTimesUs, lastReadPositionUs, /* inclusive= */ false, /* stayInBounds= */ false);
} }
} }
String eventStreamId() { /**
return eventStream.id(); * Seeks to the specified position in microseconds.
*
* @param positionUs The seek position in microseconds.
*/
public void seekToUs(long positionUs) {
currentIndex =
Util.binarySearchCeil(
eventTimesUs, positionUs, /* inclusive= */ true, /* stayInBounds= */ false);
boolean isPendingSeek = eventStreamAppendable && currentIndex == eventTimesUs.length;
pendingSeekPositionUs = isPendingSeek ? positionUs : C.TIME_UNSET;
} }
@Override @Override
...@@ -88,7 +104,7 @@ import java.io.IOException; ...@@ -88,7 +104,7 @@ import java.io.IOException;
return C.RESULT_FORMAT_READ; return C.RESULT_FORMAT_READ;
} }
if (currentIndex == eventTimesUs.length) { if (currentIndex == eventTimesUs.length) {
if (!eventStreamUpdatable) { if (!eventStreamAppendable) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ; return C.RESULT_BUFFER_READ;
} else { } else {
...@@ -118,15 +134,4 @@ import java.io.IOException; ...@@ -118,15 +134,4 @@ import java.io.IOException;
return skipped; return skipped;
} }
/**
* Seeks to the specified position in microseconds.
*
* @param positionUs The seek position in microseconds.
*/
public void seekToUs(long positionUs) {
currentIndex = Util.binarySearchCeil(eventTimesUs, positionUs, true, false);
boolean isPendingSeek = eventStreamUpdatable && currentIndex == eventTimesUs.length;
pendingSeekPositionUs = isPendingSeek ? positionUs : C.TIME_UNSET;
}
} }
...@@ -355,6 +355,7 @@ public class DashManifestParser extends DefaultHandler ...@@ -355,6 +355,7 @@ public class DashManifestParser extends DefaultHandler
protected Pair<String, SchemeData> parseContentProtection(XmlPullParser xpp) protected Pair<String, SchemeData> parseContentProtection(XmlPullParser xpp)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
String schemeType = null; String schemeType = null;
String licenseServerUrl = null;
byte[] data = null; byte[] data = null;
UUID uuid = null; UUID uuid = null;
boolean requiresSecureDecoder = false; boolean requiresSecureDecoder = false;
...@@ -364,7 +365,7 @@ public class DashManifestParser extends DefaultHandler ...@@ -364,7 +365,7 @@ public class DashManifestParser extends DefaultHandler
switch (Util.toLowerInvariant(schemeIdUri)) { switch (Util.toLowerInvariant(schemeIdUri)) {
case "urn:mpeg:dash:mp4protection:2011": case "urn:mpeg:dash:mp4protection:2011":
schemeType = xpp.getAttributeValue(null, "value"); schemeType = xpp.getAttributeValue(null, "value");
String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); String defaultKid = XmlPullParserUtil.getAttributeValueIgnorePrefix(xpp, "default_KID");
if (!TextUtils.isEmpty(defaultKid) if (!TextUtils.isEmpty(defaultKid)
&& !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) {
String[] defaultKidStrings = defaultKid.split("\\s+"); String[] defaultKidStrings = defaultKid.split("\\s+");
...@@ -389,11 +390,14 @@ public class DashManifestParser extends DefaultHandler ...@@ -389,11 +390,14 @@ public class DashManifestParser extends DefaultHandler
do { do {
xpp.next(); xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) {
licenseServerUrl = xpp.getAttributeValue(null, "licenseUrl");
} else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) {
String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); String robustnessLevel = xpp.getAttributeValue(null, "robustness_level");
requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW");
} else if (data == null) { } else if (data == null) {
if (XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { if (XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "pssh")
&& xpp.next() == XmlPullParser.TEXT) {
// The cenc:pssh element is defined in 23001-7:2015. // The cenc:pssh element is defined in 23001-7:2015.
data = Base64.decode(xpp.getText(), Base64.DEFAULT); data = Base64.decode(xpp.getText(), Base64.DEFAULT);
uuid = PsshAtomUtil.parseUuid(data); uuid = PsshAtomUtil.parseUuid(data);
...@@ -409,8 +413,11 @@ public class DashManifestParser extends DefaultHandler ...@@ -409,8 +413,11 @@ public class DashManifestParser extends DefaultHandler
} }
} }
} while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection"));
SchemeData schemeData = uuid != null SchemeData schemeData =
? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null; uuid != null
? new SchemeData(
uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder)
: null;
return Pair.create(schemeType, schemeData); return Pair.create(schemeType, schemeData);
} }
......
...@@ -401,7 +401,7 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -401,7 +401,7 @@ public final class HlsMediaSource extends BaseMediaSource
@Override @Override
public void releaseSourceInternal() { public void releaseSourceInternal() {
if (playlistTracker != null) { if (playlistTracker != null) {
playlistTracker.release(); playlistTracker.stop();
} }
} }
......
...@@ -136,6 +136,7 @@ import java.util.Arrays; ...@@ -136,6 +136,7 @@ import java.util.Arrays;
// Accessed only by the loading thread. // Accessed only by the loading thread.
private boolean tracksEnded; private boolean tracksEnded;
private long sampleOffsetUs; private long sampleOffsetUs;
private int chunkUid;
/** /**
* @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants.
...@@ -650,6 +651,7 @@ import java.util.Arrays; ...@@ -650,6 +651,7 @@ import java.util.Arrays;
audioSampleQueueMappingDone = false; audioSampleQueueMappingDone = false;
videoSampleQueueMappingDone = false; videoSampleQueueMappingDone = false;
} }
this.chunkUid = chunkUid;
for (SampleQueue sampleQueue : sampleQueues) { for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.sourceId(chunkUid); sampleQueue.sourceId(chunkUid);
} }
...@@ -704,6 +706,7 @@ import java.util.Arrays; ...@@ -704,6 +706,7 @@ import java.util.Arrays;
} }
} }
SampleQueue trackOutput = new SampleQueue(allocator); SampleQueue trackOutput = new SampleQueue(allocator);
trackOutput.sourceId(chunkUid);
trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.setSampleOffsetUs(sampleOffsetUs);
trackOutput.setUpstreamFormatChangeListener(this); trackOutput.setUpstreamFormatChangeListener(this);
sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
......
...@@ -105,7 +105,7 @@ public final class DefaultHlsPlaylistTracker ...@@ -105,7 +105,7 @@ public final class DefaultHlsPlaylistTracker
} }
@Override @Override
public void release() { public void stop() {
primaryHlsUrl = null; primaryHlsUrl = null;
primaryUrlSnapshot = null; primaryUrlSnapshot = null;
masterPlaylist = null; masterPlaylist = null;
......
...@@ -100,8 +100,8 @@ public interface HlsPlaylistTracker { ...@@ -100,8 +100,8 @@ public interface HlsPlaylistTracker {
/** /**
* Starts the playlist tracker. * Starts the playlist tracker.
* *
* <p>Must be called from the playback thread. A tracker may be restarted after a {@link * <p>Must be called from the playback thread. A tracker may be restarted after a {@link #stop()}
* #release()} call. * call.
* *
* @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master * @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master
* playlist. * playlist.
...@@ -111,8 +111,12 @@ public interface HlsPlaylistTracker { ...@@ -111,8 +111,12 @@ public interface HlsPlaylistTracker {
void start( void start(
Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener); Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener);
/** Releases all acquired resources. Must be called once per {@link #start} call. */ /**
void release(); * Stops the playlist tracker and releases any acquired resources.
*
* <p>Must be called once per {@link #start} call.
*/
void stop();
/** /**
* Registers a listener to receive events from the playlist tracker. * Registers a listener to receive events from the playlist tracker.
......
...@@ -22,6 +22,8 @@ import com.google.android.exoplayer2.C; ...@@ -22,6 +22,8 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import java.io.IOException; import java.io.IOException;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
...@@ -30,6 +32,7 @@ import org.robolectric.RobolectricTestRunner; ...@@ -30,6 +32,7 @@ import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class Aes128DataSourceTest { public class Aes128DataSourceTest {
@Ignore
@Test @Test
public void test_OpenCallsUpstreamOpen_CloseCallsUpstreamClose() throws IOException { public void test_OpenCallsUpstreamOpen_CloseCallsUpstreamClose() throws IOException {
UpstreamDataSource upstream = new UpstreamDataSource(); UpstreamDataSource upstream = new UpstreamDataSource();
...@@ -44,6 +47,7 @@ public class Aes128DataSourceTest { ...@@ -44,6 +47,7 @@ public class Aes128DataSourceTest {
assertThat(upstream.opened).isFalse(); assertThat(upstream.opened).isFalse();
} }
@Ignore
@Test @Test
public void test_OpenCallsUpstreamThrowingOpen_CloseCallsUpstreamClose() throws IOException { public void test_OpenCallsUpstreamThrowingOpen_CloseCallsUpstreamClose() throws IOException {
UpstreamDataSource upstream = UpstreamDataSource upstream =
......
...@@ -174,6 +174,12 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -174,6 +174,12 @@ public class DefaultTimeBar extends View implements TimeBar {
private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000; private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000;
private static final int DEFAULT_INCREMENT_COUNT = 20; private static final int DEFAULT_INCREMENT_COUNT = 20;
/**
* The name of the Android SDK view that most closely resembles this custom view. Used as the
* class name for accessibility.
*/
private static final String ACCESSIBILITY_CLASS_NAME = "android.widget.SeekBar";
private final Rect seekBounds; private final Rect seekBounds;
private final Rect progressBar; private final Rect progressBar;
private final Rect bufferedBar; private final Rect bufferedBar;
...@@ -184,7 +190,7 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -184,7 +190,7 @@ public class DefaultTimeBar extends View implements TimeBar {
private final Paint adMarkerPaint; private final Paint adMarkerPaint;
private final Paint playedAdMarkerPaint; private final Paint playedAdMarkerPaint;
private final Paint scrubberPaint; private final Paint scrubberPaint;
private final Drawable scrubberDrawable; private final @Nullable Drawable scrubberDrawable;
private final int barHeight; private final int barHeight;
private final int touchTargetHeight; private final int touchTargetHeight;
private final int adMarkerWidth; private final int adMarkerWidth;
...@@ -197,12 +203,12 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -197,12 +203,12 @@ public class DefaultTimeBar extends View implements TimeBar {
private final Formatter formatter; private final Formatter formatter;
private final Runnable stopScrubbingRunnable; private final Runnable stopScrubbingRunnable;
private final CopyOnWriteArraySet<OnScrubListener> listeners; private final CopyOnWriteArraySet<OnScrubListener> listeners;
private final int[] locationOnScreen;
private final Point touchPosition;
private int keyCountIncrement; private int keyCountIncrement;
private long keyTimeIncrement; private long keyTimeIncrement;
private int lastCoarseScrubXPosition; private int lastCoarseScrubXPosition;
private int[] locationOnScreen;
private Point touchPosition;
private boolean scrubbing; private boolean scrubbing;
private long scrubPosition; private long scrubPosition;
...@@ -210,12 +216,10 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -210,12 +216,10 @@ public class DefaultTimeBar extends View implements TimeBar {
private long position; private long position;
private long bufferedPosition; private long bufferedPosition;
private int adGroupCount; private int adGroupCount;
private long[] adGroupTimesMs; private @Nullable long[] adGroupTimesMs;
private boolean[] playedAdGroups; private @Nullable boolean[] playedAdGroups;
/** /** Creates a new time bar. */
* Creates a new time bar.
*/
public DefaultTimeBar(Context context, AttributeSet attrs) { public DefaultTimeBar(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
seekBounds = new Rect(); seekBounds = new Rect();
...@@ -230,6 +234,8 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -230,6 +234,8 @@ public class DefaultTimeBar extends View implements TimeBar {
scrubberPaint = new Paint(); scrubberPaint = new Paint();
scrubberPaint.setAntiAlias(true); scrubberPaint.setAntiAlias(true);
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
locationOnScreen = new int[2];
touchPosition = new Point();
// Calculate the dimensions and paints for drawn elements. // Calculate the dimensions and paints for drawn elements.
Resources res = context.getResources(); Resources res = context.getResources();
...@@ -593,14 +599,14 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -593,14 +599,14 @@ public class DefaultTimeBar extends View implements TimeBar {
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) { if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) {
event.getText().add(getProgressText()); event.getText().add(getProgressText());
} }
event.setClassName(DefaultTimeBar.class.getName()); event.setClassName(ACCESSIBILITY_CLASS_NAME);
} }
@TargetApi(21) @TargetApi(21)
@Override @Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info); super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(DefaultTimeBar.class.getCanonicalName()); info.setClassName(ACCESSIBILITY_CLASS_NAME);
info.setContentDescription(getProgressText()); info.setContentDescription(getProgressText());
if (duration <= 0) { if (duration <= 0) {
return; return;
...@@ -616,7 +622,7 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -616,7 +622,7 @@ public class DefaultTimeBar extends View implements TimeBar {
@TargetApi(16) @TargetApi(16)
@Override @Override
public boolean performAccessibilityAction(int action, Bundle args) { public boolean performAccessibilityAction(int action, @Nullable Bundle args) {
if (super.performAccessibilityAction(action, args)) { if (super.performAccessibilityAction(action, args)) {
return true; return true;
} }
...@@ -693,10 +699,6 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -693,10 +699,6 @@ public class DefaultTimeBar extends View implements TimeBar {
} }
private Point resolveRelativeTouchPosition(MotionEvent motionEvent) { private Point resolveRelativeTouchPosition(MotionEvent motionEvent) {
if (locationOnScreen == null) {
locationOnScreen = new int[2];
touchPosition = new Point();
}
getLocationOnScreen(locationOnScreen); getLocationOnScreen(locationOnScreen);
touchPosition.set( touchPosition.set(
((int) motionEvent.getRawX()) - locationOnScreen[0], ((int) motionEvent.getRawX()) - locationOnScreen[0],
...@@ -736,6 +738,11 @@ public class DefaultTimeBar extends View implements TimeBar { ...@@ -736,6 +738,11 @@ public class DefaultTimeBar extends View implements TimeBar {
if (scrubberBar.width() > 0) { if (scrubberBar.width() > 0) {
canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, playedPaint); canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, playedPaint);
} }
if (adGroupCount == 0) {
return;
}
long[] adGroupTimesMs = Assertions.checkNotNull(this.adGroupTimesMs);
boolean[] playedAdGroups = Assertions.checkNotNull(this.playedAdGroups);
int adMarkerOffset = adMarkerWidth / 2; int adMarkerOffset = adMarkerWidth / 2;
for (int i = 0; i < adGroupCount; i++) { for (int i = 0; i < adGroupCount; i++) {
long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration); long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration);
......
...@@ -55,10 +55,18 @@ public final class DownloadNotificationUtil { ...@@ -55,10 +55,18 @@ public final class DownloadNotificationUtil {
int downloadTaskCount = 0; int downloadTaskCount = 0;
boolean allDownloadPercentagesUnknown = true; boolean allDownloadPercentagesUnknown = true;
boolean haveDownloadedBytes = false; boolean haveDownloadedBytes = false;
boolean haveDownloadTasks = false;
boolean haveRemoveTasks = false;
for (TaskState taskState : taskStates) { for (TaskState taskState : taskStates) {
if (taskState.action.isRemoveAction || taskState.state != TaskState.STATE_STARTED) { if (taskState.state != TaskState.STATE_STARTED
&& taskState.state != TaskState.STATE_COMPLETED) {
continue; continue;
} }
if (taskState.action.isRemoveAction) {
haveRemoveTasks = true;
continue;
}
haveDownloadTasks = true;
if (taskState.downloadPercentage != C.PERCENTAGE_UNSET) { if (taskState.downloadPercentage != C.PERCENTAGE_UNSET) {
allDownloadPercentagesUnknown = false; allDownloadPercentagesUnknown = false;
totalPercentage += taskState.downloadPercentage; totalPercentage += taskState.downloadPercentage;
...@@ -67,18 +75,20 @@ public final class DownloadNotificationUtil { ...@@ -67,18 +75,20 @@ public final class DownloadNotificationUtil {
downloadTaskCount++; downloadTaskCount++;
} }
boolean haveDownloadTasks = downloadTaskCount > 0;
int titleStringId = int titleStringId =
haveDownloadTasks haveDownloadTasks
? R.string.exo_download_downloading ? R.string.exo_download_downloading
: (taskStates.length > 0 ? R.string.exo_download_removing : NULL_STRING_ID); : (haveRemoveTasks ? R.string.exo_download_removing : NULL_STRING_ID);
NotificationCompat.Builder notificationBuilder = NotificationCompat.Builder notificationBuilder =
newNotificationBuilder( newNotificationBuilder(
context, smallIcon, channelId, contentIntent, message, titleStringId); context, smallIcon, channelId, contentIntent, message, titleStringId);
int progress = haveDownloadTasks ? (int) (totalPercentage / downloadTaskCount) : 0; int progress = 0;
boolean indeterminate = boolean indeterminate = true;
!haveDownloadTasks || (allDownloadPercentagesUnknown && haveDownloadedBytes); if (haveDownloadTasks) {
progress = (int) (totalPercentage / downloadTaskCount);
indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes;
}
notificationBuilder.setProgress(/* max= */ 100, progress, indeterminate); notificationBuilder.setProgress(/* max= */ 100, progress, indeterminate);
notificationBuilder.setOngoing(true); notificationBuilder.setOngoing(true);
notificationBuilder.setShowWhen(false); notificationBuilder.setShowWhen(false);
......
...@@ -1088,7 +1088,7 @@ public class PlayerControlView extends FrameLayout { ...@@ -1088,7 +1088,7 @@ public class PlayerControlView extends FrameLayout {
@Override @Override
public void onTimelineChanged( public void onTimelineChanged(
Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
updateNavigation(); updateNavigation();
updateTimeBarMode(); updateTimeBarMode();
updateProgress(); updateProgress();
......
...@@ -949,7 +949,7 @@ public class PlayerNotificationManager { ...@@ -949,7 +949,7 @@ public class PlayerNotificationManager {
} }
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { if (player == null || player.getPlaybackState() == Player.STATE_IDLE) {
return; return;
} }
......
...@@ -696,6 +696,11 @@ public class PlayerView extends FrameLayout { ...@@ -696,6 +696,11 @@ public class PlayerView extends FrameLayout {
return useController && controller.dispatchMediaKeyEvent(event); return useController && controller.dispatchMediaKeyEvent(event);
} }
/** Returns whether the controller is currently visible. */
public boolean isControllerVisible() {
return controller != null && controller.isVisible();
}
/** /**
* Shows the playback controls. Does nothing if playback controls are disabled. * Shows the playback controls. Does nothing if playback controls are disabled.
* *
......
...@@ -28,6 +28,7 @@ import android.graphics.Rect; ...@@ -28,6 +28,7 @@ import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.text.Layout.Alignment; import android.text.Layout.Alignment;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout; import android.text.StaticLayout;
import android.text.TextPaint; import android.text.TextPaint;
import android.text.TextUtils; import android.text.TextUtils;
...@@ -89,7 +90,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -89,7 +90,8 @@ import com.google.android.exoplayer2.util.Util;
private int edgeColor; private int edgeColor;
@CaptionStyleCompat.EdgeType @CaptionStyleCompat.EdgeType
private int edgeType; private int edgeType;
private float textSizePx; private float defaultTextSizePx;
private float cueTextSizePx;
private float bottomPaddingFraction; private float bottomPaddingFraction;
private int parentLeft; private int parentLeft;
private int parentTop; private int parentTop;
...@@ -130,8 +132,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -130,8 +132,8 @@ import com.google.android.exoplayer2.util.Util;
/** /**
* Draws the provided {@link Cue} into a canvas with the specified styling. * Draws the provided {@link Cue} into a canvas with the specified styling.
* <p> *
* A call to this method is able to use cached results of calculations made during the previous * <p>A call to this method is able to use cached results of calculations made during the previous
* call, and so an instance of this class is able to optimize repeated calls to this method in * call, and so an instance of this class is able to optimize repeated calls to this method in
* which the same parameters are passed. * which the same parameters are passed.
* *
...@@ -140,7 +142,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -140,7 +142,8 @@ import com.google.android.exoplayer2.util.Util;
* @param applyEmbeddedFontSizes If {@code applyEmbeddedStyles} is true, defines whether font * @param applyEmbeddedFontSizes If {@code applyEmbeddedStyles} is true, defines whether font
* sizes embedded within the cue should be applied. Otherwise, it is ignored. * sizes embedded within the cue should be applied. Otherwise, it is ignored.
* @param style The style to use when drawing the cue text. * @param style The style to use when drawing the cue text.
* @param textSizePx The text size to use when drawing the cue text, in pixels. * @param defaultTextSizePx The default text size to use when drawing the text, in pixels.
* @param cueTextSizePx The embedded text size of this cue, in pixels.
* @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is * @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is
* {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height * {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height
* @param canvas The canvas into which to draw. * @param canvas The canvas into which to draw.
...@@ -149,9 +152,19 @@ import com.google.android.exoplayer2.util.Util; ...@@ -149,9 +152,19 @@ import com.google.android.exoplayer2.util.Util;
* @param cueBoxRight The right position of the enclosing cue box. * @param cueBoxRight The right position of the enclosing cue box.
* @param cueBoxBottom The bottom position of the enclosing cue box. * @param cueBoxBottom The bottom position of the enclosing cue box.
*/ */
public void draw(Cue cue, boolean applyEmbeddedStyles, boolean applyEmbeddedFontSizes, public void draw(
CaptionStyleCompat style, float textSizePx, float bottomPaddingFraction, Canvas canvas, Cue cue,
int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) { boolean applyEmbeddedStyles,
boolean applyEmbeddedFontSizes,
CaptionStyleCompat style,
float defaultTextSizePx,
float cueTextSizePx,
float bottomPaddingFraction,
Canvas canvas,
int cueBoxLeft,
int cueBoxTop,
int cueBoxRight,
int cueBoxBottom) {
boolean isTextCue = cue.bitmap == null; boolean isTextCue = cue.bitmap == null;
int windowColor = Color.BLACK; int windowColor = Color.BLACK;
if (isTextCue) { if (isTextCue) {
...@@ -180,7 +193,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -180,7 +193,8 @@ import com.google.android.exoplayer2.util.Util;
&& this.edgeType == style.edgeType && this.edgeType == style.edgeType
&& this.edgeColor == style.edgeColor && this.edgeColor == style.edgeColor
&& Util.areEqual(this.textPaint.getTypeface(), style.typeface) && Util.areEqual(this.textPaint.getTypeface(), style.typeface)
&& this.textSizePx == textSizePx && this.defaultTextSizePx == defaultTextSizePx
&& this.cueTextSizePx == cueTextSizePx
&& this.bottomPaddingFraction == bottomPaddingFraction && this.bottomPaddingFraction == bottomPaddingFraction
&& this.parentLeft == cueBoxLeft && this.parentLeft == cueBoxLeft
&& this.parentTop == cueBoxTop && this.parentTop == cueBoxTop
...@@ -209,7 +223,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -209,7 +223,8 @@ import com.google.android.exoplayer2.util.Util;
this.edgeType = style.edgeType; this.edgeType = style.edgeType;
this.edgeColor = style.edgeColor; this.edgeColor = style.edgeColor;
this.textPaint.setTypeface(style.typeface); this.textPaint.setTypeface(style.typeface);
this.textSizePx = textSizePx; this.defaultTextSizePx = defaultTextSizePx;
this.cueTextSizePx = cueTextSizePx;
this.bottomPaddingFraction = bottomPaddingFraction; this.bottomPaddingFraction = bottomPaddingFraction;
this.parentLeft = cueBoxLeft; this.parentLeft = cueBoxLeft;
this.parentTop = cueBoxTop; this.parentTop = cueBoxTop;
...@@ -228,8 +243,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -228,8 +243,8 @@ import com.google.android.exoplayer2.util.Util;
int parentWidth = parentRight - parentLeft; int parentWidth = parentRight - parentLeft;
int parentHeight = parentBottom - parentTop; int parentHeight = parentBottom - parentTop;
textPaint.setTextSize(textSizePx); textPaint.setTextSize(defaultTextSizePx);
int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + 0.5f); int textPaddingX = (int) (defaultTextSizePx * INNER_PADDING_RATIO + 0.5f);
int availableWidth = parentWidth - textPaddingX * 2; int availableWidth = parentWidth - textPaddingX * 2;
if (cueSize != Cue.DIMEN_UNSET) { if (cueSize != Cue.DIMEN_UNSET) {
...@@ -240,14 +255,12 @@ import com.google.android.exoplayer2.util.Util; ...@@ -240,14 +255,12 @@ import com.google.android.exoplayer2.util.Util;
return; return;
} }
CharSequence cueText = this.cueText;
// Remove embedded styling or font size if requested. // Remove embedded styling or font size if requested.
CharSequence cueText; if (!applyEmbeddedStyles) {
if (applyEmbeddedFontSizes && applyEmbeddedStyles) { cueText = cueText.toString(); // Equivalent to erasing all spans.
cueText = this.cueText; } else if (!applyEmbeddedFontSizes) {
} else if (!applyEmbeddedStyles) { SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText);
cueText = this.cueText.toString(); // Equivalent to erasing all spans.
} else {
SpannableStringBuilder newCueText = new SpannableStringBuilder(this.cueText);
int cueLength = newCueText.length(); int cueLength = newCueText.length();
AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class); AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class);
RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class); RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class);
...@@ -258,6 +271,19 @@ import com.google.android.exoplayer2.util.Util; ...@@ -258,6 +271,19 @@ import com.google.android.exoplayer2.util.Util;
newCueText.removeSpan(relSpan); newCueText.removeSpan(relSpan);
} }
cueText = newCueText; cueText = newCueText;
} else {
// Apply embedded styles & font size.
if (cueTextSizePx > 0) {
// Use a SpannableStringBuilder encompassing the whole cue text to apply the default
// cueTextSizePx.
SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText);
newCueText.setSpan(
new AbsoluteSizeSpan((int) cueTextSizePx),
/* start= */ 0,
/* end= */ newCueText.length(),
Spanned.SPAN_PRIORITY);
cueText = newCueText;
}
} }
Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment; Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment;
......
...@@ -269,15 +269,15 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -269,15 +269,15 @@ public final class SubtitleView extends View implements TextOutput {
for (int i = 0; i < cueCount; i++) { for (int i = 0; i < cueCount; i++) {
Cue cue = cues.get(i); Cue cue = cues.get(i);
float textSizePx = float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding);
resolveTextSizeForCue(cue, rawViewHeight, viewHeightMinusPadding, defaultViewTextSizePx);
SubtitlePainter painter = painters.get(i); SubtitlePainter painter = painters.get(i);
painter.draw( painter.draw(
cue, cue,
applyEmbeddedStyles, applyEmbeddedStyles,
applyEmbeddedFontSizes, applyEmbeddedFontSizes,
style, style,
textSizePx, defaultViewTextSizePx,
cueTextSizePx,
bottomPaddingFraction, bottomPaddingFraction,
canvas, canvas,
left, left,
...@@ -287,14 +287,13 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -287,14 +287,13 @@ public final class SubtitleView extends View implements TextOutput {
} }
} }
private float resolveTextSizeForCue( private float resolveCueTextSize(Cue cue, int rawViewHeight, int viewHeightMinusPadding) {
Cue cue, int rawViewHeight, int viewHeightMinusPadding, float defaultViewTextSizePx) {
if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) { if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) {
return defaultViewTextSizePx; return 0;
} }
float defaultCueTextSizePx = float defaultCueTextSizePx =
resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding); resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding);
return defaultCueTextSizePx > 0 ? defaultCueTextSizePx : defaultViewTextSizePx; return Math.max(defaultCueTextSizePx, 0);
} }
private float resolveTextSize( private float resolveTextSize(
......
...@@ -575,7 +575,9 @@ public abstract class Action { ...@@ -575,7 +575,9 @@ public abstract class Action {
new Player.DefaultEventListener() { new Player.DefaultEventListener() {
@Override @Override
public void onTimelineChanged( public void onTimelineChanged(
Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { Timeline timeline,
@Nullable Object manifest,
@Player.TimelineChangeReason int reason) {
if (expectedTimeline == null || timeline.equals(expectedTimeline)) { if (expectedTimeline == null || timeline.equals(expectedTimeline)) {
player.removeListener(this); player.removeListener(this);
nextAction.schedule(player, trackSelector, surface, handler); nextAction.schedule(player, trackSelector, surface, handler);
......
...@@ -601,8 +601,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener ...@@ -601,8 +601,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
// Player.EventListener // Player.EventListener
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest, public void onTimelineChanged(
@Player.TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
timelines.add(timeline); timelines.add(timeline);
manifests.add(manifest); manifests.add(manifest);
timelineChangeReasons.add(reason); timelineChangeReasons.add(reason);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment