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 #
### 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 ###
* IMA: Don't advertise support for video/mpeg ad media, as we don't have an
......
......@@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.8.2'
releaseVersionCode = 2802
releaseVersion = '2.8.3'
releaseVersionCode = 2803
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided
......
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.castdemo;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.view.KeyEvent;
import android.view.View;
import com.google.android.exoplayer2.C;
......@@ -282,7 +283,7 @@ import java.util.ArrayList;
@Override
public void onTimelineChanged(
Timeline timeline, Object manifest, @TimelineChangeReason int reason) {
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
updateCurrentItemIndex();
if (timeline.isEmpty()) {
castMediaQueueCreationPending = true;
......
......@@ -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.AdsRenderingSettings;
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.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
......@@ -62,6 +63,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
......@@ -267,13 +269,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
/** The expected ad group index that IMA should load next. */
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;
/**
* 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;
/** The current ad playback state. */
private @ImaAdState int imaAdState;
......@@ -285,9 +283,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Fields tracking the player/loader state.
/**
* Whether the player is playing an ad.
*/
/** Whether the player is playing an ad. */
private boolean playingAd;
/**
* 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
* content progress should increase. {@link C#TIME_UNSET} otherwise.
*/
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;
/**
* Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA.
*/
/** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */
private boolean sentPendingContentPositionMs;
/**
......@@ -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.
*
* <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
adsManager.destroy();
adsManager = null;
}
imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE;
pendingAdLoadError = null;
adPlaybackState = AdPlaybackState.NONE;
updateAdPlaybackState();
}
@Override
......@@ -558,7 +566,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(TAG, "onAdEvent: " + adEventType);
}
if (adsManager == null) {
Log.w(TAG, "Dropping ad event after release: " + adEvent);
Log.w(TAG, "Ignoring AdEvent after release: " + adEvent);
return;
}
try {
......@@ -654,6 +662,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void loadAd(String adUriString) {
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) {
Log.w(
TAG,
......@@ -662,9 +677,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adGroupIndex = expectedAdGroupIndex;
adsManager.start();
}
if (DEBUG) {
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
}
int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
if (adIndexInAdGroup == C.INDEX_UNSET) {
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
if (DEBUG) {
Log.d(TAG, "playAd");
}
if (adsManager == null) {
Log.w(TAG, "Ignoring playAd after release");
return;
}
switch (imaAdState) {
case IMA_AD_STATE_PLAYING:
// IMA does not always call stopAd before resuming content.
......@@ -736,6 +752,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (DEBUG) {
Log.d(TAG, "stopAd");
}
if (adsManager == null) {
Log.w(TAG, "Ignoring stopAd after release");
return;
}
if (player == null) {
// Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642].
Log.w(TAG, "Unexpected stopAd while detached");
......@@ -775,8 +795,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Player.EventListener implementation.
@Override
public void onTimelineChanged(Timeline timeline, Object manifest,
@Player.TimelineChangeReason int reason) {
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
if (reason == Player.TIMELINE_CHANGE_REASON_RESET) {
// The player is being reset and this source will be released.
return;
......@@ -1083,6 +1103,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(
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) {
// 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.
......@@ -1165,7 +1189,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.e(TAG, message, cause);
// We can't recover from an unexpected error in general, so skip all remaining ads.
if (adPlaybackState == null) {
adPlaybackState = new AdPlaybackState();
adPlaybackState = AdPlaybackState.NONE;
} else {
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
......
......@@ -281,8 +281,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest,
@TimelineChangeReason int reason) {
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
Callback callback = getCallback();
callback.onDurationChanged(LeanbackPlayerAdapter.this);
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
......
......@@ -674,8 +674,8 @@ public final class MediaSessionConnector {
private int currentWindowCount;
@Override
public void onTimelineChanged(Timeline timeline, Object manifest,
@Player.TimelineChangeReason int reason) {
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) {
......
......@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** 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.
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}. */
// 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.
......@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// 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}
......
......@@ -80,6 +80,13 @@ public final class Format implements Parcelable {
/** DRM initialization data if the stream is protected, or null otherwise. */
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.
/**
......@@ -141,15 +148,6 @@ public final class Format implements Parcelable {
*/
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.
/**
......
......@@ -228,11 +228,13 @@ import com.google.android.exoplayer2.util.Assertions;
reading = playing.next;
}
playing.release();
playing = playing.next;
length--;
if (length == 0) {
loading = null;
oldFrontPeriodUid = playing.uid;
oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber;
}
playing = playing.next;
} else {
playing = loading;
reading = loading;
......
......@@ -191,7 +191,8 @@ public interface Player {
* @param manifest The latest manifest. May be null.
* @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.
......@@ -281,8 +282,8 @@ public interface Player {
abstract class DefaultEventListener implements EventListener {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest,
@TimelineChangeReason int reason) {
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
// Call deprecated version. Otherwise, do nothing.
onTimelineChanged(timeline, manifest);
}
......@@ -337,7 +338,7 @@ public interface Player {
* instead.
*/
@Deprecated
public void onTimelineChanged(Timeline timeline, Object manifest) {
public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) {
// Do nothing.
}
......
......@@ -420,7 +420,7 @@ public class AnalyticsCollector
@Override
public final void onTimelineChanged(
Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
mediaPeriodQueueTracker.onTimelineChanged(timeline);
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
......
......@@ -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
* 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
* 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
* 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;
import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
......@@ -89,7 +88,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3;
private static final String TAG = "DefaultDrmSessionMgr";
private static final String CENC_SCHEME_MIME_TYPE = "cenc";
private final UUID uuid;
private final ExoMediaDrm<T> mediaDrm;
......@@ -509,17 +507,14 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
}
}
byte[] initData = null;
String mimeType = null;
SchemeData schemeData = null;
if (offlineLicenseKeySetId == null) {
SchemeData data = getSchemeData(drmInitData, uuid, false);
if (data == null) {
schemeData = getSchemeData(drmInitData, uuid, false);
if (schemeData == null) {
final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
eventDispatcher.drmSessionManagerError(error);
return new ErrorStateDrmSession<>(new DrmSessionException(error));
}
initData = getSchemeInitData(data, uuid);
mimeType = getSchemeMimeType(data, uuid);
}
DefaultDrmSession<T> session;
......@@ -528,6 +523,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
} else {
// Only use an existing session if it has matching init data.
session = null;
byte[] initData = schemeData != null ? schemeData.data : null;
for (DefaultDrmSession<T> existingSession : sessions) {
if (existingSession.hasInitData(initData)) {
session = existingSession;
......@@ -543,8 +539,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
uuid,
mediaDrm,
this,
initData,
mimeType,
schemeData,
mode,
offlineLicenseKeySetId,
optionalKeyRequestParameters,
......@@ -650,31 +645,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
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")
private class MediaDrmHandler extends Handler {
......
......@@ -266,9 +266,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* applies to all schemes).
*/
private final UUID uuid;
/**
* The mimeType of {@link #data}.
*/
/** The URL of the server to which license requests should be made. May be null if unknown. */
public final @Nullable String licenseServerUrl;
/** The mimeType of {@link #data}. */
public final String mimeType;
/**
* The initialization data. May be null for scheme support checks only.
......@@ -297,7 +297,25 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
* @param requiresSecureDecryption See {@link #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.licenseServerUrl = licenseServerUrl;
this.mimeType = Assertions.checkNotNull(mimeType);
this.data = data;
this.requiresSecureDecryption = requiresSecureDecryption;
......@@ -305,6 +323,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
/* package */ SchemeData(Parcel in) {
uuid = new UUID(in.readLong(), in.readLong());
licenseServerUrl = in.readString();
mimeType = in.readString();
data = in.createByteArray();
requiresSecureDecryption = in.readByte() != 0;
......@@ -346,7 +365,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
return true;
}
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);
}
......@@ -354,6 +375,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
public int hashCode() {
if (hashCode == 0) {
int result = uuid.hashCode();
result = 31 * result + (licenseServerUrl == null ? 0 : licenseServerUrl.hashCode());
result = 31 * result + mimeType.hashCode();
result = 31 * result + Arrays.hashCode(data);
hashCode = result;
......@@ -372,6 +394,7 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(uuid.getMostSignificantBits());
dest.writeLong(uuid.getLeastSignificantBits());
dest.writeString(licenseServerUrl);
dest.writeString(mimeType);
dest.writeByteArray(data);
dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0));
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.drm;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.DeniedByServerException;
import android.media.MediaCrypto;
......@@ -26,7 +27,9 @@ import android.media.UnsupportedSchemeException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
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.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.HashMap;
......@@ -40,6 +43,8 @@ import java.util.UUID;
@TargetApi(23)
public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> {
private static final String CENC_SCHEME_MIME_TYPE = "cenc";
private final UUID uuid;
private final MediaDrm mediaDrm;
......@@ -60,6 +65,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
}
}
@SuppressLint("WrongConstant")
private FrameworkMediaDrm(UUID uuid) throws UnsupportedSchemeException {
Assertions.checkNotNull(uuid);
Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead");
......@@ -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;
this.uuid = uuid;
this.mediaDrm = new MediaDrm(uuid);
if (C.WIDEVINE_UUID.equals(uuid) && needsForceL3Workaround()) {
mediaDrm.setPropertyString("securityLevel", "L3");
}
}
@Override
......@@ -116,14 +125,49 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
@Override
public KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType,
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,
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
public byte[] provideKeyResponse(byte[] scope, byte[] response)
throws NotProvisionedException, DeniedByServerException {
if (C.CLEARKEY_UUID.equals(uuid)) {
response = ClearKeyUtil.adjustResponseData(response);
}
return mediaDrm.provideKeyResponse(scope, response);
}
......@@ -183,4 +227,12 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
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;
import android.annotation.TargetApi;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
......@@ -114,8 +115,13 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
}
@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();
if (TextUtils.isEmpty(url)) {
url = mediaProvidedLicenseServerUrl;
}
if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
url = defaultLicenseUrl;
}
......
......@@ -15,6 +15,7 @@
*/
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.ProvisionRequest;
import com.google.android.exoplayer2.util.Assertions;
......@@ -44,7 +45,9 @@ public final class LocalMediaDrmCallback implements MediaDrmCallback {
}
@Override
public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception {
public byte[] executeKeyRequest(
UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl)
throws Exception {
return keyResponse;
}
......
......@@ -15,6 +15,7 @@
*/
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.ProvisionRequest;
import java.util.UUID;
......@@ -38,10 +39,13 @@ public interface MediaDrmCallback {
* Executes a key request.
*
* @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.
* @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;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
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.Pattern;
......@@ -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 =
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 {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof CommentFrame) {
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;
}
}
......@@ -103,14 +113,10 @@ public final class GaplessInfoHolder {
* 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.
*
* @param name The comment's identifier.
* @param data The comment's payload data.
* @return Whether the holder was populated.
*/
private boolean setFromComment(String name, String data) {
if (!GAPLESS_COMMENT_ID.equals(name)) {
return false;
}
private boolean setFromComment(String data) {
Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data);
if (matcher.find()) {
try {
......
......@@ -616,10 +616,10 @@ public final class MatroskaExtractor implements Extractor {
currentTrack.number = (int) value;
break;
case ID_FLAG_DEFAULT:
currentTrack.flagForced = value == 1;
currentTrack.flagDefault = value == 1;
break;
case ID_FLAG_FORCED:
currentTrack.flagDefault = value == 1;
currentTrack.flagForced = value == 1;
break;
case ID_TRACK_TYPE:
currentTrack.type = (int) value;
......
......@@ -43,6 +43,9 @@ import java.util.List;
*/
/* 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 int TYPE_vide = Util.getIntegerCodeForString("vide");
......@@ -117,10 +120,12 @@ import java.util.List;
* @param stblAtom stbl (sample table) atom to decode.
* @param gaplessInfoHolder Holder to populate with gapless playback information.
* @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,
GaplessInfoHolder gaplessInfoHolder) throws ParserException {
public static TrackSampleTable parseStbl(
Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder)
throws ParserException {
SampleSizeBox sampleSizeBox;
Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz);
if (stszAtom != null) {
......@@ -136,7 +141,13 @@ import java.util.List;
int sampleCount = sampleSizeBox.getSampleCount();
if (sampleCount == 0) {
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.
......@@ -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.
// This implementation does not support applying both gapless metadata and an edit list.
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
......@@ -342,7 +354,8 @@ import java.util.List;
gaplessInfoHolder.encoderDelay = (int) encoderDelay;
gaplessInfoHolder.encoderPadding = (int) encoderPadding;
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;
}
durationUs =
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.
......@@ -409,6 +423,11 @@ import java.util.List;
System.arraycopy(sizes, startIndex, editedSizes, 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++) {
long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
long timeInSegmentUs =
......@@ -424,20 +443,8 @@ import java.util.List;
pts += editDuration;
}
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(
track,
editedOffsets,
editedSizes,
editedMaximumSize,
......
......@@ -499,7 +499,7 @@ public final class FragmentedMp4Extractor implements Extractor {
for (int i = 0; i < trackCount; i++) {
Track track = tracks.valueAt(i);
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);
durationUs = Math.max(durationUs, track.durationUs);
}
......@@ -509,11 +509,23 @@ public final class FragmentedMp4Extractor implements Extractor {
Assertions.checkState(trackBundles.size() == trackCount);
for (int i = 0; i < trackCount; 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 {
parseMoof(moof, trackBundles, flags, extendedTypeScratch);
// If drm init data is sideloaded, we ignore pssh boxes.
......@@ -642,7 +654,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private static void parseTraf(ContainerAtom traf, SparseArray<TrackBundle> trackBundleArray,
@Flags int flags, byte[] extendedTypeScratch) throws ParserException {
LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray, flags);
TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray);
if (trackBundle == null) {
return;
}
......@@ -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
* does not refer to any {@link TrackBundle}.
*/
private static TrackBundle parseTfhd(ParsableByteArray tfhd,
SparseArray<TrackBundle> trackBundles, int flags) {
private static TrackBundle parseTfhd(
ParsableByteArray tfhd, SparseArray<TrackBundle> trackBundles) {
tfhd.setPosition(Atom.HEADER_SIZE);
int fullAtom = tfhd.readInt();
int atomFlags = Atom.parseFullAtomFlags(fullAtom);
int trackId = tfhd.readInt();
TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0);
TrackBundle trackBundle = getTrackBundle(trackBundles, trackId);
if (trackBundle == null) {
return null;
}
......@@ -824,6 +836,17 @@ public final class FragmentedMp4Extractor implements Extractor {
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).
*
......
......@@ -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.CommentFrame;
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.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
......@@ -293,14 +294,13 @@ import com.google.android.exoplayer2.util.Util;
data.skipBytes(atomSize - 12);
}
}
if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) {
// We're only interested in iTunSMPB.
if (domain == null || name == null || dataAtomPosition == -1) {
return null;
}
data.setPosition(dataAtomPosition);
data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4)
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) {
......
......@@ -391,25 +391,21 @@ public final class Mp4Extractor implements Extractor, SeekMap {
}
}
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),
C.TIME_UNSET, null, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, 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;
}
boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
ArrayList<TrackSampleTable> trackSampleTables;
try {
trackSampleTables = getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists);
} catch (AtomParsers.UnhandledEditListException e) {
// Discard gapless info as we aren't able to handle corresponding edits.
gaplessInfoHolder = new GaplessInfoHolder();
trackSampleTables =
getTrackSampleTables(moov, gaplessInfoHolder, /* ignoreEditLists= */ true);
}
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,
extractorOutput.track(i, track.type));
// 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 {
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.
* <p>
......
......@@ -24,29 +24,19 @@ import com.google.android.exoplayer2.util.Util;
*/
/* package */ final class TrackSampleTable {
/**
* Number of samples.
*/
/** The track corresponding to this sample table. */
public final Track track;
/** Number of samples. */
public final int sampleCount;
/**
* Sample offsets in bytes.
*/
/** Sample offsets in bytes. */
public final long[] offsets;
/**
* Sample sizes in bytes.
*/
/** Sample sizes in bytes. */
public final int[] sizes;
/**
* Maximum sample size in {@link #sizes}.
*/
/** Maximum sample size in {@link #sizes}. */
public final int maximumSize;
/**
* Sample timestamps in microseconds.
*/
/** Sample timestamps in microseconds. */
public final long[] timestampsUs;
/**
* Sample flags.
*/
/** Sample flags. */
public final int[] flags;
/**
* 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;
public final long durationUs;
public TrackSampleTable(
Track track,
long[] offsets,
int[] sizes,
int maximumSize,
......@@ -65,6 +56,7 @@ import com.google.android.exoplayer2.util.Util;
Assertions.checkArgument(offsets.length == timestampsUs.length);
Assertions.checkArgument(flags.length == timestampsUs.length);
this.track = track;
this.offsets = offsets;
this.sizes = sizes;
this.maximumSize = maximumSize;
......
......@@ -52,7 +52,12 @@ public final class PsExtractor implements Extractor {
private static final int PACKET_START_CODE_PREFIX = 0x000001;
private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
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;
// 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 AUDIO_STREAM = 0xC0;
......@@ -66,6 +71,7 @@ public final class PsExtractor implements Extractor {
private boolean foundAllTracks;
private boolean foundAudioTrack;
private boolean foundVideoTrack;
private long lastTrackPosition;
// Accessed only by the loading thread.
private ExtractorOutput output;
......@@ -188,18 +194,21 @@ public final class PsExtractor implements Extractor {
if (!foundAllTracks) {
if (payloadReader == null) {
ElementaryStreamReader elementaryStreamReader = null;
if (!foundAudioTrack && streamId == PRIVATE_STREAM_1) {
if (streamId == PRIVATE_STREAM_1) {
// Private stream, used for AC3 audio.
// NOTE: This may need further parsing to determine if its DTS, but that's likely only
// valid for DVDs.
elementaryStreamReader = new Ac3Reader();
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();
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();
foundVideoTrack = true;
lastTrackPosition = input.getPosition();
}
if (elementaryStreamReader != null) {
TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE);
......@@ -208,7 +217,11 @@ public final class PsExtractor implements Extractor {
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;
output.endTracks();
}
......
......@@ -369,6 +369,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
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) {
......@@ -405,7 +414,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName);
codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format);
codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName);
codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecInfo);
codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);
codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName);
codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format);
......@@ -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.
* <p>
* 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 {
}
/**
* Returns whether the decoder is known to handle the propagation of the
* {@link 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
* Returns whether the decoder is known to handle the propagation of the {@link
* 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
* behavior without relying on the flag being propagated through to an output buffer by the
* 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}
* propagation incorrectly on the host device. False otherwise.
*/
private static boolean codecNeedsEosPropagationWorkaround(String name) {
return Util.SDK_INT <= 17 && ("OMX.rk.video_decoder.avc".equals(name)
|| "OMX.allwinner.video.decoder.avc".equals(name));
private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) {
String name = codecInfo.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 {
private DownloadManagerListener downloadManagerListener;
private int lastStartId;
private boolean startedInForeground;
private boolean taskRemoved;
/**
* Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}.
......@@ -219,12 +220,17 @@ public abstract class DownloadService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
lastStartId = startId;
taskRemoved = false;
String intentAction = null;
if (intent != null) {
intentAction = intent.getAction();
startedInForeground |=
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);
switch (intentAction) {
case ACTION_INIT:
......@@ -261,6 +267,12 @@ public abstract class DownloadService extends Service {
}
@Override
public void onTaskRemoved(Intent rootIntent) {
logd("onTaskRemoved rootIntent: " + rootIntent);
taskRemoved = true;
}
@Override
public void onDestroy() {
logd("onDestroy");
foregroundNotificationUpdater.stopPeriodicUpdates();
......@@ -353,8 +365,13 @@ public abstract class DownloadService extends Service {
if (startedInForeground && Util.SDK_INT >= 26) {
foregroundNotificationUpdater.showNotificationIfNotAlready();
}
boolean stopSelfResult = stopSelfResult(lastStartId);
logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult);
if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644].
stopSelf();
logd("stopSelf()");
} else {
boolean stopSelfResult = stopSelfResult(lastStartId);
logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult);
}
}
private void logd(String message) {
......
......@@ -344,6 +344,14 @@ public final class AdPlaybackState {
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. */
@CheckResult
public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) {
......
......@@ -30,7 +30,8 @@ public final class TimestampAdjuster {
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;
......@@ -38,13 +39,13 @@ public final class TimestampAdjuster {
private long timestampOffsetUs;
// 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)}.
*/
public TimestampAdjuster(long firstSampleTimestampUs) {
lastSampleTimestamp = C.TIME_UNSET;
lastSampleTimestampUs = C.TIME_UNSET;
setFirstSampleTimestampUs(firstSampleTimestampUs);
}
......@@ -56,30 +57,24 @@ public final class TimestampAdjuster {
* {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset.
*/
public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) {
Assertions.checkState(lastSampleTimestamp == C.TIME_UNSET);
Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET);
this.firstSampleTimestampUs = firstSampleTimestampUs;
}
/**
* Returns the first adjusted sample timestamp in microseconds.
*
* @return The first adjusted sample timestamp in microseconds.
*/
/** Returns the last value passed to {@link #setFirstSampleTimestampUs(long)}. */
public long getFirstSampleTimestampUs() {
return firstSampleTimestampUs;
}
/**
* Returns the last adjusted timestamp. If no timestamp has been adjusted, returns
* {@code firstSampleTimestampUs} as provided to the constructor. 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.
* Returns the last value obtained from {@link #adjustSampleTimestamp}. If {@link
* #adjustSampleTimestamp} has not been called, returns the result of calling {@link
* #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link
* C#TIME_UNSET}.
*/
public long getLastAdjustedTimestampUs() {
return lastSampleTimestamp != C.TIME_UNSET ? lastSampleTimestamp
return lastSampleTimestampUs != C.TIME_UNSET
? (lastSampleTimestampUs + timestampOffsetUs)
: firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
}
......@@ -93,44 +88,47 @@ public final class TimestampAdjuster {
* be offset.
*/
public long getTimestampOffsetUs() {
return firstSampleTimestampUs == DO_NOT_OFFSET ? 0
: lastSampleTimestamp == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
return firstSampleTimestampUs == DO_NOT_OFFSET
? 0
: lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
}
/**
* Resets the instance to its initial state.
*/
public void reset() {
lastSampleTimestamp = C.TIME_UNSET;
lastSampleTimestampUs = C.TIME_UNSET;
}
/**
* 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.
*/
public long adjustTsTimestamp(long pts) {
if (pts == C.TIME_UNSET) {
public long adjustTsTimestamp(long pts90Khz) {
if (pts90Khz == 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),
// and we need to snap to the one closest to lastSampleTimestamp.
long lastPts = usToPts(lastSampleTimestamp);
// and we need to snap to the one closest to lastSampleTimestampUs.
long lastPts = usToPts(lastSampleTimestampUs);
long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE;
long ptsWrapBelow = pts + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1));
long ptsWrapAbove = pts + (MAX_PTS_PLUS_ONE * closestWrapCount);
pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts)
? ptsWrapBelow : ptsWrapAbove;
long ptsWrapBelow = pts90Khz + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1));
long ptsWrapAbove = pts90Khz + (MAX_PTS_PLUS_ONE * closestWrapCount);
pts90Khz =
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.
*/
public long adjustSampleTimestamp(long timeUs) {
......@@ -138,15 +136,15 @@ public final class TimestampAdjuster {
return C.TIME_UNSET;
}
// Record the adjusted PTS to adjust for wraparound next time.
if (lastSampleTimestamp != C.TIME_UNSET) {
lastSampleTimestamp = timeUs;
if (lastSampleTimestampUs != C.TIME_UNSET) {
lastSampleTimestampUs = timeUs;
} else {
if (firstSampleTimestampUs != DO_NOT_OFFSET) {
// Calculate the timestamp offset.
timestampOffsetUs = firstSampleTimestampUs - timeUs;
}
synchronized (this) {
lastSampleTimestamp = timeUs;
lastSampleTimestampUs = timeUs;
// Notify threads waiting for this adjuster to be initialized.
notifyAll();
}
......@@ -160,15 +158,15 @@ public final class TimestampAdjuster {
* @throws InterruptedException If the thread was interrupted.
*/
public synchronized void waitUntilInitialized() throws InterruptedException {
while (lastSampleTimestamp == C.TIME_UNSET) {
while (lastSampleTimestampUs == C.TIME_UNSET) {
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.
*/
public static long ptsToUs(long pts) {
......@@ -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.
* @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) {
return (us * 90000) / C.MICROS_PER_SECOND;
......
......@@ -56,8 +56,7 @@ public final class XmlPullParserUtil {
* @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 isStartTag(XmlPullParser xpp, String name)
throws XmlPullParserException {
public static boolean isStartTag(XmlPullParser xpp, String name) throws XmlPullParserException {
return isStartTag(xpp) && xpp.getName().equals(name);
}
......@@ -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.
*
* @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
* no such attribute was found.
* such attribute was found.
*/
public static String getAttributeValue(XmlPullParser xpp, String attributeName) {
int attributeCount = xpp.getAttributeCount();
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 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;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.support.annotation.Nullable;
import android.view.Surface;
import com.google.android.exoplayer2.Player.DefaultEventListener;
import com.google.android.exoplayer2.Player.EventListener;
......@@ -1981,6 +1982,44 @@ public final class ExoPlayerTest {
}
@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 {
// 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.
......@@ -2040,7 +2079,7 @@ public final class ExoPlayerTest {
final EventListener eventListener =
new DefaultEventListener() {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) {
if (timeline.isEmpty()) {
playerReference.get().setPlayWhenReady(/* playWhenReady= */ false);
}
......
......@@ -90,6 +90,19 @@ public final class AdPlaybackStateTest {
}
@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() {
state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI);
......
......@@ -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.ads.AdsMediaSource;
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.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
......@@ -974,8 +975,25 @@ public final class DashMediaSource extends BaseMediaSource {
long availableEndTimeUs = Long.MAX_VALUE;
boolean isIndexExplicit = false;
boolean seenEmptyIndex = false;
boolean haveAudioVideoAdaptationSets = false;
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) {
return new PeriodSeekInfo(true, 0, durationUs);
}
......
......@@ -36,37 +36,53 @@ import java.io.IOException;
private final EventMessageEncoder eventMessageEncoder;
private long[] eventTimesUs;
private boolean eventStreamUpdatable;
private boolean eventStreamAppendable;
private EventStream eventStream;
private boolean isFormatSentDownstream;
private int currentIndex;
private long pendingSeekPositionUs;
EventSampleStream(EventStream eventStream, Format upstreamFormat, boolean eventStreamUpdatable) {
public EventSampleStream(
EventStream eventStream, Format upstreamFormat, boolean eventStreamAppendable) {
this.upstreamFormat = upstreamFormat;
this.eventStream = eventStream;
eventMessageEncoder = new EventMessageEncoder();
pendingSeekPositionUs = C.TIME_UNSET;
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];
this.eventStreamUpdatable = eventStreamUpdatable;
this.eventStreamAppendable = eventStreamAppendable;
this.eventStream = eventStream;
this.eventTimesUs = eventStream.presentationTimesUs;
if (pendingSeekPositionUs != C.TIME_UNSET) {
seekToUs(pendingSeekPositionUs);
} 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
......@@ -88,7 +104,7 @@ import java.io.IOException;
return C.RESULT_FORMAT_READ;
}
if (currentIndex == eventTimesUs.length) {
if (!eventStreamUpdatable) {
if (!eventStreamAppendable) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
} else {
......@@ -118,15 +134,4 @@ import java.io.IOException;
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
protected Pair<String, SchemeData> parseContentProtection(XmlPullParser xpp)
throws XmlPullParserException, IOException {
String schemeType = null;
String licenseServerUrl = null;
byte[] data = null;
UUID uuid = null;
boolean requiresSecureDecoder = false;
......@@ -364,7 +365,7 @@ public class DashManifestParser extends DefaultHandler
switch (Util.toLowerInvariant(schemeIdUri)) {
case "urn:mpeg:dash:mp4protection:2011":
schemeType = xpp.getAttributeValue(null, "value");
String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID");
String defaultKid = XmlPullParserUtil.getAttributeValueIgnorePrefix(xpp, "default_KID");
if (!TextUtils.isEmpty(defaultKid)
&& !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) {
String[] defaultKidStrings = defaultKid.split("\\s+");
......@@ -389,11 +390,14 @@ public class DashManifestParser extends DefaultHandler
do {
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");
requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW");
} 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.
data = Base64.decode(xpp.getText(), Base64.DEFAULT);
uuid = PsshAtomUtil.parseUuid(data);
......@@ -409,8 +413,11 @@ public class DashManifestParser extends DefaultHandler
}
}
} while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection"));
SchemeData schemeData = uuid != null
? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null;
SchemeData schemeData =
uuid != null
? new SchemeData(
uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder)
: null;
return Pair.create(schemeType, schemeData);
}
......
......@@ -401,7 +401,7 @@ public final class HlsMediaSource extends BaseMediaSource
@Override
public void releaseSourceInternal() {
if (playlistTracker != null) {
playlistTracker.release();
playlistTracker.stop();
}
}
......
......@@ -136,6 +136,7 @@ import java.util.Arrays;
// Accessed only by the loading thread.
private boolean tracksEnded;
private long sampleOffsetUs;
private int chunkUid;
/**
* @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants.
......@@ -650,6 +651,7 @@ import java.util.Arrays;
audioSampleQueueMappingDone = false;
videoSampleQueueMappingDone = false;
}
this.chunkUid = chunkUid;
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.sourceId(chunkUid);
}
......@@ -704,6 +706,7 @@ import java.util.Arrays;
}
}
SampleQueue trackOutput = new SampleQueue(allocator);
trackOutput.sourceId(chunkUid);
trackOutput.setSampleOffsetUs(sampleOffsetUs);
trackOutput.setUpstreamFormatChangeListener(this);
sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1);
......
......@@ -105,7 +105,7 @@ public final class DefaultHlsPlaylistTracker
}
@Override
public void release() {
public void stop() {
primaryHlsUrl = null;
primaryUrlSnapshot = null;
masterPlaylist = null;
......
......@@ -100,8 +100,8 @@ public interface HlsPlaylistTracker {
/**
* Starts the playlist tracker.
*
* <p>Must be called from the playback thread. A tracker may be restarted after a {@link
* #release()} call.
* <p>Must be called from the playback thread. A tracker may be restarted after a {@link #stop()}
* call.
*
* @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master
* playlist.
......@@ -111,8 +111,12 @@ public interface HlsPlaylistTracker {
void start(
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.
......
......@@ -22,6 +22,8 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import java.io.IOException;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
......@@ -30,6 +32,7 @@ import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class Aes128DataSourceTest {
@Ignore
@Test
public void test_OpenCallsUpstreamOpen_CloseCallsUpstreamClose() throws IOException {
UpstreamDataSource upstream = new UpstreamDataSource();
......@@ -44,6 +47,7 @@ public class Aes128DataSourceTest {
assertThat(upstream.opened).isFalse();
}
@Ignore
@Test
public void test_OpenCallsUpstreamThrowingOpen_CloseCallsUpstreamClose() throws IOException {
UpstreamDataSource upstream =
......
......@@ -174,6 +174,12 @@ public class DefaultTimeBar extends View implements TimeBar {
private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000;
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 progressBar;
private final Rect bufferedBar;
......@@ -184,7 +190,7 @@ public class DefaultTimeBar extends View implements TimeBar {
private final Paint adMarkerPaint;
private final Paint playedAdMarkerPaint;
private final Paint scrubberPaint;
private final Drawable scrubberDrawable;
private final @Nullable Drawable scrubberDrawable;
private final int barHeight;
private final int touchTargetHeight;
private final int adMarkerWidth;
......@@ -197,12 +203,12 @@ public class DefaultTimeBar extends View implements TimeBar {
private final Formatter formatter;
private final Runnable stopScrubbingRunnable;
private final CopyOnWriteArraySet<OnScrubListener> listeners;
private final int[] locationOnScreen;
private final Point touchPosition;
private int keyCountIncrement;
private long keyTimeIncrement;
private int lastCoarseScrubXPosition;
private int[] locationOnScreen;
private Point touchPosition;
private boolean scrubbing;
private long scrubPosition;
......@@ -210,12 +216,10 @@ public class DefaultTimeBar extends View implements TimeBar {
private long position;
private long bufferedPosition;
private int adGroupCount;
private long[] adGroupTimesMs;
private boolean[] playedAdGroups;
private @Nullable long[] adGroupTimesMs;
private @Nullable boolean[] playedAdGroups;
/**
* Creates a new time bar.
*/
/** Creates a new time bar. */
public DefaultTimeBar(Context context, AttributeSet attrs) {
super(context, attrs);
seekBounds = new Rect();
......@@ -230,6 +234,8 @@ public class DefaultTimeBar extends View implements TimeBar {
scrubberPaint = new Paint();
scrubberPaint.setAntiAlias(true);
listeners = new CopyOnWriteArraySet<>();
locationOnScreen = new int[2];
touchPosition = new Point();
// Calculate the dimensions and paints for drawn elements.
Resources res = context.getResources();
......@@ -593,14 +599,14 @@ public class DefaultTimeBar extends View implements TimeBar {
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) {
event.getText().add(getProgressText());
}
event.setClassName(DefaultTimeBar.class.getName());
event.setClassName(ACCESSIBILITY_CLASS_NAME);
}
@TargetApi(21)
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(DefaultTimeBar.class.getCanonicalName());
info.setClassName(ACCESSIBILITY_CLASS_NAME);
info.setContentDescription(getProgressText());
if (duration <= 0) {
return;
......@@ -616,7 +622,7 @@ public class DefaultTimeBar extends View implements TimeBar {
@TargetApi(16)
@Override
public boolean performAccessibilityAction(int action, Bundle args) {
public boolean performAccessibilityAction(int action, @Nullable Bundle args) {
if (super.performAccessibilityAction(action, args)) {
return true;
}
......@@ -693,10 +699,6 @@ public class DefaultTimeBar extends View implements TimeBar {
}
private Point resolveRelativeTouchPosition(MotionEvent motionEvent) {
if (locationOnScreen == null) {
locationOnScreen = new int[2];
touchPosition = new Point();
}
getLocationOnScreen(locationOnScreen);
touchPosition.set(
((int) motionEvent.getRawX()) - locationOnScreen[0],
......@@ -736,6 +738,11 @@ public class DefaultTimeBar extends View implements TimeBar {
if (scrubberBar.width() > 0) {
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;
for (int i = 0; i < adGroupCount; i++) {
long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration);
......
......@@ -55,10 +55,18 @@ public final class DownloadNotificationUtil {
int downloadTaskCount = 0;
boolean allDownloadPercentagesUnknown = true;
boolean haveDownloadedBytes = false;
boolean haveDownloadTasks = false;
boolean haveRemoveTasks = false;
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;
}
if (taskState.action.isRemoveAction) {
haveRemoveTasks = true;
continue;
}
haveDownloadTasks = true;
if (taskState.downloadPercentage != C.PERCENTAGE_UNSET) {
allDownloadPercentagesUnknown = false;
totalPercentage += taskState.downloadPercentage;
......@@ -67,18 +75,20 @@ public final class DownloadNotificationUtil {
downloadTaskCount++;
}
boolean haveDownloadTasks = downloadTaskCount > 0;
int titleStringId =
haveDownloadTasks
? 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 =
newNotificationBuilder(
context, smallIcon, channelId, contentIntent, message, titleStringId);
int progress = haveDownloadTasks ? (int) (totalPercentage / downloadTaskCount) : 0;
boolean indeterminate =
!haveDownloadTasks || (allDownloadPercentagesUnknown && haveDownloadedBytes);
int progress = 0;
boolean indeterminate = true;
if (haveDownloadTasks) {
progress = (int) (totalPercentage / downloadTaskCount);
indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes;
}
notificationBuilder.setProgress(/* max= */ 100, progress, indeterminate);
notificationBuilder.setOngoing(true);
notificationBuilder.setShowWhen(false);
......
......@@ -1088,7 +1088,7 @@ public class PlayerControlView extends FrameLayout {
@Override
public void onTimelineChanged(
Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
updateNavigation();
updateTimeBarMode();
updateProgress();
......
......@@ -949,7 +949,7 @@ public class PlayerNotificationManager {
}
@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) {
return;
}
......
......@@ -696,6 +696,11 @@ public class PlayerView extends FrameLayout {
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.
*
......
......@@ -28,6 +28,7 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.text.Layout.Alignment;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
......@@ -89,7 +90,8 @@ import com.google.android.exoplayer2.util.Util;
private int edgeColor;
@CaptionStyleCompat.EdgeType
private int edgeType;
private float textSizePx;
private float defaultTextSizePx;
private float cueTextSizePx;
private float bottomPaddingFraction;
private int parentLeft;
private int parentTop;
......@@ -130,8 +132,8 @@ import com.google.android.exoplayer2.util.Util;
/**
* 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
* which the same parameters are passed.
*
......@@ -140,7 +142,8 @@ import com.google.android.exoplayer2.util.Util;
* @param applyEmbeddedFontSizes If {@code applyEmbeddedStyles} is true, defines whether font
* sizes embedded within the cue should be applied. Otherwise, it is ignored.
* @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
* {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height
* @param canvas The canvas into which to draw.
......@@ -149,9 +152,19 @@ import com.google.android.exoplayer2.util.Util;
* @param cueBoxRight The right 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,
CaptionStyleCompat style, float textSizePx, float bottomPaddingFraction, Canvas canvas,
int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) {
public void draw(
Cue cue,
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;
int windowColor = Color.BLACK;
if (isTextCue) {
......@@ -180,7 +193,8 @@ import com.google.android.exoplayer2.util.Util;
&& this.edgeType == style.edgeType
&& this.edgeColor == style.edgeColor
&& Util.areEqual(this.textPaint.getTypeface(), style.typeface)
&& this.textSizePx == textSizePx
&& this.defaultTextSizePx == defaultTextSizePx
&& this.cueTextSizePx == cueTextSizePx
&& this.bottomPaddingFraction == bottomPaddingFraction
&& this.parentLeft == cueBoxLeft
&& this.parentTop == cueBoxTop
......@@ -209,7 +223,8 @@ import com.google.android.exoplayer2.util.Util;
this.edgeType = style.edgeType;
this.edgeColor = style.edgeColor;
this.textPaint.setTypeface(style.typeface);
this.textSizePx = textSizePx;
this.defaultTextSizePx = defaultTextSizePx;
this.cueTextSizePx = cueTextSizePx;
this.bottomPaddingFraction = bottomPaddingFraction;
this.parentLeft = cueBoxLeft;
this.parentTop = cueBoxTop;
......@@ -228,8 +243,8 @@ import com.google.android.exoplayer2.util.Util;
int parentWidth = parentRight - parentLeft;
int parentHeight = parentBottom - parentTop;
textPaint.setTextSize(textSizePx);
int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + 0.5f);
textPaint.setTextSize(defaultTextSizePx);
int textPaddingX = (int) (defaultTextSizePx * INNER_PADDING_RATIO + 0.5f);
int availableWidth = parentWidth - textPaddingX * 2;
if (cueSize != Cue.DIMEN_UNSET) {
......@@ -240,14 +255,12 @@ import com.google.android.exoplayer2.util.Util;
return;
}
CharSequence cueText = this.cueText;
// Remove embedded styling or font size if requested.
CharSequence cueText;
if (applyEmbeddedFontSizes && applyEmbeddedStyles) {
cueText = this.cueText;
} else if (!applyEmbeddedStyles) {
cueText = this.cueText.toString(); // Equivalent to erasing all spans.
} else {
SpannableStringBuilder newCueText = new SpannableStringBuilder(this.cueText);
if (!applyEmbeddedStyles) {
cueText = cueText.toString(); // Equivalent to erasing all spans.
} else if (!applyEmbeddedFontSizes) {
SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText);
int cueLength = newCueText.length();
AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class);
RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class);
......@@ -258,6 +271,19 @@ import com.google.android.exoplayer2.util.Util;
newCueText.removeSpan(relSpan);
}
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;
......
......@@ -269,15 +269,15 @@ public final class SubtitleView extends View implements TextOutput {
for (int i = 0; i < cueCount; i++) {
Cue cue = cues.get(i);
float textSizePx =
resolveTextSizeForCue(cue, rawViewHeight, viewHeightMinusPadding, defaultViewTextSizePx);
float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding);
SubtitlePainter painter = painters.get(i);
painter.draw(
cue,
applyEmbeddedStyles,
applyEmbeddedFontSizes,
style,
textSizePx,
defaultViewTextSizePx,
cueTextSizePx,
bottomPaddingFraction,
canvas,
left,
......@@ -287,14 +287,13 @@ public final class SubtitleView extends View implements TextOutput {
}
}
private float resolveTextSizeForCue(
Cue cue, int rawViewHeight, int viewHeightMinusPadding, float defaultViewTextSizePx) {
private float resolveCueTextSize(Cue cue, int rawViewHeight, int viewHeightMinusPadding) {
if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) {
return defaultViewTextSizePx;
return 0;
}
float defaultCueTextSizePx =
resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding);
return defaultCueTextSizePx > 0 ? defaultCueTextSizePx : defaultViewTextSizePx;
return Math.max(defaultCueTextSizePx, 0);
}
private float resolveTextSize(
......
......@@ -575,7 +575,9 @@ public abstract class Action {
new Player.DefaultEventListener() {
@Override
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)) {
player.removeListener(this);
nextAction.schedule(player, trackSelector, surface, handler);
......
......@@ -601,8 +601,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
// Player.EventListener
@Override
public void onTimelineChanged(Timeline timeline, Object manifest,
@Player.TimelineChangeReason int reason) {
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
timelines.add(timeline);
manifests.add(manifest);
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