From 913dcb3e1dafc1f9340e5592df099ee460cb5c26 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 4 Jun 2019 14:20:51 +0100 Subject: [PATCH 01/24] Display last frame when seeking to end of stream. We currently don't display the last frame because the seek time is behind the last frame's timestamps and it's thus marked as decodeOnly. This case can be detected by checking whether all data sent to the codec is marked as decodeOnly at the time we read the end of stream signal. If so, we can re-enable the last frame. This should work for almost all cases because the end-of-stream signal is read in the same feedInputBuffer loop as the last frame and we therefore haven't released the last frame buffer yet. Issue:#2568 PiperOrigin-RevId: 251425870 --- RELEASENOTES.md | 5 +++ .../audio/MediaCodecAudioRenderer.java | 5 ++- .../mediacodec/MediaCodecRenderer.java | 42 ++++++++++++++----- .../source/ProgressiveMediaPeriod.java | 2 +- .../video/MediaCodecVideoRenderer.java | 25 ++++++----- .../testutil/DebugRenderersFactory.java | 8 ++-- 6 files changed, 61 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 246a968256..b20a278713 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,10 @@ # Release notes # +### 2.10.3 ### + +* Display last frame when seeking to end of stream + ([#2568](https://github.com/google/ExoPlayer/issues/2568)). + ### 2.10.2 ### * Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index d43bd6cbf8..ace7ebbcc6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -695,7 +695,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException { if (codecNeedsEosBufferTimestampWorkaround @@ -711,7 +712,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return true; } - if (shouldSkip) { + if (isDecodeOnlyBuffer) { codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.skippedOutputBufferCount++; audioSink.handleDiscontinuity(); 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 5f7f5d60b7..c3072a1590 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 @@ -328,14 +328,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private int inputIndex; private int outputIndex; private ByteBuffer outputBuffer; - private boolean shouldSkipOutputBuffer; + private boolean isDecodeOnlyOutputBuffer; + private boolean isLastOutputBuffer; private boolean codecReconfigured; @ReconfigurationState private int codecReconfigurationState; @DrainState private int codecDrainState; @DrainAction private int codecDrainAction; private boolean codecReceivedBuffers; private boolean codecReceivedEos; - + private long lastBufferInStreamPresentationTimeUs; + private long largestQueuedPresentationTimeUs; private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; @@ -598,6 +600,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { waitingForKeys = false; codecHotswapDeadlineMs = C.TIME_UNSET; decodeOnlyPresentationTimestamps.clear(); + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; try { if (codec != null) { decoderCounters.decoderReleaseCount++; @@ -704,10 +708,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { waitingForFirstSyncSample = true; codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; - shouldSkipOutputBuffer = false; + isDecodeOnlyOutputBuffer = false; + isLastOutputBuffer = false; waitingForKeys = false; decodeOnlyPresentationTimestamps.clear(); + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; codecDrainState = DRAIN_STATE_NONE; codecDrainAction = DRAIN_ACTION_NONE; // Reconfiguration data sent shortly before the flush may not have been processed by the @@ -881,7 +888,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecDrainAction = DRAIN_ACTION_NONE; codecNeedsAdaptationWorkaroundBuffer = false; shouldSkipAdaptationWorkaroundOutputBuffer = false; - shouldSkipOutputBuffer = false; + isDecodeOnlyOutputBuffer = false; + isLastOutputBuffer = false; waitingForFirstSyncSample = true; decoderCounters.decoderInitCount++; @@ -1016,6 +1024,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { result = readSource(formatHolder, buffer, false); } + if (hasReadStreamToEnd()) { + // Notify output queue of the last buffer's timestamp. + lastBufferInStreamPresentationTimeUs = largestQueuedPresentationTimeUs; + } + if (result == C.RESULT_NOTHING_READ) { return false; } @@ -1088,6 +1101,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { formatQueue.add(presentationTimeUs, inputFormat); waitingForFirstSampleInFormat = false; } + largestQueuedPresentationTimeUs = + Math.max(largestQueuedPresentationTimeUs, presentationTimeUs); buffer.flip(); onQueueInputBuffer(buffer); @@ -1458,7 +1473,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputBuffer.position(outputBufferInfo.offset); outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size); } - shouldSkipOutputBuffer = shouldSkipOutputBuffer(outputBufferInfo.presentationTimeUs); + isDecodeOnlyOutputBuffer = isDecodeOnlyBuffer(outputBufferInfo.presentationTimeUs); + isLastOutputBuffer = + lastBufferInStreamPresentationTimeUs == outputBufferInfo.presentationTimeUs; updateOutputFormatForTime(outputBufferInfo.presentationTimeUs); } @@ -1474,7 +1491,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, outputFormat); } catch (IllegalStateException e) { processEndOfStream(); @@ -1494,7 +1512,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs, - shouldSkipOutputBuffer, + isDecodeOnlyOutputBuffer, + isLastOutputBuffer, outputFormat); } @@ -1561,7 +1580,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @param bufferIndex The index of the output buffer. * @param bufferFlags The flags attached to the output buffer. * @param bufferPresentationTimeUs The presentation time of the output buffer in microseconds. - * @param shouldSkip Whether the buffer should be skipped (i.e. not rendered). + * @param isDecodeOnlyBuffer Whether the buffer was marked with {@link C#BUFFER_FLAG_DECODE_ONLY} + * by the source. + * @param isLastBuffer Whether the buffer is the last sample of the current stream. * @param format The format associated with the buffer. * @return Whether the output buffer was fully processed (e.g. rendered or skipped). * @throws ExoPlaybackException If an error occurs processing the output buffer. @@ -1574,7 +1595,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException; @@ -1654,7 +1676,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecDrainAction = DRAIN_ACTION_NONE; } - private boolean shouldSkipOutputBuffer(long presentationTimeUs) { + private boolean isDecodeOnlyBuffer(long presentationTimeUs) { // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would // box presentationTimeUs, creating a Long object that would need to be garbage collected. int size = decodeOnlyPresentationTimestamps.size(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index d9f0008a7f..4dafa0ba76 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -733,7 +733,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (prepared) { SeekMap seekMap = getPreparedState().seekMap; Assertions.checkState(isPendingReset()); - if (durationUs != C.TIME_UNSET && pendingResetPositionUs >= durationUs) { + if (durationUs != C.TIME_UNSET && pendingResetPositionUs > durationUs) { loadingFinished = true; pendingResetPositionUs = C.TIME_UNSET; return; 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 e75a3866b6..8d5b890c7f 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 @@ -679,7 +679,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException { if (initialPositionUs == C.TIME_UNSET) { @@ -688,7 +689,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs; - if (shouldSkip) { + if (isDecodeOnlyBuffer && !isLastBuffer) { skipOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } @@ -736,10 +737,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs); earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; - if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs) + if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastBuffer) && maybeDropBuffersToKeyframe(codec, bufferIndex, presentationTimeUs, positionUs)) { return false; - } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { + } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastBuffer)) { dropOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } @@ -807,8 +808,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { /** * Returns the offset that should be subtracted from {@code bufferPresentationTimeUs} in {@link - * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, Format)} to - * get the playback position with respect to the media. + * #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, boolean, + * Format)} to get the playback position with respect to the media. */ protected long getOutputStreamOffsetUs() { return outputStreamOffsetUs; @@ -860,9 +861,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * indicates that the buffer is late. * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, * measured at the start of the current iteration of the rendering loop. + * @param isLastBuffer Whether the buffer is the last buffer in the current stream. */ - protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { - return isBufferLate(earlyUs); + protected boolean shouldDropOutputBuffer( + long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { + return isBufferLate(earlyUs) && !isLastBuffer; } /** @@ -873,9 +876,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * negative value indicates that the buffer is late. * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, * measured at the start of the current iteration of the rendering loop. + * @param isLastBuffer Whether the buffer is the last buffer in the current stream. */ - protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) { - return isBufferVeryLate(earlyUs); + protected boolean shouldDropBuffersToKeyframe( + long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { + return isBufferVeryLate(earlyUs) && !isLastBuffer; } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 9feaf6863a..8b11a89d8d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -163,14 +163,15 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip, + boolean isDecodeOnlyBuffer, + boolean isLastBuffer, Format format) throws ExoPlaybackException { if (skipToPositionBeforeRenderingFirstFrame && bufferPresentationTimeUs < positionUs) { // After the codec has been initialized, don't render the first frame until we've caught up // to the playback position. Else test runs on devices that do not support dummy surface // will drop frames between rendering the first one and catching up [Internal: b/66494991]. - shouldSkip = true; + isDecodeOnlyBuffer = true; } return super.processOutputBuffer( positionUs, @@ -180,7 +181,8 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { bufferIndex, bufferFlags, bufferPresentationTimeUs, - shouldSkip, + isDecodeOnlyBuffer, + isLastBuffer, format); } From 1c7bb2899ccaaf26855700bf2c9d890812b092f5 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 21 Jun 2019 18:22:40 +0100 Subject: [PATCH 02/24] Merge pull request #6055 from xirac:dev-v2 PiperOrigin-RevId: 254182080 --- RELEASENOTES.md | 1 + .../manifest/SsManifestParser.java | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b20a278713..b441e6e995 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,7 @@ * Display last frame when seeking to end of stream ([#2568](https://github.com/google/ExoPlayer/issues/2568)). +* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. ### 2.10.2 ### diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java index 66731660f5..39e22f2982 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java @@ -586,6 +586,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { } else { subType = parser.getAttributeValue(null, KEY_SUB_TYPE); } + putNormalizedAttribute(KEY_SUB_TYPE, subType); name = parser.getAttributeValue(null, KEY_NAME); url = parseRequiredString(parser, KEY_URL); maxWidth = parseInt(parser, KEY_MAX_WIDTH, Format.NO_VALUE); @@ -645,6 +646,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { private static final String KEY_CHANNELS = "Channels"; private static final String KEY_FOUR_CC = "FourCC"; private static final String KEY_TYPE = "Type"; + private static final String KEY_SUB_TYPE = "Subtype"; private static final String KEY_LANGUAGE = "Language"; private static final String KEY_NAME = "Name"; private static final String KEY_MAX_WIDTH = "MaxWidth"; @@ -709,6 +711,18 @@ public class SsManifestParser implements ParsingLoadable.Parser { /* roleFlags= */ 0, language); } else if (type == C.TRACK_TYPE_TEXT) { + String subType = (String) getNormalizedAttribute(KEY_SUB_TYPE); + @C.RoleFlags int roleFlags = 0; + switch (subType) { + case "CAPT": + roleFlags = C.ROLE_FLAG_CAPTION; + break; + case "DESC": + roleFlags = C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND; + break; + default: + break; + } String language = (String) getNormalizedAttribute(KEY_LANGUAGE); format = Format.createTextContainerFormat( @@ -719,7 +733,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { /* codecs= */ null, bitrate, /* selectionFlags= */ 0, - /* roleFlags= */ 0, + roleFlags, language); } else { format = From feefaacb31561c0272b038905fd4d030bdd695c2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 18 Jun 2019 11:31:54 +0100 Subject: [PATCH 03/24] Gracefully handle revoked ACCESS_NETWORK_STATE permission. This permission has normal access right and can't be revoked by the user. However, an app can choose to revoke it when using ExoPlayer, e.g. if no network is required and the app doesn't want to list this permission. Support this use case by gracefully catching the exception in the relevant places. Issue:#6019 PiperOrigin-RevId: 253759332 --- RELEASENOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b441e6e995..5dde993ae0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,8 @@ * Display last frame when seeking to end of stream ([#2568](https://github.com/google/ExoPlayer/issues/2568)). * SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. +* Gracefully handle revoked `ACCESS_NETWORK_STATE` permission + ([#6019](https://github.com/google/ExoPlayer/issues/6019)). ### 2.10.2 ### From 18f1b06a688dbfe6980f49b8358af7458f0d8b47 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 17 Jun 2019 09:56:50 +0100 Subject: [PATCH 04/24] Fix all FIXME comments. These are mostly nullability issues. PiperOrigin-RevId: 253537068 --- .../android/exoplayer2/drm/DefaultDrmSessionManager.java | 2 +- .../java/com/google/android/exoplayer2/drm/ExoMediaDrm.java | 3 ++- .../google/android/exoplayer2/drm/FrameworkMediaDrm.java | 5 +---- .../android/exoplayer2/scheduler/PlatformScheduler.java | 6 +++--- 4 files changed, 7 insertions(+), 9 deletions(-) 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 3820836e49..fb684f627f 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 @@ -544,7 +544,7 @@ public class DefaultDrmSessionManager implements DrmSe @Override public void onEvent( ExoMediaDrm md, - byte[] sessionId, + @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 49915f3af5..6bd8d9688f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -80,7 +80,7 @@ public interface ExoMediaDrm { */ void onEvent( ExoMediaDrm mediaDrm, - byte[] sessionId, + @Nullable byte[] sessionId, int event, int extra, @Nullable byte[] data); @@ -215,6 +215,7 @@ public interface ExoMediaDrm { throws NotProvisionedException; /** @see MediaDrm#provideKeyResponse(byte[], byte[]) */ + @Nullable byte[] provideKeyResponse(byte[] scope, byte[] response) throws NotProvisionedException, DeniedByServerException; 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 615aa0e7b1..2cb7e66a2c 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 @@ -84,8 +84,6 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) { @@ -160,8 +158,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm Date: Tue, 2 Jul 2019 18:04:08 +0900 Subject: [PATCH 05/24] Update TrackSelectionDialog.java Fix a super tiny typo. --- .../google/android/exoplayer2/demo/TrackSelectionDialog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java index a7dd1a0df8..bc409410c3 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java @@ -306,7 +306,7 @@ public final class TrackSelectionDialog extends DialogFragment { } } - /** Fragment to show a track seleciton in tab of the track selection dialog. */ + /** Fragment to show a track selection in tab of the track selection dialog. */ public static final class TrackSelectionViewFragment extends Fragment implements TrackSelectionView.TrackSelectionListener { From d57a8587d2b379555786eae63ba514931fd3c583 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 26 Jun 2019 15:16:22 +0100 Subject: [PATCH 06/24] Add threading model note to hello-word page Also add layer of indirection between code and the guide, to make moving content easier going forward. PiperOrigin-RevId: 255182216 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 697f35e417..f1b6b5bc90 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -1231,8 +1231,7 @@ public class SimpleExoPlayer extends BasePlayer Log.w( TAG, "Player is accessed on the wrong thread. See " - + "https://exoplayer.dev/troubleshooting.html#" - + "what-do-player-is-accessed-on-the-wrong-thread-warnings-mean", + + "https://exoplayer.dev/issues/player-accessed-on-wrong-thread", hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException()); hasNotifiedFullWrongThreadWarning = true; } From cdf70edf6e2228300f9d4974912266008bee866f Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 27 Jun 2019 13:26:47 +0100 Subject: [PATCH 07/24] Remove unnecessary FileDescriptor sync PiperOrigin-RevId: 255380796 --- .../exoplayer2/upstream/cache/CacheDataSink.java | 16 ---------------- .../upstream/cache/CacheDataSinkFactory.java | 16 +--------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 2caf4c92f8..566c928b20 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -49,7 +49,6 @@ public final class CacheDataSink implements DataSink { private final long fragmentSize; private final int bufferSize; - private boolean syncFileDescriptor; private DataSpec dataSpec; private long dataSpecFragmentSize; private File file; @@ -108,18 +107,6 @@ public final class CacheDataSink implements DataSink { this.cache = Assertions.checkNotNull(cache); this.fragmentSize = fragmentSize == C.LENGTH_UNSET ? Long.MAX_VALUE : fragmentSize; this.bufferSize = bufferSize; - syncFileDescriptor = true; - } - - /** - * Sets whether file descriptors are synced when closing output streams. - * - *

