From 5f79aa253bdcaba23906ce724a4f7d3de2f5c97d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 7 Jun 2018 09:06:17 -0700 Subject: [PATCH 01/51] Add license server URL to SchemeData Allows DrmInitData to carry a license server URL when the media declares one. Issue:#3393 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199643743 --- RELEASENOTES.md | 6 + .../exoplayer2/drm/DefaultDrmSession.java | 104 +++++++++--------- .../drm/DefaultDrmSessionManager.java | 40 +------ .../android/exoplayer2/drm/DrmInitData.java | 31 +++++- .../exoplayer2/drm/FrameworkMediaDrm.java | 35 +++++- .../exoplayer2/drm/HttpMediaDrmCallback.java | 8 +- .../exoplayer2/drm/LocalMediaDrmCallback.java | 5 +- .../exoplayer2/drm/MediaDrmCallback.java | 10 +- 8 files changed, 141 insertions(+), 98 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2c3e1c78b0..46a1117202 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,11 @@ # Release notes # +### 2.8.3 ### + +* DRM: + * Allow DrmInitData to carry a license server URL + ([#3393](https://github.com/google/ExoPlayer/issues/3393)). + ### 2.8.2 ### * IMA: Don't advertise support for video/mpeg ad media, as we don't have an diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index c57b023139..fbbbcbc9ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -22,11 +22,12 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener.EventDispatcher; -import com.google.android.exoplayer2.drm.ExoMediaDrm.DefaultKeyRequest; +import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import java.util.Arrays; @@ -77,8 +78,7 @@ import java.util.UUID; private final ExoMediaDrm mediaDrm; private final ProvisioningManager provisioningManager; - private final byte[] initData; - private final String mimeType; + private final SchemeData schemeData; private final @DefaultDrmSessionManager.Mode int mode; private final HashMap optionalKeyRequestParameters; private final EventDispatcher eventDispatcher; @@ -103,9 +103,11 @@ import java.util.UUID; * @param uuid The UUID of the drm scheme. * @param mediaDrm The media DRM. * @param provisioningManager The manager for provisioning. - * @param initData The DRM init data. + * @param schemeData The DRM data for this session, or null if a {@code offlineLicenseKeySetId} is + * provided. * @param mode The DRM mode. - * @param offlineLicenseKeySetId The offlineLicense KeySetId. + * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using + * offline keys. * @param optionalKeyRequestParameters The optional key request parameters. * @param callback The media DRM callback. * @param playbackLooper The playback looper. @@ -117,10 +119,9 @@ import java.util.UUID; UUID uuid, ExoMediaDrm mediaDrm, ProvisioningManager provisioningManager, - byte[] initData, - String mimeType, + @Nullable SchemeData schemeData, @DefaultDrmSessionManager.Mode int mode, - byte[] offlineLicenseKeySetId, + @Nullable byte[] offlineLicenseKeySetId, HashMap optionalKeyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, @@ -131,6 +132,7 @@ import java.util.UUID; this.mediaDrm = mediaDrm; this.mode = mode; this.offlineLicenseKeySetId = offlineLicenseKeySetId; + this.schemeData = offlineLicenseKeySetId == null ? schemeData : null; this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.callback = callback; this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; @@ -141,14 +143,6 @@ import java.util.UUID; requestHandlerThread = new HandlerThread("DrmRequestHandler"); requestHandlerThread.start(); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - - if (offlineLicenseKeySetId == null) { - this.initData = initData; - this.mimeType = mimeType; - } else { - this.initData = null; - this.mimeType = null; - } } // Life cycle. @@ -187,13 +181,37 @@ import java.util.UUID; } public boolean hasInitData(byte[] initData) { - return Arrays.equals(this.initData, initData); + return Arrays.equals(schemeData != null ? schemeData.data : null, initData); } public boolean hasSessionId(byte[] sessionId) { return Arrays.equals(this.sessionId, sessionId); } + @SuppressWarnings("deprecation") + public void onMediaDrmEvent(int what) { + if (!isOpen()) { + return; + } + switch (what) { + case ExoMediaDrm.EVENT_KEY_REQUIRED: + doLicense(false); + break; + case ExoMediaDrm.EVENT_KEY_EXPIRED: + // When an already expired key is loaded MediaDrm sends this event immediately. Ignore + // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still + // waiting for key response. + onKeysExpired(); + break; + case ExoMediaDrm.EVENT_PROVISION_REQUIRED: + state = STATE_OPENED; + provisioningManager.provisionRequired(this); + break; + default: + break; + } + } + // Provisioning implementation. public void provision() { @@ -356,14 +374,19 @@ import java.util.UUID; private void postKeyRequest(int type, boolean allowRetry) { byte[] scope = type == ExoMediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; + byte[] initData = null; + String mimeType = null; + String licenseServerUrl = null; + if (schemeData != null) { + initData = schemeData.data; + mimeType = schemeData.mimeType; + licenseServerUrl = schemeData.licenseServerUrl; + } try { - KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, - optionalKeyRequestParameters); - if (C.CLEARKEY_UUID.equals(uuid)) { - request = new DefaultKeyRequest(ClearKeyUtil.adjustRequestData(request.getData()), - request.getDefaultUrl()); - } - postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget(); + KeyRequest request = + mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); + Pair arguments = Pair.create(request, licenseServerUrl); + postRequestHandler.obtainMessage(MSG_KEYS, arguments, allowRetry).sendToTarget(); } catch (Exception e) { onKeysError(e); } @@ -382,9 +405,6 @@ import java.util.UUID; try { byte[] responseData = (byte[]) response; - if (C.CLEARKEY_UUID.equals(uuid)) { - responseData = ClearKeyUtil.adjustResponseData(responseData); - } if (mode == DefaultDrmSessionManager.MODE_RELEASE) { mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData); eventDispatcher.drmKeysRemoved(); @@ -430,30 +450,7 @@ import java.util.UUID; return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; } - @SuppressWarnings("deprecation") - public void onMediaDrmEvent(int what) { - if (!isOpen()) { - return; - } - switch (what) { - case ExoMediaDrm.EVENT_KEY_REQUIRED: - doLicense(false); - break; - case ExoMediaDrm.EVENT_KEY_EXPIRED: - // When an already expired key is loaded MediaDrm sends this event immediately. Ignore - // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still - // waiting for key response. - onKeysExpired(); - break; - case ExoMediaDrm.EVENT_PROVISION_REQUIRED: - state = STATE_OPENED; - provisioningManager.provisionRequired(this); - break; - default: - break; - } - - } + // Internal classes. @SuppressLint("HandlerLeak") private class PostResponseHandler extends Handler { @@ -492,6 +489,7 @@ import java.util.UUID; } @Override + @SuppressWarnings("unchecked") public void handleMessage(Message msg) { Object response; try { @@ -500,7 +498,8 @@ import java.util.UUID; response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj); break; case MSG_KEYS: - response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj); + Pair arguments = (Pair) msg.obj; + response = callback.executeKeyRequest(uuid, arguments.first, arguments.second); break; default: throw new RuntimeException(); @@ -534,5 +533,4 @@ import java.util.UUID; } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 66c9e5cde7..28fd7e15ab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -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 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 mediaDrm; @@ -509,17 +507,14 @@ public class DefaultDrmSessionManager 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 session; @@ -528,6 +523,7 @@ public class DefaultDrmSessionManager 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 existingSession : sessions) { if (existingSession.hasInitData(initData)) { session = existingSession; @@ -543,8 +539,7 @@ public class DefaultDrmSessionManager implements DrmSe uuid, mediaDrm, this, - initData, - mimeType, + schemeData, mode, offlineLicenseKeySetId, optionalKeyRequestParameters, @@ -650,31 +645,6 @@ public class DefaultDrmSessionManager 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 { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index c2de662010..cd7adea1e2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -266,9 +266,9 @@ public final class DrmInitData implements Comparator, 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, 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, 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, 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, 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, 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)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index f960cd637f..9f30cef0f8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -26,7 +26,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 +42,8 @@ import java.util.UUID; @TargetApi(23) public final class FrameworkMediaDrm implements ExoMediaDrm { + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; + private final UUID uuid; private final MediaDrm mediaDrm; @@ -116,14 +120,43 @@ public final class FrameworkMediaDrm implements ExoMediaDrm optionalParameters) throws NotProvisionedException { + + // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. + if (Util.SDK_INT < 21 && C.WIDEVINE_UUID.equals(uuid)) { + byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(init, uuid); + if (psshData == null) { + // Extraction failed. schemeData isn't a Widevine 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); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index 9150a72b53..fc1e62a89c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -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; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java index 7b9aeca30a..7ed4a61a60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java @@ -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; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java index 617e168f9a..4405d6e538 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java @@ -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; } From 5992b310ad0e05232e9b584006b2c5bc862866f5 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 22 Jun 2018 06:50:28 -0700 Subject: [PATCH 02/51] Enable EOS workaround for FireTV Gen 2 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201678261 --- .../mediacodec/MediaCodecRenderer.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 03a0b66661..48002c7a86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -405,7 +405,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); @@ -1272,20 +1272,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. - *