This method is experimental, and will be renamed or removed in a future release. - * - * @param syncFileDescriptor Whether file descriptors are synced when closing output streams. - */ - public void experimental_setSyncFileDescriptor(boolean syncFileDescriptor) { - this.syncFileDescriptor = syncFileDescriptor; } @Override @@ -208,9 +195,6 @@ public final class CacheDataSink implements DataSink { boolean success = false; try { outputStream.flush(); - if (syncFileDescriptor) { - underlyingFileOutputStream.getFD().sync(); - } success = true; } finally { Util.closeQuietly(outputStream); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java index 856e9db168..ce9735badd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java @@ -26,8 +26,6 @@ public final class CacheDataSinkFactory implements DataSink.Factory { private final long fragmentSize; private final int bufferSize; - private boolean syncFileDescriptor; - /** @see CacheDataSink#CacheDataSink(Cache, long) */ public CacheDataSinkFactory(Cache cache, long fragmentSize) { this(cache, fragmentSize, CacheDataSink.DEFAULT_BUFFER_SIZE); @@ -40,20 +38,8 @@ public final class CacheDataSinkFactory implements DataSink.Factory { this.bufferSize = bufferSize; } - /** - * See {@link CacheDataSink#experimental_setSyncFileDescriptor(boolean)}. - * - *

This method is experimental, and will be renamed or removed in a future release. - */ - public CacheDataSinkFactory experimental_setSyncFileDescriptor(boolean syncFileDescriptor) { - this.syncFileDescriptor = syncFileDescriptor; - return this; - } - @Override public DataSink createDataSink() { - CacheDataSink dataSink = new CacheDataSink(cache, fragmentSize, bufferSize); - dataSink.experimental_setSyncFileDescriptor(syncFileDescriptor); - return dataSink; + return new CacheDataSink(cache, fragmentSize, bufferSize); } } From d3e7ea89d94ada524a52e3444213887222855218 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 27 Jun 2019 21:51:41 +0100 Subject: [PATCH 08/24] call setPlayWhenReady in any case ISSUE: #6093 PiperOrigin-RevId: 255471282 --- .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 c0b5fd67f6..3341100e51 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 @@ -1066,8 +1066,9 @@ public final class MediaSessionConnector { } } else if (player.getPlaybackState() == Player.STATE_ENDED) { controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); - controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true); } + controlDispatcher.dispatchSetPlayWhenReady( + Assertions.checkNotNull(player), /* playWhenReady= */ true); } } From 5365272b609b5c18ddaf5532621d9e25f704b60b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 28 Jun 2019 13:21:43 +0100 Subject: [PATCH 09/24] Use the floor of the frame rate for capability checks PiperOrigin-RevId: 255584000 --- .../exoplayer2/mediacodec/MediaCodecInfo.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 08ba94f257..2158f182b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -518,9 +518,15 @@ public final class MediaCodecInfo { @TargetApi(21) private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width, int height, double frameRate) { - return frameRate == Format.NO_VALUE || frameRate <= 0 - ? capabilities.isSizeSupported(width, height) - : capabilities.areSizeAndRateSupported(width, height, frameRate); + if (frameRate == Format.NO_VALUE || frameRate <= 0) { + return capabilities.isSizeSupported(width, height); + } else { + // The signaled frame rate may be slightly higher than the actual frame rate, so we take the + // floor to avoid situations where a range check in areSizeAndRateSupported fails due to + // slightly exceeding the limits for a standard format (e.g., 1080p at 30 fps). + double floorFrameRate = Math.floor(frameRate); + return capabilities.areSizeAndRateSupported(width, height, floorFrameRate); + } } @TargetApi(23) From 71e0f2e81cedbb927526a587b6dbcf6fb3f54b72 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 1 Jul 2019 13:03:07 +0100 Subject: [PATCH 10/24] Don't consume touch events if no controller is attached. Issue:#6109 PiperOrigin-RevId: 255933121 --- .../com/google/android/exoplayer2/ui/PlayerView.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 c7ffda8ae5..e6bc1a6a71 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 @@ -1050,6 +1050,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onTouchEvent(MotionEvent event) { + if (!useController || player == null) { + return false; + } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isTouching = true; @@ -1150,9 +1153,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider // Internal methods. private boolean toggleControllerVisibility() { - if (!useController || player == null) { - return false; - } if (!controller.isVisible()) { maybeShowController(true); } else if (controllerHideOnTouch) { @@ -1472,6 +1472,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Override public boolean onSingleTapUp(MotionEvent e) { + if (!useController || player == null) { + return false; + } return toggleControllerVisibility(); } } From c4c7f4bafa4cfa91e1ca2c10ee0674e60ac9f728 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 1 Jul 2019 19:13:03 +0100 Subject: [PATCH 11/24] MediaSessionConnector: Document how to provide metadata asynchronously Issue: #6047 PiperOrigin-RevId: 255992898 --- .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 3341100e51..7e72904078 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 @@ -377,6 +377,13 @@ public final class MediaSessionConnector { /** * Gets the {@link MediaMetadataCompat} to be published to the session. * + *

An app may need to load metadata resources like artwork bitmaps asynchronously. In such a + * case the app should return a {@link MediaMetadataCompat} object that does not contain these + * resources as a placeholder. The app should start an asynchronous operation to download the + * bitmap and put it into a cache. Finally, the app should call {@link + * #invalidateMediaSessionMetadata()}. This causes this callback to be called again and the app + * can now return a {@link MediaMetadataCompat} object with all the resources included. + * * @param player The player connected to the media session. * @return The {@link MediaMetadataCompat} to be published to the session. */ From 3d5d23778abc9cdc1bc94da6930be6feacad5815 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jul 2019 13:42:05 +0100 Subject: [PATCH 12/24] FLV extractor fixes 1. Only output video starting from a keyframe 2. When calculating the timestamp offset to adjust live streams to start at t=0, use the timestamp of the first tag from which a sample is actually output, rather than just the first audio/video tag. The test streams in the referenced GitHub issue start with a video tag whose packet type is AVC_PACKET_TYPE_SEQUENCE_HEADER (i.e. does not contain a sample) and whose timestamp is set to 0 (i.e. isn't set). The timestamp is set correctly on tags that from which a sample is actually output. Issue: #6111 PiperOrigin-RevId: 256147747 --- RELEASENOTES.md | 2 ++ .../extractor/flv/AudioTagPayloadReader.java | 8 ++++-- .../extractor/flv/FlvExtractor.java | 26 ++++++++++++------- .../extractor/flv/ScriptTagPayloadReader.java | 7 ++--- .../extractor/flv/TagPayloadReader.java | 16 ++++++------ .../extractor/flv/VideoTagPayloadReader.java | 18 ++++++++++--- 6 files changed, 51 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5dde993ae0..29f4d94946 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,8 @@ * SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. * Gracefully handle revoked `ACCESS_NETWORK_STATE` permission ([#6019](https://github.com/google/ExoPlayer/issues/6019)). +* FLV: Fix bug that caused playback of some live streams to not start + ([#6111](https://github.com/google/ExoPlayer/issues/6111)). ### 2.10.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index ec5ad88aeb..b10f2bf80b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -86,11 +86,12 @@ import java.util.Collections; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { if (audioFormat == AUDIO_FORMAT_MP3) { int sampleSize = data.bytesLeft(); output.sampleData(data, sampleSize); output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + return true; } else { int packetType = data.readUnsignedByte(); if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { @@ -104,12 +105,15 @@ import java.util.Collections; Collections.singletonList(audioSpecificConfig), null, 0, null); output.format(format); hasOutputFormat = true; + return false; } else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) { int sampleSize = data.bytesLeft(); output.sampleData(data, sampleSize); output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); + return true; + } else { + return false; } } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index 0a2c0c46f6..de6ed2710a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -74,6 +74,7 @@ public final class FlvExtractor implements Extractor { private ExtractorOutput extractorOutput; private @States int state; + private boolean outputFirstSample; private long mediaTagTimestampOffsetUs; private int bytesToNextTagHeader; private int tagType; @@ -90,7 +91,6 @@ public final class FlvExtractor implements Extractor { tagData = new ParsableByteArray(); metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; - mediaTagTimestampOffsetUs = C.TIME_UNSET; } @Override @@ -132,7 +132,7 @@ public final class FlvExtractor implements Extractor { @Override public void seek(long position, long timeUs) { state = STATE_READING_FLV_HEADER; - mediaTagTimestampOffsetUs = C.TIME_UNSET; + outputFirstSample = false; bytesToNextTagHeader = 0; } @@ -253,14 +253,16 @@ public final class FlvExtractor implements Extractor { */ private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; + boolean wasSampleOutput = false; + long timestampUs = getCurrentTimestampUs(); if (tagType == TAG_TYPE_AUDIO && audioReader != null) { ensureReadyForMediaOutput(); - audioReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); + wasSampleOutput = audioReader.consume(prepareTagData(input), timestampUs); } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { ensureReadyForMediaOutput(); - videoReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); + wasSampleOutput = videoReader.consume(prepareTagData(input), timestampUs); } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { - metadataReader.consume(prepareTagData(input), tagTimestampUs); + wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs); long durationUs = metadataReader.getDurationUs(); if (durationUs != C.TIME_UNSET) { extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); @@ -270,6 +272,11 @@ public final class FlvExtractor implements Extractor { input.skipFully(tagDataSize); wasConsumed = false; } + if (!outputFirstSample && wasSampleOutput) { + outputFirstSample = true; + mediaTagTimestampOffsetUs = + metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0; + } bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header. state = STATE_SKIPPING_TO_TAG_HEADER; return wasConsumed; @@ -292,10 +299,11 @@ public final class FlvExtractor implements Extractor { extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); outputSeekMap = true; } - if (mediaTagTimestampOffsetUs == C.TIME_UNSET) { - mediaTagTimestampOffsetUs = - metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0; - } } + private long getCurrentTimestampUs() { + return outputFirstSample + ? (mediaTagTimestampOffsetUs + tagTimestampUs) + : (metadataReader.getDurationUs() == C.TIME_UNSET ? 0 : tagTimestampUs); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java index 2dec85ffcc..eb1cc8f336 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java @@ -63,7 +63,7 @@ import java.util.Map; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { int nameType = readAmfType(data); if (nameType != AMF_TYPE_STRING) { // Should never happen. @@ -72,12 +72,12 @@ import java.util.Map; String name = readAmfString(data); if (!NAME_METADATA.equals(name)) { // We're only interested in metadata. - return; + return false; } int type = readAmfType(data); if (type != AMF_TYPE_ECMA_ARRAY) { // We're not interested in this metadata. - return; + return false; } // Set the duration to the value contained in the metadata, if present. Map metadata = readAmfEcmaArray(data); @@ -87,6 +87,7 @@ import java.util.Map; durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND); } } + return false; } private static int readAmfType(ParsableByteArray data) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java index e8652d653f..48914b7c2c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java @@ -58,12 +58,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; * * @param data The payload data to consume. * @param timeUs The timestamp associated with the payload. + * @return Whether a sample was output. * @throws ParserException If an error occurs parsing the data. */ - public final void consume(ParsableByteArray data, long timeUs) throws ParserException { - if (parseHeader(data)) { - parsePayload(data, timeUs); - } + public final boolean consume(ParsableByteArray data, long timeUs) throws ParserException { + return parseHeader(data) && parsePayload(data, timeUs); } /** @@ -78,10 +77,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Parses tag payload. * - * @param data Buffer where tag payload is stored - * @param timeUs Time position of the frame + * @param data Buffer where tag payload is stored. + * @param timeUs Time position of the frame. + * @return Whether a sample was output. * @throws ParserException If an error occurs parsing the payload. */ - protected abstract void parsePayload(ParsableByteArray data, long timeUs) throws ParserException; - + protected abstract boolean parsePayload(ParsableByteArray data, long timeUs) + throws ParserException; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java index 92db91e20b..5ddaafb4a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java @@ -47,6 +47,7 @@ import com.google.android.exoplayer2.video.AvcConfig; // State variables. private boolean hasOutputFormat; + private boolean hasOutputKeyframe; private int frameType; /** @@ -60,7 +61,7 @@ import com.google.android.exoplayer2.video.AvcConfig; @Override public void seek() { - // Do nothing. + hasOutputKeyframe = false; } @Override @@ -77,7 +78,7 @@ import com.google.android.exoplayer2.video.AvcConfig; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException { int packetType = data.readUnsignedByte(); int compositionTimeMs = data.readInt24(); @@ -94,7 +95,12 @@ import com.google.android.exoplayer2.video.AvcConfig; avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null); output.format(format); hasOutputFormat = true; + return false; } else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) { + boolean isKeyframe = frameType == VIDEO_FRAME_KEYFRAME; + if (!hasOutputKeyframe && !isKeyframe) { + return false; + } // TODO: Deduplicate with Mp4Extractor. // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case // they're only 1 or 2 bytes long. @@ -123,8 +129,12 @@ import com.google.android.exoplayer2.video.AvcConfig; output.sampleData(data, bytesToWrite); bytesWritten += bytesToWrite; } - output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.BUFFER_FLAG_KEY_FRAME : 0, - bytesWritten, 0, null); + output.sampleMetadata( + timeUs, isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0, bytesWritten, 0, null); + hasOutputKeyframe = true; + return true; + } else { + return false; } } From 91728d41de971a1d7b43fa280172968dded8e6df Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 2 Jul 2019 17:49:45 +0100 Subject: [PATCH 13/24] Merge pull request #5908 from sr1990:dev-v2 PiperOrigin-RevId: 256147805 --- .../dash/manifest/DashManifestParser.java | 57 ++++++++++++++++--- .../source/dash/manifest/SegmentBase.java | 18 +++++- 2 files changed, 64 insertions(+), 11 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 c4f61a73cd..f03a443431 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 @@ -42,6 +42,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.regex.Matcher; @@ -242,7 +243,7 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { segmentBase = parseSegmentList(xpp, null); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, null); + segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList()); } else { maybeSkipTag(xpp); } @@ -323,6 +324,7 @@ public class DashManifestParser extends DefaultHandler language, roleDescriptors, accessibilityDescriptors, + supplementalProperties, segmentBase); contentType = checkContentTypeConsistency(contentType, getContentType(representationInfo.format)); @@ -332,7 +334,8 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase); + segmentBase = + parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, supplementalProperties); } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp)) { @@ -492,6 +495,7 @@ public class DashManifestParser extends DefaultHandler String adaptationSetLanguage, List adaptationSetRoleDescriptors, List adaptationSetAccessibilityDescriptors, + List adaptationSetSupplementalProperties, SegmentBase segmentBase) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); @@ -524,7 +528,9 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase); + segmentBase = + parseSegmentTemplate( + xpp, (SegmentTemplate) segmentBase, adaptationSetSupplementalProperties); } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { Pair contentProtection = parseContentProtection(xpp); if (contentProtection.first != null) { @@ -763,13 +769,19 @@ public class DashManifestParser extends DefaultHandler startNumber, duration, timeline, segments); } - protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, SegmentTemplate parent) + protected SegmentTemplate parseSegmentTemplate( + XmlPullParser xpp, + SegmentTemplate parent, + List adaptationSetSupplementalProperties) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", parent != null ? parent.presentationTimeOffset : 0); long duration = parseLong(xpp, "duration", parent != null ? parent.duration : C.TIME_UNSET); long startNumber = parseLong(xpp, "startNumber", parent != null ? parent.startNumber : 1); + long endNumber = + parseLastSegmentNumberSupplementalProperty(adaptationSetSupplementalProperties); + UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media", parent != null ? parent.mediaTemplate : null); UrlTemplate initializationTemplate = parseUrlTemplate(xpp, "initialization", @@ -794,8 +806,16 @@ public class DashManifestParser extends DefaultHandler timeline = timeline != null ? timeline : parent.segmentTimeline; } - return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, - startNumber, duration, timeline, initializationTemplate, mediaTemplate); + return buildSegmentTemplate( + initialization, + timescale, + presentationTimeOffset, + startNumber, + endNumber, + duration, + timeline, + initializationTemplate, + mediaTemplate); } protected SegmentTemplate buildSegmentTemplate( @@ -803,12 +823,21 @@ public class DashManifestParser extends DefaultHandler long timescale, long presentationTimeOffset, long startNumber, + long endNumber, long duration, List timeline, UrlTemplate initializationTemplate, UrlTemplate mediaTemplate) { - return new SegmentTemplate(initialization, timescale, presentationTimeOffset, - startNumber, duration, timeline, initializationTemplate, mediaTemplate); + return new SegmentTemplate( + initialization, + timescale, + presentationTimeOffset, + startNumber, + endNumber, + duration, + timeline, + initializationTemplate, + mediaTemplate); } /** @@ -1445,6 +1474,18 @@ public class DashManifestParser extends DefaultHandler } } + protected static long parseLastSegmentNumberSupplementalProperty( + List supplementalProperties) { + for (int i = 0; i < supplementalProperties.size(); i++) { + Descriptor descriptor = supplementalProperties.get(i); + if ("http://dashif.org/guidelines/last-segment-number" + .equalsIgnoreCase(descriptor.schemeIdUri)) { + return Long.parseLong(descriptor.value); + } + } + return C.INDEX_UNSET; + } + /** A parsed Representation element. */ protected static final class RepresentationInfo { diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index f033232590..ba4faafd95 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -277,6 +277,7 @@ public abstract class SegmentBase { /* package */ final UrlTemplate initializationTemplate; /* package */ final UrlTemplate mediaTemplate; + /* package */ final long endNumber; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -286,6 +287,9 @@ public abstract class SegmentBase { * @param presentationTimeOffset The presentation time offset. The value in seconds is the * division of this value and {@code timescale}. * @param startNumber The sequence number of the first segment. + * @param endNumber The sequence number of the last segment as specified by the + * SupplementalProperty with schemeIdUri="http://dashif.org/guidelines/last-segment-number", + * or {@link C#INDEX_UNSET}. * @param duration The duration of each segment in the case of fixed duration segments. The * value in seconds is the division of this value and {@code timescale}. If {@code * segmentTimeline} is non-null then this parameter is ignored. @@ -302,14 +306,21 @@ public abstract class SegmentBase { long timescale, long presentationTimeOffset, long startNumber, + long endNumber, long duration, List segmentTimeline, UrlTemplate initializationTemplate, UrlTemplate mediaTemplate) { - super(initialization, timescale, presentationTimeOffset, startNumber, - duration, segmentTimeline); + super( + initialization, + timescale, + presentationTimeOffset, + startNumber, + duration, + segmentTimeline); this.initializationTemplate = initializationTemplate; this.mediaTemplate = mediaTemplate; + this.endNumber = endNumber; } @Override @@ -340,6 +351,8 @@ public abstract class SegmentBase { public int getSegmentCount(long periodDurationUs) { if (segmentTimeline != null) { return segmentTimeline.size(); + } else if (endNumber != C.INDEX_UNSET) { + return (int) (endNumber - startNumber + 1); } else if (periodDurationUs != C.TIME_UNSET) { long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; return (int) Util.ceilDivide(periodDurationUs, durationUs); @@ -347,7 +360,6 @@ public abstract class SegmentBase { return DashSegmentIndex.INDEX_UNBOUNDED; } } - } /** From 1538e5d9661f5d20b756784d9ba4692768692b0d Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 5 Jul 2019 16:20:17 +0100 Subject: [PATCH 14/24] CEA608: no-op readability clean-up PiperOrigin-RevId: 256676196 --- .../exoplayer2/text/cea/Cea608Decoder.java | 87 +++++++++++-------- 1 file changed, 49 insertions(+), 38 deletions(-) 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 774b94a43c..9d4b914d76 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 @@ -387,45 +387,27 @@ public final class Cea608Decoder extends CeaDecoder { continue; } - // Special North American character set. - // ccData1 - 0|0|0|1|C|0|0|1 - // ccData2 - 0|0|1|1|X|X|X|X - if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) { - if (getChannel(ccData1) == selectedChannel) { + if (!updateAndVerifyCurrentChannel(ccData1)) { + // Wrong channel. + continue; + } + + if (isCtrlCode(ccData1)) { + if (isSpecialChar(ccData1, ccData2)) { + // Special North American character. currentCueBuilder.append(getSpecialChar(ccData2)); - } - continue; - } - - // Extended Western European character set. - // ccData1 - 0|0|0|1|C|0|1|S - // ccData2 - 0|0|1|X|X|X|X|X - if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) { - if (getChannel(ccData1) == selectedChannel) { - // Remove standard equivalent of the special extended char before appending new one + } else if (isExtendedWestEuropeanChar(ccData1, ccData2)) { + // Extended West European character. + // Remove standard equivalent of the special extended char before appending new one. currentCueBuilder.backspace(); - if ((ccData1 & 0x01) == 0x00) { - // Extended Spanish/Miscellaneous and French character set (S = 0). - currentCueBuilder.append(getExtendedEsFrChar(ccData2)); - } else { - // Extended Portuguese and German/Danish character set (S = 1). - currentCueBuilder.append(getExtendedPtDeChar(ccData2)); - } + currentCueBuilder.append(getExtendedWestEuropeanChar(ccData1, ccData2)); + } else { + // Non-character control code. + handleCtrl(ccData1, ccData2, repeatedControlPossible); } continue; } - // Control character. - // ccData1 - 0|0|0|X|X|X|X|X - if ((ccData1 & 0xE0) == 0x00) { - handleCtrl(ccData1, ccData2, repeatedControlPossible); - continue; - } - - if (currentChannel != selectedChannel) { - continue; - } - // Basic North American character set. currentCueBuilder.append(getChar(ccData1)); if ((ccData2 & 0xE0) != 0x00) { @@ -440,8 +422,14 @@ public final class Cea608Decoder extends CeaDecoder { } } + private boolean updateAndVerifyCurrentChannel(byte cc1) { + if (isCtrlCode(cc1)) { + currentChannel = getChannel(cc1); + } + return currentChannel == selectedChannel; + } + private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) { - currentChannel = getChannel(cc1); // Most control commands are sent twice in succession to ensure they are received properly. We // don't want to process duplicate commands, so if we see the same repeatable command twice in a // row then we ignore the second one. @@ -459,10 +447,6 @@ public final class Cea608Decoder extends CeaDecoder { } } - if (currentChannel != selectedChannel) { - return; - } - if (isMidrowCtrlCode(cc1, cc2)) { handleMidrowCtrl(cc2); } else if (isPreambleAddressCode(cc1, cc2)) { @@ -681,11 +665,33 @@ public final class Cea608Decoder extends CeaDecoder { return (char) BASIC_CHARACTER_SET[index]; } + private static boolean isSpecialChar(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|0|0|1 + // cc2 - 0|0|1|1|X|X|X|X + return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x30); + } + private static char getSpecialChar(byte ccData) { int index = ccData & 0x0F; return (char) SPECIAL_CHARACTER_SET[index]; } + private static boolean isExtendedWestEuropeanChar(byte cc1, byte cc2) { + // cc1 - 0|0|0|1|C|0|1|S + // cc2 - 0|0|1|X|X|X|X|X + return ((cc1 & 0xF6) == 0x12) && ((cc2 & 0xE0) == 0x20); + } + + private static char getExtendedWestEuropeanChar(byte cc1, byte cc2) { + if ((cc1 & 0x01) == 0x00) { + // Extended Spanish/Miscellaneous and French character set (S = 0). + return getExtendedEsFrChar(cc2); + } else { + // Extended Portuguese and German/Danish character set (S = 1). + return getExtendedPtDeChar(cc2); + } + } + private static char getExtendedEsFrChar(byte ccData) { int index = ccData & 0x1F; return (char) SPECIAL_ES_FR_CHARACTER_SET[index]; @@ -696,6 +702,11 @@ public final class Cea608Decoder extends CeaDecoder { return (char) SPECIAL_PT_DE_CHARACTER_SET[index]; } + private static boolean isCtrlCode(byte cc1) { + // cc1 - 0|0|0|X|X|X|X|X + return (cc1 & 0xE0) == 0x00; + } + private static int getChannel(byte cc1) { // cc1 - X|X|X|X|C|X|X|X return (cc1 >> 3) & 0x1; From d035f24e878f6106dbb71e414164836568e78ce0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 8 Jul 2019 17:24:18 +0100 Subject: [PATCH 15/24] CEA608: Fix repeated Special North American chars. We currently handle most the control code logic after handling special characters. This includes filtering out repeated control codes and checking for the correct channel. As the special character sets are control codes as well, these checks should happen before parsing the characters. Issue:#6133 PiperOrigin-RevId: 256993672 --- RELEASENOTES.md | 2 + .../exoplayer2/text/cea/Cea608Decoder.java | 91 +++++++++---------- 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 29f4d94946..1ef1dbb98c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,8 @@ ([#6019](https://github.com/google/ExoPlayer/issues/6019)). * FLV: Fix bug that caused playback of some live streams to not start ([#6111](https://github.com/google/ExoPlayer/issues/6111)). +* CEA608: Fix repetition of special North American characters + ([#6133](https://github.com/google/ExoPlayer/issues/6133)). ### 2.10.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 9d4b914d76..5a14063aa1 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 @@ -242,7 +242,7 @@ public final class Cea608Decoder extends CeaDecoder { private int captionMode; private int captionRowCount; - private boolean captionValid; + private boolean isCaptionValid; private boolean repeatableControlSet; private byte repeatableControlCc1; private byte repeatableControlCc2; @@ -300,7 +300,7 @@ public final class Cea608Decoder extends CeaDecoder { setCaptionMode(CC_MODE_UNKNOWN); setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT); resetCueBuilders(); - captionValid = false; + isCaptionValid = false; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -358,13 +358,19 @@ public final class Cea608Decoder extends CeaDecoder { continue; } - boolean repeatedControlPossible = repeatableControlSet; - repeatableControlSet = false; + boolean previousIsCaptionValid = isCaptionValid; + isCaptionValid = + (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG + && ODD_PARITY_BYTE_TABLE[ccByte1] + && ODD_PARITY_BYTE_TABLE[ccByte2]; - boolean previousCaptionValid = captionValid; - captionValid = (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG; - if (!captionValid) { - if (previousCaptionValid) { + if (isRepeatedCommand(isCaptionValid, ccData1, ccData2)) { + // Ignore repeated valid commands. + continue; + } + + if (!isCaptionValid) { + if (previousIsCaptionValid) { // The encoder has flipped the validity bit to indicate captions are being turned off. resetCueBuilders(); captionDataProcessed = true; @@ -372,15 +378,6 @@ public final class Cea608Decoder extends CeaDecoder { continue; } - // If we've reached this point then there is data to process; flag that work has been done. - captionDataProcessed = true; - - if (!ODD_PARITY_BYTE_TABLE[ccByte1] || !ODD_PARITY_BYTE_TABLE[ccByte2]) { - // The data is invalid. - resetCueBuilders(); - continue; - } - maybeUpdateIsInCaptionService(ccData1, ccData2); if (!isInCaptionService) { // Only the Captioning service is supported. Drop all other bytes. @@ -393,26 +390,29 @@ public final class Cea608Decoder extends CeaDecoder { } if (isCtrlCode(ccData1)) { - if (isSpecialChar(ccData1, ccData2)) { - // Special North American character. - currentCueBuilder.append(getSpecialChar(ccData2)); + if (isSpecialNorthAmericanChar(ccData1, ccData2)) { + currentCueBuilder.append(getSpecialNorthAmericanChar(ccData2)); } else if (isExtendedWestEuropeanChar(ccData1, ccData2)) { - // Extended West European character. // Remove standard equivalent of the special extended char before appending new one. currentCueBuilder.backspace(); currentCueBuilder.append(getExtendedWestEuropeanChar(ccData1, ccData2)); - } else { - // Non-character control code. - handleCtrl(ccData1, ccData2, repeatedControlPossible); + } else if (isMidrowCtrlCode(ccData1, ccData2)) { + handleMidrowCtrl(ccData2); + } else if (isPreambleAddressCode(ccData1, ccData2)) { + handlePreambleAddressCode(ccData1, ccData2); + } else if (isTabCtrlCode(ccData1, ccData2)) { + currentCueBuilder.tabOffset = ccData2 - 0x20; + } else if (isMiscCode(ccData1, ccData2)) { + handleMiscCode(ccData2); + } + } else { + // Basic North American character set. + currentCueBuilder.append(getBasicChar(ccData1)); + if ((ccData2 & 0xE0) != 0x00) { + currentCueBuilder.append(getBasicChar(ccData2)); } - continue; - } - - // Basic North American character set. - currentCueBuilder.append(getChar(ccData1)); - if ((ccData2 & 0xE0) != 0x00) { - currentCueBuilder.append(getChar(ccData2)); } + captionDataProcessed = true; } if (captionDataProcessed) { @@ -429,14 +429,15 @@ public final class Cea608Decoder extends CeaDecoder { return currentChannel == selectedChannel; } - private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) { + private boolean isRepeatedCommand(boolean captionValid, byte cc1, byte cc2) { // Most control commands are sent twice in succession to ensure they are received properly. We // don't want to process duplicate commands, so if we see the same repeatable command twice in a // row then we ignore the second one. - if (isRepeatable(cc1)) { - if (repeatedControlPossible && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) { + if (captionValid && isRepeatable(cc1)) { + if (repeatableControlSet && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) { // This is a repeated command, so we ignore it. - return; + repeatableControlSet = false; + return true; } else { // This is the first occurrence of a repeatable command. Set the repeatable control // variables so that we can recognize and ignore a duplicate (if there is one), and then @@ -445,17 +446,11 @@ public final class Cea608Decoder extends CeaDecoder { repeatableControlCc1 = cc1; repeatableControlCc2 = cc2; } + } else { + // This command is not repeatable. + repeatableControlSet = false; } - - if (isMidrowCtrlCode(cc1, cc2)) { - handleMidrowCtrl(cc2); - } else if (isPreambleAddressCode(cc1, cc2)) { - handlePreambleAddressCode(cc1, cc2); - } else if (isTabCtrlCode(cc1, cc2)) { - currentCueBuilder.tabOffset = cc2 - 0x20; - } else if (isMiscCode(cc1, cc2)) { - handleMiscCode(cc2); - } + return false; } private void handleMidrowCtrl(byte cc2) { @@ -660,18 +655,18 @@ public final class Cea608Decoder extends CeaDecoder { } } - private static char getChar(byte ccData) { + private static char getBasicChar(byte ccData) { int index = (ccData & 0x7F) - 0x20; return (char) BASIC_CHARACTER_SET[index]; } - private static boolean isSpecialChar(byte cc1, byte cc2) { + private static boolean isSpecialNorthAmericanChar(byte cc1, byte cc2) { // cc1 - 0|0|0|1|C|0|0|1 // cc2 - 0|0|1|1|X|X|X|X return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x30); } - private static char getSpecialChar(byte ccData) { + private static char getSpecialNorthAmericanChar(byte ccData) { int index = ccData & 0x0F; return (char) SPECIAL_CHARACTER_SET[index]; } From 6fd235f95a7393675a1ed63ebac1bbf2cec08a91 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 9 Jul 2019 08:50:01 +0100 Subject: [PATCH 16/24] Merge pull request #5732 from ToxicBakery:feature/add-license-to-pom PiperOrigin-RevId: 257138448 --- build.gradle | 1 + publish.gradle | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/build.gradle b/build.gradle index a0e8fcf20a..bc538ead68 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ allprojects { } buildDir = "${externalBuildDir}/${project.name}" } + group = 'com.google.android.exoplayer' } apply from: 'javadoc_combined.gradle' diff --git a/publish.gradle b/publish.gradle index 85cf87aa85..96ec3d2f10 100644 --- a/publish.gradle +++ b/publish.gradle @@ -23,6 +23,21 @@ if (project.ext.has("exoplayerPublishEnabled") groupId = 'com.google.android.exoplayer' website = 'https://github.com/google/ExoPlayer' } + + gradle.taskGraph.whenReady { taskGraph -> + project.tasks + .findAll { task -> task.name.contains("generatePomFileFor") } + .forEach { task -> + task.doLast { + task.outputs.files + .filter { File file -> + file.path.contains("publications") + && file.name.matches("^pom-.+\\.xml\$") + } + .forEach { File file -> addLicense(file) } + } + } + } } def getBintrayRepo() { @@ -30,3 +45,23 @@ def getBintrayRepo() { property('publicRepo').toBoolean() return publicRepo ? 'exoplayer' : 'exoplayer-test' } + +static void addLicense(File pom) { + def licenseNode = new Node(null, "license") + licenseNode.append( + new Node(null, "name", "The Apache Software License, Version 2.0")) + licenseNode.append( + new Node(null, "url", "http://www.apache.org/licenses/LICENSE-2.0.txt")) + licenseNode.append(new Node(null, "distribution", "repo")) + def licensesNode = new Node(null, "licenses") + licensesNode.append(licenseNode) + + def xml = new XmlParser().parse(pom) + xml.append(licensesNode) + + def writer = new PrintWriter(new FileWriter(pom)) + def printer = new XmlNodePrinter(writer) + printer.preserveWhitespace = true + printer.print(xml) + writer.close() +} From b3495dfe66da2b5c364df21f97f2a679f067aecf Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 9 Jul 2019 11:36:42 +0100 Subject: [PATCH 17/24] Update release notes --- RELEASENOTES.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1ef1dbb98c..c6d3faadc9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,13 +4,15 @@ * Display last frame when seeking to end of stream ([#2568](https://github.com/google/ExoPlayer/issues/2568)). -* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. -* Gracefully handle revoked `ACCESS_NETWORK_STATE` permission - ([#6019](https://github.com/google/ExoPlayer/issues/6019)). -* FLV: Fix bug that caused playback of some live streams to not start - ([#6111](https://github.com/google/ExoPlayer/issues/6111)). +* UI: Fix `PlayerView` incorrectly consuming touch events if no controller is + attached ([#6109](https://github.com/google/ExoPlayer/issues/6133)). * CEA608: Fix repetition of special North American characters ([#6133](https://github.com/google/ExoPlayer/issues/6133)). +* FLV: Fix bug that caused playback of some live streams to not start + ([#6111](https://github.com/google/ExoPlayer/issues/6111)). +* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`. +* MediaSession extension: Fix `MediaSessionConnector.play()` not resuming + playback ([#6093](https://github.com/google/ExoPlayer/issues/6093)). ### 2.10.2 ### From 1d766c4603a9767f8ef69c8db2445af709c71547 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 23 Apr 2019 09:18:03 +0100 Subject: [PATCH 18/24] Play out remaining data on reconfiguration Before this change we'd release the audio track and create a new one as soon as audio processors had drained when reconfiguring. Fix this behavior by stop()ing the AudioTrack to play out all written data. Issue: #2446 PiperOrigin-RevId: 244812402 --- RELEASENOTES.md | 3 + .../exoplayer2/audio/DefaultAudioSink.java | 57 +++++++++++-------- library/ui/build.gradle | 2 +- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c6d3faadc9..4b9bda112a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,9 @@ * Display last frame when seeking to end of stream ([#2568](https://github.com/google/ExoPlayer/issues/2568)). +* Audio: + * Fix an issue where not all audio was played out when the configuration + for the underlying track was changing (e.g., at some period transitions). * UI: Fix `PlayerView` incorrectly consuming touch events if no controller is attached ([#6109](https://github.com/google/ExoPlayer/issues/6133)). * CEA608: Fix repetition of special North American characters diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index a3c0990366..a65a94d965 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -272,6 +272,7 @@ public final class DefaultAudioSink implements AudioSink { private int preV21OutputBufferOffset; private int drainingAudioProcessorIndex; private boolean handledEndOfStream; + private boolean stoppedAudioTrack; private boolean playing; private int audioSessionId; @@ -465,19 +466,15 @@ public final class DefaultAudioSink implements AudioSink { processingEnabled, canApplyPlaybackParameters, availableAudioProcessors); - if (isInitialized()) { - if (!pendingConfiguration.canReuseAudioTrack(configuration)) { - // We need a new AudioTrack before we can handle more input. We should first stop() the - // track and wait for audio to play out (tracked by [Internal: b/33161961]), but for now we - // discard the audio track immediately. - flush(); - } else if (flushAudioProcessors) { - // We don't need a new AudioTrack but audio processors need to be drained and flushed. - this.pendingConfiguration = pendingConfiguration; - return; - } + // If we have a pending configuration already, we always drain audio processors as the preceding + // configuration may have required it (even if this one doesn't). + boolean drainAudioProcessors = flushAudioProcessors || this.pendingConfiguration != null; + if (isInitialized() + && (!pendingConfiguration.canReuseAudioTrack(configuration) || drainAudioProcessors)) { + this.pendingConfiguration = pendingConfiguration; + } else { + configuration = pendingConfiguration; } - configuration = pendingConfiguration; } private void setupAudioProcessors() { @@ -579,12 +576,21 @@ public final class DefaultAudioSink implements AudioSink { Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer); if (pendingConfiguration != null) { - // We are waiting for audio processors to drain before applying a the new configuration. if (!drainAudioProcessorsToEndOfStream()) { + // There's still pending data in audio processors to write to the track. return false; + } else if (!pendingConfiguration.canReuseAudioTrack(configuration)) { + playPendingData(); + if (hasPendingData()) { + // We're waiting for playout on the current audio track to finish. + return false; + } + flush(); + } else { + // The current audio track can be reused for the new configuration. + configuration = pendingConfiguration; + pendingConfiguration = null; } - configuration = pendingConfiguration; - pendingConfiguration = null; playbackParameters = configuration.canApplyPlaybackParameters ? audioProcessorChain.applyPlaybackParameters(playbackParameters) @@ -786,15 +792,8 @@ public final class DefaultAudioSink implements AudioSink { @Override public void playToEndOfStream() throws WriteException { - if (handledEndOfStream || !isInitialized()) { - return; - } - - if (drainAudioProcessorsToEndOfStream()) { - // The audio processors have drained, so drain the underlying audio track. - audioTrackPositionTracker.handleEndOfStream(getWrittenFrames()); - audioTrack.stop(); - bytesUntilNextAvSync = 0; + if (!handledEndOfStream && isInitialized() && drainAudioProcessorsToEndOfStream()) { + playPendingData(); handledEndOfStream = true; } } @@ -976,6 +975,7 @@ public final class DefaultAudioSink implements AudioSink { flushAudioProcessors(); inputBuffer = null; outputBuffer = null; + stoppedAudioTrack = false; handledEndOfStream = false; drainingAudioProcessorIndex = C.INDEX_UNSET; avSyncHeader = null; @@ -1223,6 +1223,15 @@ public final class DefaultAudioSink implements AudioSink { audioTrack.setStereoVolume(volume, volume); } + private void playPendingData() { + if (!stoppedAudioTrack) { + stoppedAudioTrack = true; + audioTrackPositionTracker.handleEndOfStream(getWrittenFrames()); + audioTrack.stop(); + bytesUntilNextAvSync = 0; + } + } + /** Stores playback parameters with the position and media time at which they apply. */ private static final class PlaybackParametersCheckpoint { diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 49446b25de..6384bf920f 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -40,7 +40,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.media:media:1.0.0' + implementation 'androidx.media:media:1.0.1' implementation 'androidx.annotation:annotation:1.0.2' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils-robolectric') From dbabb7c9a3ba6640c51230bc7932c7ab471284d8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 4 Jul 2019 12:54:56 +0100 Subject: [PATCH 19/24] Apply playback parameters in a consistent way. Currently, we sometimes apply new playback parameters directly and sometimes through the list of playbackParameterCheckpoints. Only when using the checkpoints, we also reset the offset and corresponding position for speedup position calculation. However, these offsets need to be changed in all cases to prevent calculation errors during speedup calculation[1]. This change channels all playback parameters changes through the checkpoints to ensure the offsets get updated accordingly. This fixes an issue introduced in https://github.com/google/ExoPlayer/commit/31911ca54a13b0003d6cf902b95c2ed445afa930. [1] - The speed up is calculated using the ratio of input and output bytes in SonicAudioProcessor.scaleDurationForSpeedUp. Whenever we set new playback parameters to the audio processor these two counts are reset. If we don't reset the offsets too, the scaled timestamp can be a large value compared to the input and output bytes causing massive inaccuracies (like the +20 seconds in the linked issue). Issue:#6117 PiperOrigin-RevId: 256533780 --- RELEASENOTES.md | 2 + .../exoplayer2/audio/DefaultAudioSink.java | 47 ++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4b9bda112a..e666f3ff39 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,8 @@ * Audio: * Fix an issue where not all audio was played out when the configuration for the underlying track was changing (e.g., at some period transitions). + * Fix an issue where playback speed was applied inaccurately in playlists + ([#6117](https://github.com/google/ExoPlayer/issues/6117)). * UI: Fix `PlayerView` incorrectly consuming touch events if no controller is attached ([#6109](https://github.com/google/ExoPlayer/issues/6133)). * CEA608: Fix repetition of special North American characters diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index a65a94d965..be1b7d3d53 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -501,7 +501,7 @@ public final class DefaultAudioSink implements AudioSink { } } - private void initialize() throws InitializationException { + private void initialize(long presentationTimeUs) throws InitializationException { // If we're asynchronously releasing a previous audio track then we block until it has been // released. This guarantees that we cannot end up in a state where we have multiple audio // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust @@ -533,11 +533,7 @@ public final class DefaultAudioSink implements AudioSink { } } - playbackParameters = - configuration.canApplyPlaybackParameters - ? audioProcessorChain.applyPlaybackParameters(playbackParameters) - : PlaybackParameters.DEFAULT; - setupAudioProcessors(); + applyPlaybackParameters(playbackParameters, presentationTimeUs); audioTrackPositionTracker.setAudioTrack( audioTrack, @@ -591,15 +587,12 @@ public final class DefaultAudioSink implements AudioSink { configuration = pendingConfiguration; pendingConfiguration = null; } - playbackParameters = - configuration.canApplyPlaybackParameters - ? audioProcessorChain.applyPlaybackParameters(playbackParameters) - : PlaybackParameters.DEFAULT; - setupAudioProcessors(); + // Re-apply playback parameters. + applyPlaybackParameters(playbackParameters, presentationTimeUs); } if (!isInitialized()) { - initialize(); + initialize(presentationTimeUs); if (playing) { play(); } @@ -635,15 +628,7 @@ public final class DefaultAudioSink implements AudioSink { } PlaybackParameters newPlaybackParameters = afterDrainPlaybackParameters; afterDrainPlaybackParameters = null; - newPlaybackParameters = audioProcessorChain.applyPlaybackParameters(newPlaybackParameters); - // Store the position and corresponding media time from which the parameters will apply. - playbackParametersCheckpoints.add( - new PlaybackParametersCheckpoint( - newPlaybackParameters, - Math.max(0, presentationTimeUs), - configuration.framesToDurationUs(getWrittenFrames()))); - // Update the set of active audio processors to take into account the new parameters. - setupAudioProcessors(); + applyPlaybackParameters(newPlaybackParameters, presentationTimeUs); } if (startMediaTimeState == START_NOT_SET) { @@ -857,8 +842,9 @@ public final class DefaultAudioSink implements AudioSink { // parameters apply. afterDrainPlaybackParameters = playbackParameters; } else { - // Update the playback parameters now. - this.playbackParameters = audioProcessorChain.applyPlaybackParameters(playbackParameters); + // Update the playback parameters now. They will be applied to the audio processors during + // initialization. + this.playbackParameters = playbackParameters; } } return this.playbackParameters; @@ -1040,6 +1026,21 @@ public final class DefaultAudioSink implements AudioSink { }.start(); } + private void applyPlaybackParameters( + PlaybackParameters playbackParameters, long presentationTimeUs) { + PlaybackParameters newPlaybackParameters = + configuration.canApplyPlaybackParameters + ? audioProcessorChain.applyPlaybackParameters(playbackParameters) + : PlaybackParameters.DEFAULT; + // Store the position and corresponding media time from which the parameters will apply. + playbackParametersCheckpoints.add( + new PlaybackParametersCheckpoint( + newPlaybackParameters, + /* mediaTimeUs= */ Math.max(0, presentationTimeUs), + /* positionUs= */ configuration.framesToDurationUs(getWrittenFrames()))); + setupAudioProcessors(); + } + private long applySpeedup(long positionUs) { @Nullable PlaybackParametersCheckpoint checkpoint = null; while (!playbackParametersCheckpoints.isEmpty() From be9fea89a6a669a2d7125a90a90054810c9a4d0b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 9 Jul 2019 11:18:50 +0100 Subject: [PATCH 20/24] Fix race condition in DownloadHelper Sending MESSAGE_PREPARE_SOURCE should happen last in the constructor. It was previously happening before initialization finished (and in particular before pendingMediaPeriods was instantiated). Issue: #6146 PiperOrigin-RevId: 257158275 --- .../google/android/exoplayer2/offline/DownloadHelper.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 7e98f30301..821696aae7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -817,10 +817,10 @@ public final class DownloadHelper { private final MediaSource mediaSource; private final DownloadHelper downloadHelper; private final Allocator allocator; + private final ArrayList pendingMediaPeriods; + private final Handler downloadHelperHandler; private final HandlerThread mediaSourceThread; private final Handler mediaSourceHandler; - private final Handler downloadHelperHandler; - private final ArrayList pendingMediaPeriods; @Nullable public Object manifest; public @MonotonicNonNull Timeline timeline; @@ -832,6 +832,7 @@ public final class DownloadHelper { this.mediaSource = mediaSource; this.downloadHelper = downloadHelper; allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + pendingMediaPeriods = new ArrayList<>(); @SuppressWarnings("methodref.receiver.bound.invalid") Handler downloadThreadHandler = Util.createHandler(this::handleDownloadHelperCallbackMessage); this.downloadHelperHandler = downloadThreadHandler; @@ -839,7 +840,6 @@ public final class DownloadHelper { mediaSourceThread.start(); mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this); mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE); - pendingMediaPeriods = new ArrayList<>(); } public void release() { From fdef76c8431a3577f206605c629f68452a8652ec Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 9 Jul 2019 11:50:56 +0100 Subject: [PATCH 21/24] Bump version to 2.10.3 PiperOrigin-RevId: 257161518 --- 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 bf464ad2c1..70e77b22c6 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.10.2' - releaseVersionCode = 2010002 + releaseVersion = '2.10.3' + releaseVersionCode = 2010003 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 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 db3f3943e1..190f4de5a6 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.10.2"; + public static final String VERSION = "2.10.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.10.2"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.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 = 2010002; + public static final int VERSION_INT = 2010003; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 1273f18e809dada00649799fb72c0d1071f0faec Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 9 Jul 2019 15:06:12 +0100 Subject: [PATCH 22/24] Fix syntax error in publish.gradle PiperOrigin-RevId: 257184313 --- publish.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/publish.gradle b/publish.gradle index 96ec3d2f10..f293673c49 100644 --- a/publish.gradle +++ b/publish.gradle @@ -31,7 +31,7 @@ if (project.ext.has("exoplayerPublishEnabled") task.doLast { task.outputs.files .filter { File file -> - file.path.contains("publications") + file.path.contains("publications") \ && file.name.matches("^pom-.+\\.xml\$") } .forEach { File file -> addLicense(file) } From f314be583aef39e3bfc07ebb39c58fb98ce465d5 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 9 Jul 2019 15:11:11 +0100 Subject: [PATCH 23/24] fix typo in release notes PiperOrigin-RevId: 257185017 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e666f3ff39..04bf514c12 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -10,7 +10,7 @@ * Fix an issue where playback speed was applied inaccurately in playlists ([#6117](https://github.com/google/ExoPlayer/issues/6117)). * UI: Fix `PlayerView` incorrectly consuming touch events if no controller is - attached ([#6109](https://github.com/google/ExoPlayer/issues/6133)). + attached ([#6109](https://github.com/google/ExoPlayer/issues/6109)). * CEA608: Fix repetition of special North American characters ([#6133](https://github.com/google/ExoPlayer/issues/6133)). * FLV: Fix bug that caused playback of some live streams to not start From 1275217bca00184d52f0c374d59db985ebf070c1 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 11 Jul 2019 18:11:08 +0100 Subject: [PATCH 24/24] Add missing file header PiperOrigin-RevId: 257630168 --- publish.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/publish.gradle b/publish.gradle index f293673c49..8cfc2b2ea1 100644 --- a/publish.gradle +++ b/publish.gradle @@ -60,6 +60,7 @@ static void addLicense(File pom) { xml.append(licensesNode) def writer = new PrintWriter(new FileWriter(pom)) + writer.write("\n") def printer = new XmlNodePrinter(writer) printer.preserveWhitespace = true printer.print(xml)