- * 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. + * + *

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); } /** From 9b4981df8a6d304c766219b66e50ecce4859d584 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 22 Jun 2018 06:55:44 -0700 Subject: [PATCH 03/51] Add some FireOS workarounds for max input buffer size limitations ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201678686 --- .../video/MediaCodecVideoRenderer.java | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index fe50f26717..990025b5af 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -459,7 +459,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (areAdaptationCompatible(codecInfo.adaptive, oldFormat, newFormat) && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height - && getMaxInputSize(newFormat) <= codecMaxValues.inputSize) { + && getMaxInputSize(codecInfo, newFormat) <= codecMaxValues.inputSize) { return oldFormat.initializationDataEquals(newFormat) ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION : KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION; @@ -981,7 +981,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { throws DecoderQueryException { int maxWidth = format.width; int maxHeight = format.height; - int maxInputSize = getMaxInputSize(format); + int maxInputSize = getMaxInputSize(codecInfo, format); if (streamFormats.length == 1) { // The single entry in streamFormats must correspond to the format for which the codec is // being configured. @@ -994,7 +994,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { (streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE); maxWidth = Math.max(maxWidth, streamFormat.width); maxHeight = Math.max(maxHeight, streamFormat.height); - maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat)); + maxInputSize = Math.max(maxInputSize, getMaxInputSize(codecInfo, streamFormat)); } } if (haveUnknownDimensions) { @@ -1004,7 +1004,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maxWidth = Math.max(maxWidth, codecMaxSize.x); maxHeight = Math.max(maxHeight, codecMaxSize.y); maxInputSize = - Math.max(maxInputSize, getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight)); + Math.max( + maxInputSize, + getMaxInputSize(codecInfo, format.sampleMimeType, maxWidth, maxHeight)); Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight); } } @@ -1053,13 +1055,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * Returns a maximum input buffer size for a given format. + * Returns a maximum input buffer size for a given codec and format. * + * @param codecInfo Information about the {@link MediaCodec} being configured. * @param format The format. * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not * be determined. */ - private static int getMaxInputSize(Format format) { + private static int getMaxInputSize(MediaCodecInfo codecInfo, Format format) { if (format.maxInputSize != Format.NO_VALUE) { // The format defines an explicit maximum input size. Add the total size of initialization // data buffers, as they may need to be queued in the same input buffer as the largest sample. @@ -1072,20 +1075,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } else { // Calculated maximum input sizes are overestimates, so it's not necessary to add the size of // initialization data. - return getMaxInputSize(format.sampleMimeType, format.width, format.height); + return getMaxInputSize(codecInfo, format.sampleMimeType, format.width, format.height); } } /** - * Returns a maximum input size for a given mime type, width and height. + * Returns a maximum input size for a given codec, mime type, width and height. * + * @param codecInfo Information about the {@link MediaCodec} being configured. * @param sampleMimeType The format mime type. * @param width The width in pixels. * @param height The height in pixels. * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be * determined. */ - private static int getMaxInputSize(String sampleMimeType, int width, int height) { + private static int getMaxInputSize( + MediaCodecInfo codecInfo, String sampleMimeType, int width, int height) { if (width == Format.NO_VALUE || height == Format.NO_VALUE) { // We can't infer a maximum input size without video dimensions. return Format.NO_VALUE; @@ -1101,9 +1106,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { minCompressionRatio = 2; break; case MimeTypes.VIDEO_H264: - if ("BRAVIA 4K 2015".equals(Util.MODEL)) { - // The Sony BRAVIA 4k TV has input buffers that are too small for the calculated 4k video - // maximum input size, so use the default value. + if ("BRAVIA 4K 2015".equals(Util.MODEL) // Sony Bravia 4K + || ("Amazon".equals(Util.MANUFACTURER) + && ("KFSOWI".equals(Util.MODEL) // Kindle Soho + || ("AFTS".equals(Util.MODEL) && codecInfo.secure)))) { // Fire TV Gen 2 + // Use the default value for cases where platform limitations may prevent buffers of the + // calculated maximum input size from being allocated. return Format.NO_VALUE; } // Round up width/height to an integer number of macroblocks. From 0a80b47edb83503297e1e629e1db86712b6bfb06 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 22 Jun 2018 07:01:57 -0700 Subject: [PATCH 04/51] Extract scheme specific data from PSSH for some FireOS devices ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201679215 --- .../android/exoplayer2/drm/FrameworkMediaDrm.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index 9f30cef0f8..a10c4c612e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -121,11 +121,17 @@ public final class FrameworkMediaDrm implements ExoMediaDrm optionalParameters) throws NotProvisionedException { - // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. - if (Util.SDK_INT < 21 && C.WIDEVINE_UUID.equals(uuid)) { + // 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 Widevine PSSH atom, so leave it unchanged. + // Extraction failed. schemeData isn't a PSSH atom, so leave it unchanged. } else { init = psshData; } From a37bd0d66fffb287190139166daa4405b9525ae0 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 22 Jun 2018 09:19:23 -0700 Subject: [PATCH 05/51] Parse ms:laurl from ContentProtection in DASH Issue:#3393 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201694813 --- .../source/dash/manifest/DashManifestParser.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 0a4274e674..a443ca861f 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -355,6 +355,7 @@ public class DashManifestParser extends DefaultHandler protected Pair parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { String schemeType = null; + String licenseServerUrl = null; byte[] data = null; UUID uuid = null; boolean requiresSecureDecoder = false; @@ -389,7 +390,9 @@ 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) { @@ -409,8 +412,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); } From 9c76ba06036b56205232cca4f2e97e748334ee80 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 22 Jun 2018 09:27:37 -0700 Subject: [PATCH 06/51] Wait for DRM keys before codec configuration on FireOS devices ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201695876 --- .../mediacodec/MediaCodecRenderer.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 48002c7a86..b966492d8c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -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) { @@ -1209,6 +1218,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } + /** + * 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. *

From cbfa602866fedaef883ecb0118c14ce46645633a Mon Sep 17 00:00:00 2001 From: Arek Karbowy Date: Wed, 27 Jun 2018 11:29:04 +0100 Subject: [PATCH 07/51] opt out of using DummySurface on specific Fire TV device --- .../exoplayer2/video/MediaCodecVideoRenderer.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 990025b5af..5e8a98ea68 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -825,10 +825,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private boolean shouldUseDummySurface(MediaCodecInfo codecInfo) { return Util.SDK_INT >= 23 && !tunneling + && !codecNeedsDummySurfaceWorkaround(codecInfo.name) && !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name) && (!codecInfo.secure || DummySurface.isSecureSupported(context)); } - + private void setJoiningDeadlineMs() { joiningDeadlineMs = allowedJoiningTimeMs > 0 ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; @@ -1171,6 +1172,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); } + private static boolean codecNeedsDummySurfaceWorkaround(String name) { + // Work around https://github.com/google/ExoPlayer/issues/4419. + return (("needle".equals(Util.DEVICE)) // FireTV 4K + && "OMX.amlogic.avc.decoder.awesome".equals(name)); + } + /** * Returns whether the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} * incorrectly. From 4a8bd911cf98f967ec25605f19f6d92290ff5c1d Mon Sep 17 00:00:00 2001 From: Keith Thompson Date: Wed, 27 Jun 2018 16:19:22 +0100 Subject: [PATCH 08/51] Add withSkippedAd method to AdPlaybackState. Currently it is only possible to skip entire ad groups but not individual ads within a given ad group. --- .../exoplayer2/source/ads/AdPlaybackState.java | 8 ++++++++ .../exoplayer2/source/ads/AdPlaybackStateTest.java | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index 8654e94bdb..246d804c89 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -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) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java index a8cc04473d..da03df9b8a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java @@ -89,6 +89,19 @@ public final class AdPlaybackStateTest { assertThat(state.adGroups[0].states[2]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE); } + @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); From fd8751dbc6de30194584e623a047c7ba19b1beb1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 26 Jun 2018 03:03:36 -0700 Subject: [PATCH 09/51] Make ImaAdsLoader robust to calls after it's released Issue: #3879 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202100576 --- RELEASENOTES.md | 2 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 51 +++++++++++-------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 46a1117202..42b9449410 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,8 @@ * DRM: * Allow DrmInitData to carry a license server URL ([#3393](https://github.com/google/ExoPlayer/issues/3393)). +* IMA: Fix behavior when creating/releasing the player then releasing + `ImaAdsLoader` ((#3879)[https://github.com/google/ExoPlayer/issues/3879]). ### 2.8.2 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 3256da21dd..9045ee777a 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -267,13 +267,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 +281,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 +304,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; /** @@ -509,6 +499,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 +553,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 +649,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 +664,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 +692,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 +739,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"); @@ -1083,6 +1090,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 +1176,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); From be995f0b274c98efa286121b25cfbcf69fa15af7 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 26 Jun 2018 03:34:19 -0700 Subject: [PATCH 10/51] Rename HlsPlaylistTracker's release to stop ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202103550 --- .../exoplayer2/source/hls/HlsMediaSource.java | 2 +- .../hls/playlist/DefaultHlsPlaylistTracker.java | 2 +- .../source/hls/playlist/HlsPlaylistTracker.java | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index e0c805e1af..aa57ca24f5 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -401,7 +401,7 @@ public final class HlsMediaSource extends BaseMediaSource @Override public void releaseSourceInternal() { if (playlistTracker != null) { - playlistTracker.release(); + playlistTracker.stop(); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java index 014a302de7..7266184750 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -105,7 +105,7 @@ public final class DefaultHlsPlaylistTracker } @Override - public void release() { + public void stop() { primaryHlsUrl = null; primaryUrlSnapshot = null; masterPlaylist = null; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index febd1c217d..01dce9fcd3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -100,8 +100,8 @@ public interface HlsPlaylistTracker { /** * Starts the playlist tracker. * - *

Must be called from the playback thread. A tracker may be restarted after a {@link - * #release()} call. + *

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. + * + *

Must be called once per {@link #start} call. + */ + void stop(); /** * Registers a listener to receive events from the playlist tracker. From a0810856e71018551a077569e13c2d630adc622e Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 27 Jun 2018 02:47:21 -0700 Subject: [PATCH 11/51] Add DRM workaround for Asus Zenfone 2. Issue: #4413 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202277924 --- RELEASENOTES.md | 4 +++- .../android/exoplayer2/drm/FrameworkMediaDrm.java | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 42b9449410..39d1a78403 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,7 +6,9 @@ * Allow DrmInitData to carry a license server URL ([#3393](https://github.com/google/ExoPlayer/issues/3393)). * IMA: Fix behavior when creating/releasing the player then releasing - `ImaAdsLoader` ((#3879)[https://github.com/google/ExoPlayer/issues/3879]). + `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). +* Fix issue playing DRM protected streams on Asus Zenfone 2 + ([#4403](https://github.com/google/ExoPlayer/issues/4413)). ### 2.8.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index a10c4c612e..c87e82f972 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -71,6 +71,9 @@ public final class FrameworkMediaDrm implements ExoMediaDrmSee GitHub issue #4413. + */ + private static boolean needsForceL3Workaround() { + return "ASUS_Z00AD".equals(Util.MODEL); + } } From 186711287b0ebd827fa175a0556e8d9b64f38426 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 28 Jun 2018 04:05:43 -0700 Subject: [PATCH 12/51] Add some leeway for finding additional tracks in PsExtractor. Currently we immediately stop searching after we found one video and one audio track. This change adds some leeway to detect additional tracks. Issue:#4406 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202455491 --- RELEASENOTES.md | 2 ++ .../exoplayer2/extractor/ts/PsExtractor.java | 21 +++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 39d1a78403..fb8ca532ec 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,8 @@ `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). * 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)). ### 2.8.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index f3aad6ba6b..8acb36b41e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -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(); } From 6cdaf593e01483e87b32c04bd14495ed9064b0bf Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 28 Jun 2018 09:54:09 -0700 Subject: [PATCH 13/51] Improve DefaultTrackSelector documentation It's quite hard to find the defaults currently. Placing them on each variable makes them easier to find. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202495929 --- .../trackselection/DefaultTrackSelector.java | 167 ++++++++++-------- 1 file changed, 94 insertions(+), 73 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 3bbb2a7941..e905060bd2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -153,7 +153,8 @@ import java.util.concurrent.atomic.AtomicReference; public class DefaultTrackSelector extends MappingTrackSelector { /** - * A builder for {@link Parameters}. + * A builder for {@link Parameters}. See the {@link Parameters} documentation for explanations of + * the parameters that can be configured using this builder. */ public static final class ParametersBuilder { @@ -177,9 +178,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean viewportOrientationMayChange; private int tunnelingAudioSessionId; - /** - * Creates a builder obtaining the initial values from {@link Parameters#DEFAULT}. - */ + /** Creates a builder with default initial values. */ public ParametersBuilder() { this(Parameters.DEFAULT); } @@ -343,15 +342,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Equivalent to invoking {@link #setViewportSize} with the viewport size obtained from - * {@link Util#getPhysicalDisplaySize(Context)}. + * Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size + * obtained from {@link Util#getPhysicalDisplaySize(Context)}. * - * @param context The context to obtain the viewport size from. - * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}. + * @param context Any context. + * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}. * @return This builder. */ - public ParametersBuilder setViewportSizeToPhysicalDisplaySize(Context context, - boolean viewportOrientationMayChange) { + public ParametersBuilder setViewportSizeToPhysicalDisplaySize( + Context context, boolean viewportOrientationMayChange) { // Assume the viewport is fullscreen. Point viewportSize = Util.getPhysicalDisplaySize(context); return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); @@ -368,13 +367,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and - * {@link Parameters#viewportOrientationMayChange}. + * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and {@link + * Parameters#viewportOrientationMayChange}. * + * @param viewportWidth See {@link Parameters#viewportWidth}. + * @param viewportHeight See {@link Parameters#viewportHeight}. + * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}. * @return This builder. */ - public ParametersBuilder setViewportSize(int viewportWidth, int viewportHeight, - boolean viewportOrientationMayChange) { + public ParametersBuilder setViewportSize( + int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { this.viewportWidth = viewportWidth; this.viewportHeight = viewportHeight; this.viewportOrientationMayChange = viewportOrientationMayChange; @@ -485,8 +487,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Enables or disables tunneling. To enable tunneling, pass an audio session id to use when in - * tunneling mode. Session ids can be generated using {@link + * See {@link Parameters#tunnelingAudioSessionId}. + * + *

Enables or disables tunneling. To enable tunneling, pass an audio session id to use when + * in tunneling mode. Session ids can be generated using {@link * C#generateAudioSessionIdV21(Context)}. To disable tunneling pass {@link * C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and * supported by the audio and video renderers for the selected tracks. @@ -540,25 +544,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** Constraint parameters for {@link DefaultTrackSelector}. */ public static final class Parameters implements Parcelable { - /** - * An instance with default values: - * - *

    - *
  • No preferred audio language. - *
  • No preferred text language. - *
  • Text tracks with undetermined language are not selected if no track with {@link - * #preferredTextLanguage} is available. - *
  • All selection flags are considered for text track selections. - *
  • Lowest bitrate track selections are not forced. - *
  • Adaptation between different mime types is not allowed. - *
  • Non seamless adaptation is allowed. - *
  • No max limit for video width/height. - *
  • No max video bitrate. - *
  • Video constraints are exceeded if no supported selection can be made otherwise. - *
  • Renderer capabilities are exceeded if no supported selection can be made. - *
  • No viewport constraints. - *
- */ + /** An instance with default values. */ public static final Parameters DEFAULT = new Parameters(); // Per renderer overrides. @@ -568,105 +554,140 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio /** - * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. - * {@code null} selects the default track, or the first track if there's no default. + * The preferred language for audio and forced text tracks, as an ISO 639-2/T tag. {@code null} + * selects the default track, or the first track if there's no default. The default value is + * {@code null}. */ public final @Nullable String preferredAudioLanguage; // Text /** * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the - * default track if there is one, or no track otherwise. + * default track if there is one, or no track otherwise. The default value is {@code null}. */ public final @Nullable String preferredTextLanguage; /** - * Whether a text track with undetermined language should be selected if no track with - * {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. + * Whether a text track with undetermined language should be selected if no track with {@link + * #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The + * default value is {@code false}. */ public final boolean selectUndeterminedTextLanguage; /** * Bitmask of selection flags that are disabled for text track selections. See {@link - * C.SelectionFlags}. + * C.SelectionFlags}. The default value is {@code 0} (i.e. no flags). */ public final int disabledTextTrackSelectionFlags; // Video /** - * Maximum allowed video width. + * Maximum allowed video width. The default value is {@link Integer#MAX_VALUE} (i.e. no + * constraint). + * + *

Note: To restrict adaptive video track selections to be suitable for a given viewport (the + * region of the display within which video will be played), it's preferable to use viewport + * constraints ({@link #viewportWidth}, {@link #viewportHeight} and + * {@link #viewportOrientationMayChange}) rather than video size constraints + * ({@link #maxVideoWidth and {@link #maxVideoHeight}). This is because selecting video tracks + * for a given viewport is normally more nuanced than imposing fixed limits on resolution (e.g. + * it's normally preferable to select one format that exceeds the size of the viewport, and to + * take into account the possibility that the orientation of the viewport may change). */ public final int maxVideoWidth; /** - * Maximum allowed video height. + * Maximum allowed video height. The default value is {@link Integer#MAX_VALUE} (i.e. no + * constraint). + * + *

Note: To restrict adaptive video track selections to be suitable for a given viewport (the + * region of the display within which video will be played), it's preferable to use viewport + * constraints ({@link #viewportWidth}, {@link #viewportHeight} and + * {@link #viewportOrientationMayChange}) rather than video size constraints + * ({@link #maxVideoWidth and {@link #maxVideoHeight}). This is because selecting video tracks + * for a given viewport is normally more nuanced than imposing fixed limits on resolution (e.g. + * it's normally preferable to select one format that exceeds the size of the viewport, and to + * take into account the possibility that the orientation of the viewport may change). */ public final int maxVideoHeight; /** - * Maximum video bitrate. + * Maximum video bitrate. The default value is {@link Integer#MAX_VALUE} (i.e. no constraint). */ public final int maxVideoBitrate; /** - * Whether to exceed video constraints when no selection can be made otherwise. + * Whether to exceed the {@link #maxVideoWidth}, {@link #maxVideoHeight} and {@link + * #maxVideoBitrate} constraints when no selection can be made otherwise. The default value is + * {@code true}. */ public final boolean exceedVideoConstraintsIfNecessary; /** - * Viewport width in pixels. Constrains video tracks selections for adaptive playbacks so that - * only tracks suitable for the viewport are selected. + * Viewport width in pixels. Constrains video track selections for adaptive playbacks so that + * only tracks suitable for the viewport are selected. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). */ public final int viewportWidth; /** - * Viewport height in pixels. Constrains video tracks selections for adaptive playbacks so that - * only tracks suitable for the viewport are selected. + * Viewport height in pixels. Constrains video track selections for adaptive playbacks so that + * only tracks suitable for the viewport are selected. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). */ public final int viewportHeight; /** - * Whether the viewport orientation may change during playback. Constrains video tracks + * Whether the viewport orientation may change during playback. Constrains video track * selections for adaptive playbacks so that only tracks suitable for the viewport are selected. + * The default value is {@code true}. */ public final boolean viewportOrientationMayChange; // General /** * Whether to force selection of the single lowest bitrate audio and video tracks that comply - * with all other constraints. + * with all other constraints. The default value is {@code false}. */ public final boolean forceLowestBitrate; /** - * Whether to allow adaptive selections containing mixed mime types. + * Whether to allow adaptive selections containing mixed mime types. The default value is {@code + * false}. */ public final boolean allowMixedMimeAdaptiveness; /** - * Whether to allow adaptive selections where adaptation may not be completely seamless. + * Whether to allow adaptive selections where adaptation may not be completely seamless. The + * default value is {@code true}. */ public final boolean allowNonSeamlessAdaptiveness; /** - * Whether to exceed renderer capabilities when no selection can be made otherwise. + * Whether to exceed renderer capabilities when no selection can be made otherwise. This + * parameter applies when all of the tracks available for a renderer exceed the renderer's + * reported capabilities. If the parameter is {@code true} then the lowest quality track will + * still be selected. Playback may succeed if the renderer has under-reported its true + * capabilities. If {@code false} then no track will be selected. The default value is {@code + * true}. */ public final boolean exceedRendererCapabilitiesIfNecessary; /** * The audio session id to use when tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling - * is not to be enabled. + * is disabled. The default value is {@link C#AUDIO_SESSION_ID_UNSET} (i.e. tunneling is + * disabled). */ public final int tunnelingAudioSessionId; private Parameters() { this( - new SparseArray>(), - new SparseBooleanArray(), - null, - null, - false, - 0, - false, - false, - true, - Integer.MAX_VALUE, - Integer.MAX_VALUE, - Integer.MAX_VALUE, - true, - true, - Integer.MAX_VALUE, - Integer.MAX_VALUE, - true, - C.AUDIO_SESSION_ID_UNSET); + /* selectionOverrides= */ new SparseArray<>(), + /* rendererDisabledFlags= */ new SparseBooleanArray(), + /* preferredAudioLanguage= */ null, + /* preferredTextLanguage= */ null, + /* selectUndeterminedTextLanguage= */ false, + /* disabledTextTrackSelectionFlags= */ 0, + /* forceLowestBitrate= */ false, + /* allowMixedMimeAdaptiveness= */ false, + /* allowNonSeamlessAdaptiveness= */ true, + /* maxVideoWidth= */ Integer.MAX_VALUE, + /* maxVideoHeight= */ Integer.MAX_VALUE, + /* maxVideoBitrate= */ Integer.MAX_VALUE, + /* exceedVideoConstraintsIfNecessary= */ true, + /* exceedRendererCapabilitiesIfNecessary= */ true, + /* viewportWidth= */ Integer.MAX_VALUE, + /* viewportHeight= */ Integer.MAX_VALUE, + /* viewportOrientationMayChange= */ true, + /* tunnelingAudioSessionId= */ C.AUDIO_SESSION_ID_UNSET); } /* package */ Parameters( From 3c4384ddd974316a23ba313a4cb060ceacf00f97 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 29 Jun 2018 03:20:18 -0700 Subject: [PATCH 14/51] Simplify DefaultTrackSelector documentation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202623116 --- .../trackselection/DefaultTrackSelector.java | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index e905060bd2..9000ace46d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -583,28 +583,18 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Maximum allowed video width. The default value is {@link Integer#MAX_VALUE} (i.e. no * constraint). * - *

Note: To restrict adaptive video track selections to be suitable for a given viewport (the - * region of the display within which video will be played), it's preferable to use viewport - * constraints ({@link #viewportWidth}, {@link #viewportHeight} and - * {@link #viewportOrientationMayChange}) rather than video size constraints - * ({@link #maxVideoWidth and {@link #maxVideoHeight}). This is because selecting video tracks - * for a given viewport is normally more nuanced than imposing fixed limits on resolution (e.g. - * it's normally preferable to select one format that exceeds the size of the viewport, and to - * take into account the possibility that the orientation of the viewport may change). + *

To constrain adaptive video track selections to be suitable for a given viewport (the + * region of the display within which video will be played), use ({@link #viewportWidth}, {@link + * #viewportHeight} and {@link #viewportOrientationMayChange}) instead. */ public final int maxVideoWidth; /** * Maximum allowed video height. The default value is {@link Integer#MAX_VALUE} (i.e. no * constraint). * - *

Note: To restrict adaptive video track selections to be suitable for a given viewport (the - * region of the display within which video will be played), it's preferable to use viewport - * constraints ({@link #viewportWidth}, {@link #viewportHeight} and - * {@link #viewportOrientationMayChange}) rather than video size constraints - * ({@link #maxVideoWidth and {@link #maxVideoHeight}). This is because selecting video tracks - * for a given viewport is normally more nuanced than imposing fixed limits on resolution (e.g. - * it's normally preferable to select one format that exceeds the size of the viewport, and to - * take into account the possibility that the orientation of the viewport may change). + *

To constrain adaptive video track selections to be suitable for a given viewport (the + * region of the display within which video will be played), use ({@link #viewportWidth}, {@link + * #viewportHeight} and {@link #viewportOrientationMayChange}) instead. */ public final int maxVideoHeight; /** @@ -653,12 +643,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean allowNonSeamlessAdaptiveness; /** - * Whether to exceed renderer capabilities when no selection can be made otherwise. This - * parameter applies when all of the tracks available for a renderer exceed the renderer's - * reported capabilities. If the parameter is {@code true} then the lowest quality track will - * still be selected. Playback may succeed if the renderer has under-reported its true - * capabilities. If {@code false} then no track will be selected. The default value is {@code - * true}. + * Whether to exceed renderer capabilities when no selection can be made otherwise. + * + *

This parameter applies when all of the tracks available for a renderer exceed the + * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality + * track will still be selected. Playback may succeed if the renderer has under-reported its + * true capabilities. If {@code false} then no track will be selected. The default value is + * {@code true}. */ public final boolean exceedRendererCapabilitiesIfNecessary; /** From 165fef878239f8483d4e2d171b47060ac87bb26d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 29 Jun 2018 09:43:49 -0700 Subject: [PATCH 15/51] Fix application of styles for CEA-608 Issue: #4321 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202660712 --- RELEASENOTES.md | 2 + .../exoplayer2/text/cea/Cea608Decoder.java | 222 ++++++++++-------- 2 files changed, 127 insertions(+), 97 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fb8ca532ec..f5171af80e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,8 @@ * DRM: * Allow DrmInitData to carry a license server URL ([#3393](https://github.com/google/ExoPlayer/issues/3393)). +* CEA-608: Improve handling of embedded styles + ([#4321](https://github.com/google/ExoPlayer/issues/4321)). * IMA: Fix behavior when creating/releasing the player then releasing `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). * Fix issue playing DRM protected streams on Asus Zenfone 2 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 57614ae880..725321e53f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -21,10 +21,10 @@ import android.text.Layout.Alignment; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; -import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; @@ -55,15 +55,13 @@ public final class Cea608Decoder extends CeaDecoder { private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9}; private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28}; - private static final int[] COLORS = new int[] { - Color.WHITE, - Color.GREEN, - Color.BLUE, - Color.CYAN, - Color.RED, - Color.YELLOW, - Color.MAGENTA, - }; + + private static final int[] STYLE_COLORS = + new int[] { + Color.WHITE, Color.GREEN, Color.BLUE, Color.CYAN, Color.RED, Color.YELLOW, Color.MAGENTA + }; + private static final int STYLE_ITALICS = 0x07; + private static final int STYLE_UNCHANGED = 0x08; // The default number of rows to display in roll-up captions mode. private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4; @@ -377,18 +375,10 @@ public final class Cea608Decoder extends CeaDecoder { // A midrow control code advances the cursor. currentCueBuilder.append(' '); - // cc2 - 0|0|1|0|ATRBT|U - // ATRBT is the 3-byte encoded attribute, and U is the underline toggle - boolean isUnderlined = (cc2 & 0x01) == 0x01; - currentCueBuilder.setUnderline(isUnderlined); - - int attribute = (cc2 >> 1) & 0x0F; - if (attribute == 0x07) { - currentCueBuilder.setMidrowStyle(new StyleSpan(Typeface.ITALIC), 2); - currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(Color.WHITE), 1); - } else { - currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(COLORS[attribute]), 1); - } + // cc2 - 0|0|1|0|STYLE|U + boolean underline = (cc2 & 0x01) == 0x01; + int style = (cc2 >> 1) & 0x07; + currentCueBuilder.setStyle(style, underline); } private void handlePreambleAddressCode(byte cc1, byte cc2) { @@ -414,22 +404,18 @@ public final class Cea608Decoder extends CeaDecoder { currentCueBuilder.setRow(row); } - if ((cc2 & 0x01) == 0x01) { - currentCueBuilder.setPreambleStyle(new UnderlineSpan()); - } - // cc2 - 0|1|N|0|STYLE|U // cc2 - 0|1|N|1|CURSR|U - int attribute = cc2 >> 1 & 0x0F; - if (attribute <= 0x07) { - if (attribute == 0x07) { - currentCueBuilder.setPreambleStyle(new StyleSpan(Typeface.ITALIC)); - currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(Color.WHITE)); - } else { - currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(COLORS[attribute])); - } - } else { - currentCueBuilder.setIndent(COLUMN_INDICES[attribute & 0x07]); + boolean isCursor = (cc2 & 0x10) == 0x10; + boolean underline = (cc2 & 0x01) == 0x01; + int cursorOrStyle = (cc2 >> 1) & 0x07; + + // We need to call setStyle even for the isCursor case, to update the underline bit. + // STYLE_UNCHANGED is used for this case. + currentCueBuilder.setStyle(isCursor ? STYLE_UNCHANGED : cursorOrStyle, underline); + + if (isCursor) { + currentCueBuilder.setIndent(COLUMN_INDICES[cursorOrStyle]); } } @@ -585,44 +571,37 @@ public final class Cea608Decoder extends CeaDecoder { private static class CueBuilder { - private static final int POSITION_UNSET = -1; - // 608 captions define a 15 row by 32 column screen grid. These constants convert from 608 // positions to normalized screen position. private static final int SCREEN_CHARWIDTH = 32; private static final int BASE_ROW = 15; - private final List preambleStyles; - private final List midrowStyles; + private final List cueStyles; private final List rolledUpCaptions; - private final SpannableStringBuilder captionStringBuilder; + private final StringBuilder captionStringBuilder; private int row; private int indent; private int tabOffset; private int captionMode; private int captionRowCount; - private int underlineStartPosition; public CueBuilder(int captionMode, int captionRowCount) { - preambleStyles = new ArrayList<>(); - midrowStyles = new ArrayList<>(); + cueStyles = new ArrayList<>(); rolledUpCaptions = new ArrayList<>(); - captionStringBuilder = new SpannableStringBuilder(); + captionStringBuilder = new StringBuilder(); reset(captionMode); setCaptionRowCount(captionRowCount); } public void reset(int captionMode) { this.captionMode = captionMode; - preambleStyles.clear(); - midrowStyles.clear(); + cueStyles.clear(); rolledUpCaptions.clear(); - captionStringBuilder.clear(); + captionStringBuilder.setLength(0); row = BASE_ROW; indent = 0; tabOffset = 0; - underlineStartPosition = POSITION_UNSET; } public void setCaptionRowCount(int captionRowCount) { @@ -630,7 +609,8 @@ public final class Cea608Decoder extends CeaDecoder { } public boolean isEmpty() { - return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty() + return cueStyles.isEmpty() + && rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0; } @@ -638,6 +618,16 @@ public final class Cea608Decoder extends CeaDecoder { int length = captionStringBuilder.length(); if (length > 0) { captionStringBuilder.delete(length - 1, length); + // Decrement style start positions if necessary. + for (int i = cueStyles.size() - 1; i >= 0; i--) { + CueStyle style = cueStyles.get(i); + if (style.start == length) { + style.start--; + } else { + // All earlier cues must have style.start < length. + break; + } + } } } @@ -651,11 +641,8 @@ public final class Cea608Decoder extends CeaDecoder { public void rollUp() { rolledUpCaptions.add(buildSpannableString()); - captionStringBuilder.clear(); - preambleStyles.clear(); - midrowStyles.clear(); - underlineStartPosition = POSITION_UNSET; - + captionStringBuilder.setLength(0); + cueStyles.clear(); int numRows = Math.min(captionRowCount, row); while (rolledUpCaptions.size() >= numRows) { rolledUpCaptions.remove(0); @@ -670,23 +657,8 @@ public final class Cea608Decoder extends CeaDecoder { tabOffset = tabs; } - public void setPreambleStyle(CharacterStyle style) { - preambleStyles.add(style); - } - - public void setMidrowStyle(CharacterStyle style, int nextStyleIncrement) { - midrowStyles.add(new CueStyle(style, captionStringBuilder.length(), nextStyleIncrement)); - } - - public void setUnderline(boolean enabled) { - if (enabled) { - underlineStartPosition = captionStringBuilder.length(); - } else if (underlineStartPosition != POSITION_UNSET) { - // underline spans won't overlap, so it's safe to modify the builder directly with them - captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, - captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - underlineStartPosition = POSITION_UNSET; - } + public void setStyle(int style, boolean underline) { + cueStyles.add(new CueStyle(style, underline, captionStringBuilder.length())); } public void append(char text) { @@ -694,31 +666,69 @@ public final class Cea608Decoder extends CeaDecoder { } public SpannableString buildSpannableString() { - int length = captionStringBuilder.length(); + SpannableStringBuilder builder = new SpannableStringBuilder(captionStringBuilder); + int length = builder.length(); - // preamble styles apply to the entire cue - for (int i = 0; i < preambleStyles.size(); i++) { - captionStringBuilder.setSpan(preambleStyles.get(i), 0, length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + int underlineStartPosition = C.INDEX_UNSET; + int italicStartPosition = C.INDEX_UNSET; + int colorStartPosition = 0; + int color = Color.WHITE; + + boolean nextItalic = false; + int nextColor = Color.WHITE; + + for (int i = 0; i < cueStyles.size(); i++) { + CueStyle cueStyle = cueStyles.get(i); + boolean underline = cueStyle.underline; + int style = cueStyle.style; + if (style != STYLE_UNCHANGED) { + // If the style is a color then italic is cleared. + nextItalic = style == STYLE_ITALICS; + // If the style is italic then the color is left unchanged. + nextColor = style == STYLE_ITALICS ? nextColor : STYLE_COLORS[style]; + } + + int position = cueStyle.start; + int nextPosition = (i + 1) < cueStyles.size() ? cueStyles.get(i + 1).start : length; + if (position == nextPosition) { + // There are more cueStyles to process at the current position. + continue; + } + + // Process changes to underline up to the current position. + if (underlineStartPosition != C.INDEX_UNSET && !underline) { + setUnderlineSpan(builder, underlineStartPosition, position); + underlineStartPosition = C.INDEX_UNSET; + } else if (underlineStartPosition == C.INDEX_UNSET && underline) { + underlineStartPosition = position; + } + // Process changes to italic up to the current position. + if (italicStartPosition != C.INDEX_UNSET && !nextItalic) { + setItalicSpan(builder, italicStartPosition, position); + italicStartPosition = C.INDEX_UNSET; + } else if (italicStartPosition == C.INDEX_UNSET && nextItalic) { + italicStartPosition = position; + } + // Process changes to color up to the current position. + if (nextColor != color) { + setColorSpan(builder, colorStartPosition, position, color); + color = nextColor; + colorStartPosition = position; + } } - // midrow styles only apply to part of the cue, and after preamble styles - for (int i = 0; i < midrowStyles.size(); i++) { - CueStyle cueStyle = midrowStyles.get(i); - int end = (i < midrowStyles.size() - cueStyle.nextStyleIncrement) - ? midrowStyles.get(i + cueStyle.nextStyleIncrement).start - : length; - captionStringBuilder.setSpan(cueStyle.style, cueStyle.start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + // Add any final spans. + if (underlineStartPosition != C.INDEX_UNSET && underlineStartPosition != length) { + setUnderlineSpan(builder, underlineStartPosition, length); + } + if (italicStartPosition != C.INDEX_UNSET && italicStartPosition != length) { + setItalicSpan(builder, italicStartPosition, length); + } + if (colorStartPosition != length) { + setColorSpan(builder, colorStartPosition, length, color); } - // special case for midrow underlines that went to the end of the cue - if (underlineStartPosition != POSITION_UNSET) { - captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - return new SpannableString(captionStringBuilder); + return new SpannableString(builder); } public Cue build() { @@ -788,16 +798,34 @@ public final class Cea608Decoder extends CeaDecoder { return captionStringBuilder.toString(); } + private static void setUnderlineSpan(SpannableStringBuilder builder, int start, int end) { + builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static void setItalicSpan(SpannableStringBuilder builder, int start, int end) { + builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static void setColorSpan( + SpannableStringBuilder builder, int start, int end, int color) { + if (color == Color.WHITE) { + // White is treated as the default color (i.e. no span is attached). + return; + } + builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + private static class CueStyle { - public final CharacterStyle style; - public final int start; - public final int nextStyleIncrement; + public final int style; + public final boolean underline; - public CueStyle(CharacterStyle style, int start, int nextStyleIncrement) { + public int start; + + public CueStyle(int style, boolean underline, int start) { this.style = style; + this.underline = underline; this.start = start; - this.nextStyleIncrement = nextStyleIncrement; } } From 1f188c7c0063c374e18d445b48415df533100442 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Jul 2018 01:38:26 -0700 Subject: [PATCH 16/51] Exclude text streams from duration calculations Issue: #4029 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202912333 --- RELEASENOTES.md | 2 ++ .../source/dash/DashMediaSource.java | 20 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f5171af80e..3df15654e0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### 2.8.3 ### +* DASH: Exclude text streams from duration calculations + ([#4029](https://github.com/google/ExoPlayer/issues/4029)). * DRM: * Allow DrmInitData to carry a license server URL ([#3393](https://github.com/google/ExoPlayer/issues/3393)). diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 7b854e9d29..66b933f234 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -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); } From a50d31a70b975d16bd863ded771451b6157675ad Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 6 Jul 2018 08:32:38 -0700 Subject: [PATCH 17/51] Add workaround for unmatched track indices in trex and tkhd. Both boxes should contain the same list of track indices. However, if only one track index in each list does not match, we can just assume that these belong together. Issue:#4477 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203481258 --- RELEASENOTES.md | 3 +++ .../extractor/mp4/FragmentedMp4Extractor.java | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3df15654e0..93c5c344bc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,9 @@ ([#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)). ### 2.8.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 0bf42f1839..a61b41dea2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -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 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. From 6ad98405a331e31f3cc673204ef96623399e2be4 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 6 Jul 2018 08:36:14 -0700 Subject: [PATCH 18/51] Avoid providing invalid responses to MediaDrm MediaDrm.provideXResponse methods only accept the response corresponding to the most recent MediaDrm.getXRequest call. Previously, our code allowed the following incorrect call sequence: a = getKeyRequest b = getKeyRequest provideKeyResponse(responseFor(a)); This would occur in the edge case of a second key request being triggered whilst the first was still in flight. The provideKeyResponse call would then fail. This change fixes the problem by treating responseFor(a) as stale. Note that a slightly better fix would be to defer calling getKeyRequest the second time until after processing the response corresponding to the first one, however this is significantly harder to implement, and is probably not worth it for what should be an edge case. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203481685 --- .../exoplayer2/drm/DefaultDrmSession.java | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index fbbbcbc9ef..c4be283548 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -97,6 +97,9 @@ import java.util.UUID; private byte[] sessionId; private byte[] offlineLicenseKeySetId; + private Object currentKeyRequest; + private Object currentProvisionRequest; + /** * Instantiates a new DRM session. * @@ -171,6 +174,8 @@ import java.util.UUID; requestHandlerThread = null; mediaCrypto = null; lastException = null; + currentKeyRequest = null; + currentProvisionRequest = null; if (sessionId != null) { mediaDrm.closeSession(sessionId); sessionId = null; @@ -215,8 +220,8 @@ import java.util.UUID; // Provisioning implementation. public void provision() { - ProvisionRequest request = mediaDrm.getProvisionRequest(); - postRequestHandler.obtainMessage(MSG_PROVISION, request, true).sendToTarget(); + currentProvisionRequest = mediaDrm.getProvisionRequest(); + postRequestHandler.post(MSG_PROVISION, currentProvisionRequest, /* allowRetry= */ true); } public void onProvisionCompleted() { @@ -289,11 +294,12 @@ import java.util.UUID; return false; } - private void onProvisionResponse(Object response) { - if (state != STATE_OPENING && !isOpen()) { + private void onProvisionResponse(Object request, Object response) { + if (request != currentProvisionRequest || (state != STATE_OPENING && !isOpen())) { // This event is stale. return; } + currentProvisionRequest = null; if (response instanceof Exception) { provisioningManager.onProvisionError((Exception) response); @@ -383,20 +389,21 @@ import java.util.UUID; licenseServerUrl = schemeData.licenseServerUrl; } try { - KeyRequest request = + KeyRequest mediaDrmKeyRequest = mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); - Pair arguments = Pair.create(request, licenseServerUrl); - postRequestHandler.obtainMessage(MSG_KEYS, arguments, allowRetry).sendToTarget(); + currentKeyRequest = Pair.create(mediaDrmKeyRequest, licenseServerUrl); + postRequestHandler.post(MSG_KEYS, currentKeyRequest, allowRetry); } catch (Exception e) { onKeysError(e); } } - private void onKeyResponse(Object response) { - if (!isOpen()) { + private void onKeyResponse(Object request, Object response) { + if (request != currentKeyRequest || !isOpen()) { // This event is stale. return; } + currentKeyRequest = null; if (response instanceof Exception) { onKeysError((Exception) response); @@ -461,12 +468,15 @@ import java.util.UUID; @Override public void handleMessage(Message msg) { + Pair requestAndResponse = (Pair) msg.obj; + Object request = requestAndResponse.first; + Object response = requestAndResponse.second; switch (msg.what) { case MSG_PROVISION: - onProvisionResponse(msg.obj); + onProvisionResponse(request, response); break; case MSG_KEYS: - onKeyResponse(msg.obj); + onKeyResponse(request, response); break; default: break; @@ -483,23 +493,27 @@ import java.util.UUID; super(backgroundLooper); } - Message obtainMessage(int what, Object object, boolean allowRetry) { - return obtainMessage(what, allowRetry ? 1 : 0 /* allow retry*/, 0 /* error count */, - object); + void post(int what, Object request, boolean allowRetry) { + int allowRetryInt = allowRetry ? 1 : 0; + int errorCount = 0; + obtainMessage(what, allowRetryInt, errorCount, request).sendToTarget(); } @Override @SuppressWarnings("unchecked") public void handleMessage(Message msg) { + Object request = msg.obj; Object response; try { switch (msg.what) { case MSG_PROVISION: - response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj); + response = callback.executeProvisionRequest(uuid, (ProvisionRequest) request); break; case MSG_KEYS: - Pair arguments = (Pair) msg.obj; - response = callback.executeKeyRequest(uuid, arguments.first, arguments.second); + Pair keyRequest = (Pair) request; + KeyRequest mediaDrmKeyRequest = keyRequest.first; + String licenseServerUrl = keyRequest.second; + response = callback.executeKeyRequest(uuid, mediaDrmKeyRequest, licenseServerUrl); break; default: throw new RuntimeException(); @@ -510,7 +524,7 @@ import java.util.UUID; } response = e; } - postResponseHandler.obtainMessage(msg.what, response).sendToTarget(); + postResponseHandler.obtainMessage(msg.what, Pair.create(request, response)).sendToTarget(); } private boolean maybeRetryRequest(Message originalMsg) { From f2f149adca2bd9133e9d973d33de2efee0409c2d Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 6 Jul 2018 09:16:31 -0700 Subject: [PATCH 19/51] Add workaround for unmatched track indices in tkhd and tfhd. If there is only one track, we can assume that both boxes refer to the same track even if the track indices don't match. Issue:#4083 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203485872 --- RELEASENOTES.md | 3 +++ .../extractor/mp4/FragmentedMp4Extractor.java | 19 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 93c5c344bc..70fd33db6d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -18,6 +18,9 @@ * 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)). ### 2.8.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index a61b41dea2..74181f8898 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -654,7 +654,7 @@ public final class FragmentedMp4Extractor implements Extractor { private static void parseTraf(ContainerAtom traf, SparseArray 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; } @@ -805,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 trackBundles, int flags) { + private static TrackBundle parseTfhd( + ParsableByteArray tfhd, SparseArray 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; } @@ -836,6 +836,17 @@ public final class FragmentedMp4Extractor implements Extractor { return trackBundle; } + private static @Nullable TrackBundle getTrackBundle( + SparseArray 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). * From 7207665ce0bc24bf5d3f6e56fc13b8db153dbc94 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sat, 7 Jul 2018 12:28:58 -0400 Subject: [PATCH 20/51] flip flag values to their proper names so that trackselector parameters can be useful --- .../android/exoplayer2/extractor/mkv/MatroskaExtractor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 1049554f7a..82e4a6ff46 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -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; From d49c5a476db87ae38533fd18d9cd5d49b777453f Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Jul 2018 01:52:35 -0700 Subject: [PATCH 21/51] Tweak DefaultTrackSelector documentation Viewport constraints apply to adaptive content even if the actual playback isn't adaptive (i.e. because the selector ends up making a fixed track selection). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203726831 --- .../trackselection/DefaultTrackSelector.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 9000ace46d..31b79cdd3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -608,20 +608,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean exceedVideoConstraintsIfNecessary; /** - * Viewport width in pixels. Constrains video track selections for adaptive playbacks so that - * only tracks suitable for the viewport are selected. The default value is {@link - * Integer#MAX_VALUE} (i.e. no constraint). + * Viewport width in pixels. Constrains video track selections for adaptive content so that only + * tracks suitable for the viewport are selected. The default value is {@link Integer#MAX_VALUE} + * (i.e. no constraint). */ public final int viewportWidth; /** - * Viewport height in pixels. Constrains video track selections for adaptive playbacks so that + * Viewport height in pixels. Constrains video track selections for adaptive content so that * only tracks suitable for the viewport are selected. The default value is {@link * Integer#MAX_VALUE} (i.e. no constraint). */ public final int viewportHeight; /** * Whether the viewport orientation may change during playback. Constrains video track - * selections for adaptive playbacks so that only tracks suitable for the viewport are selected. + * selections for adaptive content so that only tracks suitable for the viewport are selected. * The default value is {@code true}. */ public final boolean viewportOrientationMayChange; From 522adc3772e8a1397941fffc054885b92d7f2ef3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 9 Jul 2018 05:44:39 -0700 Subject: [PATCH 22/51] Restructure track selection in DashMediaPeriod. Until now, the streams were released and re-enabled for each type of stream (primary, event, embedded) in that order. That leads to problems when replacing streams from one type to another (for example embedded to primary). This change restructures the track selection to: 1. Release and reset all streams that need to be released or replaced. 1(a). Including embedded orphan streams. 2. Select new streams. Issue:#4477 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203751233 --- RELEASENOTES.md | 2 + .../source/dash/DashMediaPeriod.java | 255 ++++++++++-------- 2 files changed, 139 insertions(+), 118 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 70fd33db6d..86ddada1f0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,8 @@ * Add workaround for track index mismatches between tfhd and tkhd boxes in fragmented MP4 files ([#4083](https://github.com/google/ExoPlayer/issues/4083)). +* Fix issue when switching track selection from an embedded track to a primary + track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)). ### 2.8.2 ### diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index f80ff89fc1..dd41db29e9 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source.dash; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Pair; -import android.util.SparseArray; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -186,126 +185,34 @@ import java.util.List; @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - SparseArray> primarySampleStreams = new SparseArray<>(); - List eventSampleStreamList = new ArrayList<>(); + int[] streamIndexToTrackGroupIndex = getStreamIndexToTrackGroupIndex(selections); + releaseDisabledStreams(selections, mayRetainStreamFlags, streams); + releaseOrphanEmbeddedStreams(selections, streams, streamIndexToTrackGroupIndex); + selectNewStreams( + selections, streams, streamResetFlags, positionUs, streamIndexToTrackGroupIndex); - selectPrimarySampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs, primarySampleStreams); - selectEventSampleStreams(selections, mayRetainStreamFlags, streams, - streamResetFlags, eventSampleStreamList); - selectEmbeddedSampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs, primarySampleStreams); - - sampleStreams = newSampleStreamArray(primarySampleStreams.size()); - for (int i = 0; i < sampleStreams.length; i++) { - sampleStreams[i] = primarySampleStreams.valueAt(i); + ArrayList> sampleStreamList = new ArrayList<>(); + ArrayList eventSampleStreamList = new ArrayList<>(); + for (SampleStream sampleStream : sampleStreams) { + if (sampleStream instanceof ChunkSampleStream) { + @SuppressWarnings("unchecked") + ChunkSampleStream stream = + (ChunkSampleStream) sampleStream; + sampleStreamList.add(stream); + } else if (sampleStream instanceof EventSampleStream) { + eventSampleStreamList.add((EventSampleStream) sampleStream); + } } + sampleStreams = newSampleStreamArray(sampleStreamList.size()); + sampleStreamList.toArray(sampleStreams); eventSampleStreams = new EventSampleStream[eventSampleStreamList.size()]; eventSampleStreamList.toArray(eventSampleStreams); + compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); return positionUs; } - private void selectPrimarySampleStreams( - TrackSelection[] selections, - boolean[] mayRetainStreamFlags, - SampleStream[] streams, - boolean[] streamResetFlags, - long positionUs, - SparseArray> primarySampleStreams) { - for (int i = 0; i < selections.length; i++) { - if (streams[i] instanceof ChunkSampleStream) { - @SuppressWarnings("unchecked") - ChunkSampleStream stream = (ChunkSampleStream) streams[i]; - if (selections[i] == null || !mayRetainStreamFlags[i]) { - stream.release(this); - streams[i] = null; - } else { - int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - primarySampleStreams.put(trackGroupIndex, stream); - } - } - - if (streams[i] == null && selections[i] != null) { - int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; - if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) { - ChunkSampleStream stream = buildSampleStream(trackGroupInfo, - selections[i], positionUs); - primarySampleStreams.put(trackGroupIndex, stream); - streams[i] = stream; - streamResetFlags[i] = true; - } - } - } - } - - private void selectEventSampleStreams(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, - List eventSampleStreamsList) { - for (int i = 0; i < selections.length; i++) { - if (streams[i] instanceof EventSampleStream) { - EventSampleStream stream = (EventSampleStream) streams[i]; - if (selections[i] == null || !mayRetainStreamFlags[i]) { - streams[i] = null; - } else { - eventSampleStreamsList.add(stream); - } - } - - if (streams[i] == null && selections[i] != null) { - int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; - if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) { - EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex); - Format format = selections[i].getTrackGroup().getFormat(0); - EventSampleStream stream = new EventSampleStream(eventStream, format, manifest.dynamic); - streams[i] = stream; - streamResetFlags[i] = true; - eventSampleStreamsList.add(stream); - } - } - } - } - - private void selectEmbeddedSampleStreams( - TrackSelection[] selections, - boolean[] mayRetainStreamFlags, - SampleStream[] streams, - boolean[] streamResetFlags, - long positionUs, - SparseArray> primarySampleStreams) { - for (int i = 0; i < selections.length; i++) { - if ((streams[i] instanceof EmbeddedSampleStream || streams[i] instanceof EmptySampleStream) - && (selections[i] == null || !mayRetainStreamFlags[i])) { - // The stream is for an embedded track and is either no longer selected or needs replacing. - releaseIfEmbeddedSampleStream(streams[i]); - streams[i] = null; - } - // We need to consider replacing the stream even if it's non-null because the primary stream - // may have been replaced, selected or deselected. - if (selections[i] != null) { - int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; - if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) { - ChunkSampleStream primaryStream = primarySampleStreams.get( - trackGroupInfo.primaryTrackGroupIndex); - SampleStream stream = streams[i]; - boolean mayRetainStream = primaryStream == null ? stream instanceof EmptySampleStream - : (stream instanceof EmbeddedSampleStream - && ((EmbeddedSampleStream) stream).parent == primaryStream); - if (!mayRetainStream) { - releaseIfEmbeddedSampleStream(stream); - streams[i] = primaryStream == null ? new EmptySampleStream() - : primaryStream.selectEmbeddedTrack(positionUs, trackGroupInfo.trackType); - streamResetFlags[i] = true; - } - } - } - } - } - @Override public void discardBuffer(long positionUs, boolean toKeyframe) { for (ChunkSampleStream sampleStream : sampleStreams) { @@ -372,6 +279,124 @@ import java.util.List; // Internal methods. + private int[] getStreamIndexToTrackGroupIndex(TrackSelection[] selections) { + int[] streamIndexToTrackGroupIndex = new int[selections.length]; + for (int i = 0; i < selections.length; i++) { + if (selections[i] != null) { + streamIndexToTrackGroupIndex[i] = trackGroups.indexOf(selections[i].getTrackGroup()); + } else { + streamIndexToTrackGroupIndex[i] = C.INDEX_UNSET; + } + } + return streamIndexToTrackGroupIndex; + } + + private void releaseDisabledStreams( + TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams) { + for (int i = 0; i < selections.length; i++) { + if (selections[i] == null || !mayRetainStreamFlags[i]) { + if (streams[i] instanceof ChunkSampleStream) { + @SuppressWarnings("unchecked") + ChunkSampleStream stream = + (ChunkSampleStream) streams[i]; + stream.release(this); + } else if (streams[i] instanceof EmbeddedSampleStream) { + ((EmbeddedSampleStream) streams[i]).release(); + } + streams[i] = null; + } + } + } + + private void releaseOrphanEmbeddedStreams( + TrackSelection[] selections, SampleStream[] streams, int[] streamIndexToTrackGroupIndex) { + for (int i = 0; i < selections.length; i++) { + if (streams[i] instanceof EmptySampleStream || streams[i] instanceof EmbeddedSampleStream) { + // We need to release an embedded stream if the corresponding primary stream is released. + int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex); + boolean mayRetainStream; + if (primaryStreamIndex == C.INDEX_UNSET) { + // If the corresponding primary stream is not selected, we may retain an existing + // EmptySampleStream. + mayRetainStream = streams[i] instanceof EmptySampleStream; + } else { + // If the corresponding primary stream is selected, we may retain the embedded stream if + // the stream's parent still matches. + mayRetainStream = + (streams[i] instanceof EmbeddedSampleStream) + && ((EmbeddedSampleStream) streams[i]).parent == streams[primaryStreamIndex]; + } + if (!mayRetainStream) { + if (streams[i] instanceof EmbeddedSampleStream) { + ((EmbeddedSampleStream) streams[i]).release(); + } + streams[i] = null; + } + } + } + } + + private void selectNewStreams( + TrackSelection[] selections, + SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs, + int[] streamIndexToTrackGroupIndex) { + // Create newly selected primary and event streams. + for (int i = 0; i < selections.length; i++) { + if (streams[i] == null && selections[i] != null) { + streamResetFlags[i] = true; + int trackGroupIndex = streamIndexToTrackGroupIndex[i]; + TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; + if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) { + streams[i] = buildSampleStream(trackGroupInfo, selections[i], positionUs); + } else if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) { + EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex); + Format format = selections[i].getTrackGroup().getFormat(0); + streams[i] = new EventSampleStream(eventStream, format, manifest.dynamic); + } + } + } + // Create newly selected embedded streams from the corresponding primary stream. Note that this + // second pass is needed because the primary stream may not have been created yet in a first + // pass if the index of the primary stream is greater than the index of the embedded stream. + for (int i = 0; i < selections.length; i++) { + if (streams[i] == null && selections[i] != null) { + int trackGroupIndex = streamIndexToTrackGroupIndex[i]; + TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; + if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) { + int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex); + if (primaryStreamIndex == C.INDEX_UNSET) { + // If an embedded track is selected without the corresponding primary track, create an + // empty sample stream instead. + streams[i] = new EmptySampleStream(); + } else { + streams[i] = + ((ChunkSampleStream) streams[primaryStreamIndex]) + .selectEmbeddedTrack(positionUs, trackGroupInfo.trackType); + } + } + } + } + } + + private int getPrimaryStreamIndex(int embeddedStreamIndex, int[] streamIndexToTrackGroupIndex) { + int embeddedTrackGroupIndex = streamIndexToTrackGroupIndex[embeddedStreamIndex]; + if (embeddedTrackGroupIndex == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + int primaryTrackGroupIndex = trackGroupInfos[embeddedTrackGroupIndex].primaryTrackGroupIndex; + for (int i = 0; i < streamIndexToTrackGroupIndex.length; i++) { + int trackGroupIndex = streamIndexToTrackGroupIndex[i]; + if (trackGroupIndex == primaryTrackGroupIndex + && trackGroupInfos[trackGroupIndex].trackGroupCategory + == TrackGroupInfo.CATEGORY_PRIMARY) { + return i; + } + } + return C.INDEX_UNSET; + } + private static Pair buildTrackGroups( List adaptationSets, List eventStreams) { int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets); @@ -624,12 +649,6 @@ import java.util.List; return new ChunkSampleStream[length]; } - private static void releaseIfEmbeddedSampleStream(SampleStream sampleStream) { - if (sampleStream instanceof EmbeddedSampleStream) { - ((EmbeddedSampleStream) sampleStream).release(); - } - } - private static final class TrackGroupInfo { @Retention(RetentionPolicy.SOURCE) From 7e9e29d8a87557acdbf5fe52c5445761c48af900 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 9 Jul 2018 08:07:39 -0700 Subject: [PATCH 23/51] Fix wrong loop variable in DashMediaPeriod. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203766579 --- .../google/android/exoplayer2/source/dash/DashMediaPeriod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index dd41db29e9..ddb655fa92 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -193,7 +193,7 @@ import java.util.List; ArrayList> sampleStreamList = new ArrayList<>(); ArrayList eventSampleStreamList = new ArrayList<>(); - for (SampleStream sampleStream : sampleStreams) { + for (SampleStream sampleStream : streams) { if (sampleStream instanceof ChunkSampleStream) { @SuppressWarnings("unchecked") ChunkSampleStream stream = From 44c45fd18db8345d389ed3335aaffd69b658dcf0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Jul 2018 08:17:36 -0700 Subject: [PATCH 24/51] Move subsampleOffset in Format It's no longer text specific (it's used for metadata as well, and in theory could apply to any stream in which samples contain multiple sub-samples) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203767825 --- .../com/google/android/exoplayer2/Format.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 61d416da09..1614e53164 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -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. /** From 2f6273c9fcc2fcafe49a6404ab658042e13dfe17 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 10 Jul 2018 05:00:34 -0700 Subject: [PATCH 25/51] Fix DownloadService doesn't stop when the app is killed Also fixed showing "remove notification" when download is completed. Issue:#4469 Issue:#4488 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203927268 --- .../exoplayer2/offline/DownloadService.java | 21 ++++++++++++++++-- .../ui/DownloadNotificationUtil.java | 22 ++++++++++++++----- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index 6dae3f70b3..995e71a94a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -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: @@ -260,6 +266,12 @@ public abstract class DownloadService extends Service { return START_STICKY; } + @Override + public void onTaskRemoved(Intent rootIntent) { + logd("onTaskRemoved rootIntent: " + rootIntent); + taskRemoved = true; + } + @Override public void onDestroy() { logd("onDestroy"); @@ -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) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java index 0a841fa38f..97832abfc7 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java @@ -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); From 0a46e74105eb51f3499ec544260e25a2ac2f6848 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 12 Jul 2018 01:47:22 -0700 Subject: [PATCH 26/51] Ignore all edit lists if one track's edits can't be applied Issue: #4348 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=204261718 --- RELEASENOTES.md | 2 + .../exoplayer2/extractor/mp4/AtomParsers.java | 47 ++++++++------ .../extractor/mp4/Mp4Extractor.java | 65 ++++++++++++++----- .../extractor/mp4/TrackSampleTable.java | 28 +++----- 4 files changed, 86 insertions(+), 56 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 86ddada1f0..09c920ddda 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,8 @@ * 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)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index a2b787d6b0..d11914919a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -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, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index e70a49a2d7..1b455ab9e2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -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 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 getTrackSampleTables( + ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists) + throws ParserException { + ArrayList 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. *

diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java index 9f77c49664..56851fc1e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java @@ -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; From d24e7cdffe53c545c8125d0aa226602313321577 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 13 Jul 2018 07:08:13 -0700 Subject: [PATCH 27/51] Fix a bug with TTML using font size as % of cellResolution. After [] we support default font size for TTML, relative to the cellResolution of the document. However, this introduced a bug that makes TTML font-size in such case always follow the cellResolution font size, even when SubtitleView.setApplyEmbeddedStyles(false) and SubtitleView.setApplyEmbeddedFontSizes(false) were used. This CL updates the fix so that the default font-size using cellResolution works in the same way as other embedded styles, and can be turned off using setters from SubtitleView. GitHub: #4491 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=204467033 --- RELEASENOTES.md | 8 ++- .../exoplayer2/ui/SubtitlePainter.java | 61 +++++++++++++------ .../android/exoplayer2/ui/SubtitleView.java | 13 ++-- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 09c920ddda..1829d4a956 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,13 +2,17 @@ ### 2.8.3 ### +* 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)). * DRM: * Allow DrmInitData to carry a license server URL ([#3393](https://github.com/google/ExoPlayer/issues/3393)). -* CEA-608: Improve handling of embedded styles - ([#4321](https://github.com/google/ExoPlayer/issues/4321)). * IMA: Fix behavior when creating/releasing the player then releasing `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). * Fix issue playing DRM protected streams on Asus Zenfone 2 diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index c5d264b310..3ad3e9d496 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -89,7 +89,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 +131,8 @@ import com.google.android.exoplayer2.util.Util; /** * Draws the provided {@link Cue} into a canvas with the specified styling. - *

- * A call to this method is able to use cached results of calculations made during the previous + * + *

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 +141,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 +151,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 +192,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 +222,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 +242,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 +254,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 +270,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; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index bb9c38d886..a3db02bee5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -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( From 0b8724cb668b7c09008dffcef0652f7ff0456b3d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 13 Jul 2018 07:13:46 -0700 Subject: [PATCH 28/51] Fix bug where sourceId wasn't set for the first chunk The sample queues haven't been created when the first init call occurs. In this case we need to set sourceId when we subsequently create the queues. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=204467538 --- .../android/exoplayer2/source/hls/HlsSampleStreamWrapper.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 705320bdad..e0b236fb4a 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -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); From 9bb64b7e44dee50baf8eed7ee444575aa6ddaf87 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 20 Jul 2018 03:01:16 -0700 Subject: [PATCH 29/51] Add support for setting companion ad slots ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205373398 --- RELEASENOTES.md | 6 ++++-- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1829d4a956..1249be7acb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,8 +13,10 @@ * DRM: * Allow DrmInitData to carry a license server URL ([#3393](https://github.com/google/ExoPlayer/issues/3393)). -* IMA: Fix behavior when creating/releasing the player then releasing - `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). +* 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. * 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 diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 9045ee777a..cce45f2d2c 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -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; @@ -395,6 +397,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adsLoader; } + /** + * 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 companionSlots) { + adDisplayContainer.setCompanionSlots(companionSlots); + } + /** * Requests ads, if they have not already been requested. Must be called on the main thread. * From e03623f7012cd25075fd90d01dad4504fac684cf Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 20 Jul 2018 03:25:11 -0700 Subject: [PATCH 30/51] Fix issue with keeping window sequence number after repeated seeks. The number is shelved in calls to queue.clear() to keep it for the next media period. However, the queue may also become empty by repeated calls to advancePlayingPeriod which may happen when seeking to an unprepared period. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205376036 --- .../android/exoplayer2/MediaPeriodQueue.java | 4 +- .../android/exoplayer2/ExoPlayerTest.java | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index 717f873622..17a8ddd8d4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -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; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index c05f8914f5..559a915b2b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -1980,6 +1980,44 @@ public final class ExoPlayerTest { .inOrder(); } + @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 From 7d4ac516e72233ebee63bc57fa4529c8b76c54d1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 23 Jul 2018 03:12:22 -0700 Subject: [PATCH 31/51] Update period index in DashMediaPeriod event dispatcher after manifest update. Issue:#4492 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205636634 --- .../android/exoplayer2/source/dash/DashMediaPeriod.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index ddb655fa92..e217c244b5 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -61,7 +61,6 @@ import java.util.List; /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; private final int minLoadableRetryCount; - private final EventDispatcher eventDispatcher; private final long elapsedRealtimeOffset; private final LoaderErrorThrower manifestLoaderErrorThrower; private final Allocator allocator; @@ -72,6 +71,7 @@ import java.util.List; private final IdentityHashMap, PlayerTrackEmsgHandler> trackEmsgHandlerBySampleStream; + private EventDispatcher eventDispatcher; private @Nullable Callback callback; private ChunkSampleStream[] sampleStreams; private EventSampleStream[] eventSampleStreams; @@ -126,6 +126,13 @@ import java.util.List; */ public void updateManifest(DashManifest manifest, int periodIndex) { this.manifest = manifest; + if (this.periodIndex != periodIndex) { + eventDispatcher = + eventDispatcher.withParameters( + /* windowIndex= */ 0, + eventDispatcher.mediaPeriodId.copyWithPeriodIndex(periodIndex), + manifest.getPeriod(periodIndex).startMs); + } this.periodIndex = periodIndex; playerEmsgHandler.updateManifest(manifest); if (sampleStreams != null) { From 50c8197004884196701b83d987a5c5af451c6296 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 23 Jul 2018 14:03:59 +0100 Subject: [PATCH 32/51] Widen setOutputSurface workaround --- .../video/MediaCodecVideoRenderer.java | 204 ++++++++++++++---- 1 file changed, 167 insertions(+), 37 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 5e8a98ea68..916c311217 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -84,6 +84,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // pending output streams that have fewer frames than the codec latency. private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10; + private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround; + private static boolean deviceNeedsSetOutputSurfaceWorkaround; + private final Context context; private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; private final EventDispatcher eventDispatcher; @@ -825,11 +828,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private boolean shouldUseDummySurface(MediaCodecInfo codecInfo) { return Util.SDK_INT >= 23 && !tunneling - && !codecNeedsDummySurfaceWorkaround(codecInfo.name) && !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name) && (!codecInfo.secure || DummySurface.isSecureSupported(context)); } - + private void setJoiningDeadlineMs() { joiningDeadlineMs = allowedJoiningTimeMs > 0 ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; @@ -1172,20 +1174,36 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); } - private static boolean codecNeedsDummySurfaceWorkaround(String name) { - // Work around https://github.com/google/ExoPlayer/issues/4419. - return (("needle".equals(Util.DEVICE)) // FireTV 4K - && "OMX.amlogic.avc.decoder.awesome".equals(name)); - } - - /** - * Returns whether the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} - * incorrectly. - *

- * If true is returned then we fall back to releasing and re-instantiating the codec instead. + /* + * TODO: + * + * 1. Validate that Android device certification now ensures correct behavior, and add a + * corresponding SDK_INT upper bound for applying the workaround (probably SDK_INT < 26). + * 2. Determine a complete list of affected devices. + * 3. Some of the devices in this list only fail to support setOutputSurface when switching from + * a SurfaceView provided Surface to a Surface of another type (e.g. TextureView/DummySurface), + * and vice versa. One hypothesis is that setOutputSurface fails when the surfaces have + * different pixel formats. If we can find a way to query the Surface instances to determine + * whether this case applies, then we'll be able to provide a more targeted workaround. */ - private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { - // Work around https://github.com/google/ExoPlayer/issues/3236, + /** + * Returns whether the codec is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + * + *

If true is returned then we fall back to releasing and re-instantiating the codec instead. + * + * @param name The name of the codec. + * @return True if the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + */ + protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) { + if (Util.SDK_INT >= 27 || name.startsWith("OMX.google")) { + // Devices running API level 27 or later should also be unaffected. Google OMX decoders are + // not known to have this issue on any API level. + return false; + } + // Work around: + // https://github.com/google/ExoPlayer/issues/3236, // https://github.com/google/ExoPlayer/issues/3355, // https://github.com/google/ExoPlayer/issues/3439, // https://github.com/google/ExoPlayer/issues/3724, @@ -1194,28 +1212,140 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // https://github.com/google/ExoPlayer/issues/4084, // https://github.com/google/ExoPlayer/issues/4104, // https://github.com/google/ExoPlayer/issues/4134, - // https://github.com/google/ExoPlayer/issues/4315. - return (("deb".equals(Util.DEVICE) // Nexus 7 (2013) - || "flo".equals(Util.DEVICE) // Nexus 7 (2013) - || "mido".equals(Util.DEVICE) // Redmi Note 4 - || "santoni".equals(Util.DEVICE)) // Redmi 4X - && "OMX.qcom.video.decoder.avc".equals(name)) - || (("tcl_eu".equals(Util.DEVICE) // TCL Percee TV - || "SVP-DTV15".equals(Util.DEVICE) // Sony Bravia 4K 2015 - || "BRAVIA_ATV2".equals(Util.DEVICE) // Sony Bravia 4K GB - || Util.DEVICE.startsWith("panell_") // Motorola Moto C Plus - || "F3311".equals(Util.DEVICE) // Sony Xperia E5 - || "M5c".equals(Util.DEVICE) // Meizu M5C - || "QM16XE_U".equals(Util.DEVICE) // Philips QM163E - || "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note - || "woods_f".equals(Util.MODEL) // Moto E (4) - || "watson".equals(Util.DEVICE)) // Moto C - && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)) - || (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite - || "CAM-L21".equals(Util.MODEL)) // Huawei Y6II - && "OMX.k3.video.decoder.avc".equals(name)) - || (("HUAWEI VNS-L21".equals(Util.MODEL)) // Huawei P9 Lite - && "OMX.IMG.MSVDX.Decoder.AVC".equals(name)); + // https://github.com/google/ExoPlayer/issues/4315, + // https://github.com/google/ExoPlayer/issues/4419, + // https://github.com/google/ExoPlayer/issues/4460, + // https://github.com/google/ExoPlayer/issues/4468. + synchronized (MediaCodecVideoRenderer.class) { + if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) { + switch (Util.DEVICE) { + case "1601": + case "1713": + case "1714": + case "A10-70F": + case "A1601": + case "A2016a40": + case "A7000-a": + case "A7000plus": + case "A7010a48": + case "A7020a48": + case "AquaPowerM": + case "Aura_Note_2": + case "BLACK-1X": + case "BRAVIA_ATV2": + case "C1": + case "ComioS1": + case "CP8676_I02": + case "CPH1609": + case "CPY83_I00": + case "cv1": + case "cv3": + case "deb": + case "E5643": + case "ELUGA_A3_Pro": + case "ELUGA_Note": + case "ELUGA_Prim": + case "ELUGA_Ray_X": + case "EverStar_S": + case "F3111": + case "F3113": + case "F3116": + case "F3211": + case "F3213": + case "F3215": + case "F3311": + case "flo": + case "GiONEE_CBL7513": + case "GiONEE_GBL7319": + case "GIONEE_GBL7360": + case "GIONEE_SWW1609": + case "GIONEE_SWW1627": + case "GIONEE_SWW1631": + case "GIONEE_WBL5708": + case "GIONEE_WBL7365": + case "GIONEE_WBL7519": + case "griffin": + case "htc_e56ml_dtul": + case "hwALE-H": + case "HWBLN-H": + case "HWCAM-H": + case "HWVNS-H": + case "iball8735_9806": + case "Infinix-X572": + case "iris60": + case "itel_S41": + case "j2xlteins": + case "JGZ": + case "K50a40": + case "le_x6": + case "LS-5017": + case "M5c": + case "manning": + case "marino_f": + case "MEIZU_M5": + case "mh": + case "mido": + case "MX6": + case "namath": + case "nicklaus_f": + case "NX541J": + case "NX573J": + case "OnePlus5T": + case "p212": + case "P681": + case "P85": + case "panell_d": + case "panell_dl": + case "panell_ds": + case "panell_dt": + case "PB2-670M": + case "PGN528": + case "PGN610": + case "PGN611": + case "Phantom6": + case "Pixi4-7_3G": + case "Pixi5-10_4G": + case "PLE": + case "Q350": + case "Q4260": + case "Q427": + case "Q4310": + case "Q5": + case "QM16XE_U": + case "QX1": + case "santoni": + case "Slate_Pro": + case "SVP-DTV15": + case "s905x018": + case "taido_row": + case "TB3-730F": + case "TB3-730X": + case "TB3-850F": + case "TB3-850M": + case "tcl_eu": + case "V1": + case "V23GB": + case "V5": + case "vernee_M5": + case "watson": + case "whyred": + case "woods_f": + case "woods_fn": + case "X3_HK": + case "XE2X": + case "XT1663": + case "Z12_PRO": + case "Z80": + deviceNeedsSetOutputSurfaceWorkaround = true; + break; + default: + // Workaround not required. + break; + } + evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true; + } + } + return deviceNeedsSetOutputSurfaceWorkaround; } protected static final class CodecMaxValues { From dcd549bd3e689e54d953c8b49cbd1b25bb7127b4 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 23 Jul 2018 14:06:29 +0100 Subject: [PATCH 33/51] Fix release branch --- .../android/exoplayer2/trackselection/DefaultTrackSelector.java | 2 +- .../java/com/google/android/exoplayer2/ui/SubtitlePainter.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 31b79cdd3f..0067ffa552 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -661,7 +661,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private Parameters() { this( - /* selectionOverrides= */ new SparseArray<>(), + /* selectionOverrides= */ new SparseArray>(), /* rendererDisabledFlags= */ new SparseBooleanArray(), /* preferredAudioLanguage= */ null, /* preferredTextLanguage= */ null, diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 3ad3e9d496..687cd78004 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -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; From 224d0c2b2c68a3d9001fa7f8ce37c4d79c4d66b6 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 18 Jun 2018 06:44:23 -0700 Subject: [PATCH 34/51] Add isControllerVisible Issue: #4385 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=200986828 --- RELEASENOTES.md | 2 ++ .../java/com/google/android/exoplayer2/ui/PlayerView.java | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1249be7acb..dd26b721c6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -31,6 +31,8 @@ ([#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)). +* Add `PlayerView.isControllerVisible` + ([#4385](https://github.com/google/ExoPlayer/issues/4385)). ### 2.8.2 ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index a7aa48c0db..a7688bc540 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -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. * From f39d28135dd872b4de88d5acb6378714eed5791f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 20 Jun 2018 01:38:42 -0700 Subject: [PATCH 35/51] Pass through all ID3 internal data from udta Also switch from using a CommentFrame to a new InternalFrame type for ID3 data stored with ID '----', to distinguish internal data from actual ID3 comments. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201315254 --- RELEASENOTES.md | 2 + .../extractor/GaplessInfoHolder.java | 20 ++-- .../extractor/mp4/MetadataUtil.java | 6 +- .../metadata/id3/InternalFrame.java | 96 +++++++++++++++++++ 4 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dd26b721c6..f4f032b7fc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -33,6 +33,8 @@ track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)). * Add `PlayerView.isControllerVisible` ([#4385](https://github.com/google/ExoPlayer/issues/4385)). +* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using + CommentFrame to InternalFrame for frames with gapless metadata in MP4. ### 2.8.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 75d8b4cf2d..54d48350fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -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 { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index fed1694925..991f765d0d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java new file mode 100644 index 0000000000..a828d80069 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java @@ -0,0 +1,96 @@ +/* + * 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 CREATOR = + new Creator() { + + @Override + public InternalFrame createFromParcel(Parcel in) { + return new InternalFrame(in); + } + + @Override + public InternalFrame[] newArray(int size) { + return new InternalFrame[size]; + } + }; +} From b0f44029b7554efc8f3c9f37f787c09b11dbc208 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 23 Jul 2018 14:44:42 +0100 Subject: [PATCH 36/51] Clean up release notes --- RELEASENOTES.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f4f032b7fc..a917bc5d38 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,10 @@ ### 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. @@ -10,13 +14,12 @@ ([#4321](https://github.com/google/ExoPlayer/issues/4321)). * DASH: Exclude text streams from duration calculations ([#4029](https://github.com/google/ExoPlayer/issues/4029)). -* DRM: - * Allow DrmInitData to carry a license server URL - ([#3393](https://github.com/google/ExoPlayer/issues/3393)). -* 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. +* DRM: Allow DrmInitData to carry a license server URL + ([#3393](https://github.com/google/ExoPlayer/issues/3393)). +* 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 @@ -31,10 +34,7 @@ ([#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)). -* Add `PlayerView.isControllerVisible` - ([#4385](https://github.com/google/ExoPlayer/issues/4385)). -* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using - CommentFrame to InternalFrame for frames with gapless metadata in MP4. +* Improved compatibility with FireOS devices. ### 2.8.2 ### From a3de7bf39920603fed9526a4c01ea7e83dcd3bcf Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 23 Jul 2018 07:51:13 -0700 Subject: [PATCH 37/51] Update release notes + bump version ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205660355 --- constants.gradle | 4 ++-- .../com/google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/constants.gradle b/constants.gradle index 5544173a3c..b8cfc7d2a4 100644 --- a/constants.gradle +++ b/constants.gradle @@ -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 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 172eb19da3..8de3385d1a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -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} From a3e3f64b7951117d259945ea90e0207526b35a9c Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 24 Jul 2018 06:47:38 -0700 Subject: [PATCH 38/51] Add PRO7S to surface switch workaround Issue: #4468 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205821059 --- .../google/android/exoplayer2/video/MediaCodecVideoRenderer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 916c311217..126087c04b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1306,6 +1306,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { case "Pixi4-7_3G": case "Pixi5-10_4G": case "PLE": + case "PRO7S": case "Q350": case "Q4260": case "Q427": From ed18be4eea1edc7cc23a05d447768373385299f7 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 1 Aug 2018 04:32:11 -0700 Subject: [PATCH 39/51] Add missing Nullable annotation Player.EventListener.onTimelineChanged Issue: #4593 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=206911927 --- .../android/exoplayer2/castdemo/PlayerManager.java | 3 ++- .../google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 4 ++-- .../exoplayer2/ext/leanback/LeanbackPlayerAdapter.java | 4 ++-- .../ext/mediasession/MediaSessionConnector.java | 4 ++-- .../main/java/com/google/android/exoplayer2/Player.java | 9 +++++---- .../android/exoplayer2/analytics/AnalyticsCollector.java | 2 +- .../com/google/android/exoplayer2/ExoPlayerTest.java | 2 +- .../google/android/exoplayer2/ui/PlayerControlView.java | 2 +- .../android/exoplayer2/ui/PlayerNotificationManager.java | 2 +- .../com/google/android/exoplayer2/testutil/Action.java | 4 +++- .../android/exoplayer2/testutil/ExoPlayerTestRunner.java | 4 ++-- 11 files changed, 22 insertions(+), 18 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 63b18b0aa7..6c8d7a294f 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -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; diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index cce45f2d2c..dc2df9eb71 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -795,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; diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 03f53c263f..a279874ec1 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -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); diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 4bafaa4326..039115174c 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 328816d709..8501f5b827 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -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. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 8f4267efce..16c1c5d170 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -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) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 559a915b2b..44f6f2aa82 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -2078,7 +2078,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); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index 63c791d166..d673f3675b 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -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(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 19051ba932..e64d03e0bf 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -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; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index a6c3438a52..1d9ccf4b50 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -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); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index cf7470b80a..101f2c4817 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -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); From 5faa662e438d35066dd4afe77514fdeb6efbf1d1 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 1 Aug 2018 05:56:04 -0700 Subject: [PATCH 40/51] Apply setOutputSurfaceWorkaround to required FireOS devices Amazon like to use Device.MODEL, so key on that instead for these workarounds. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=206917935 --- .../exoplayer2/video/MediaCodecVideoRenderer.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 126087c04b..3c3f2ecac1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1340,7 +1340,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { deviceNeedsSetOutputSurfaceWorkaround = true; break; default: - // Workaround not required. + // Do nothing. + break; + } + switch (Util.MODEL) { + case "AFTA": + case "AFTN": + deviceNeedsSetOutputSurfaceWorkaround = true; + break; + default: + // Do nothing. break; } evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true; From 00891f6469a8f23e58209451ae6ae4d0adfd8062 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 25 Jul 2018 07:07:53 -0700 Subject: [PATCH 41/51] Don't exclude .idea from MOE input ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205988261 --- .idea/codeStyleSettings.xml | 496 ++++++++++++++++++++++++++++++++++++ 1 file changed, 496 insertions(+) create mode 100644 .idea/codeStyleSettings.xml diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000000..c89e6fd000 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,496 @@ + + + +

+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .*:.*Style + + http://schemas.android.com/apk/res/android + + + BY_NAME + +
+
+ + + + .*:layout_width + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:layout_height + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:layout_weight + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:layout_margin + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:layout_marginTop + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:layout_marginBottom + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:layout_marginStart + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:layout_marginEnd + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:layout_marginLeft + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:layout_marginRight + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:layout_.* + + http://schemas.android.com/apk/res/android + + + BY_NAME + +
+
+ + + + .*:padding + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:paddingTop + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:paddingBottom + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:paddingStart + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:paddingEnd + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:paddingLeft + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:paddingRight + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .* + http://schemas.android.com/apk/res/android + + + BY_NAME + +
+
+ + + + .* + http://schemas.android.com/apk/res-auto + + + BY_NAME + +
+
+ + + + .* + http://schemas.android.com/tools + + + BY_NAME + +
+
+ + + + .* + .* + + + BY_NAME + +
+ + + + + +