From ddda2eef7eb8a02af7a61dd49a53f7460fcdd62f Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 4 Dec 2018 17:30:08 +0000 Subject: [PATCH 01/24] Add no-op defaults to Video(Audio)RendererEventListener. This is in line with how Player.EventListener and AnalyticsListener methods are defined and helps to only implement the callbacks needed. PiperOrigin-RevId: 223991262 --- .../audio/AudioRendererEventListener.java | 18 ++++++----- .../video/VideoRendererEventListener.java | 32 +++++++++---------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index 7c3c1481fc..eff7bc8de2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -25,7 +25,8 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.util.Assertions; /** - * Listener of audio {@link Renderer} events. + * Listener of audio {@link Renderer} events. All methods have no-op default implementations to + * allow selective overrides. */ public interface AudioRendererEventListener { @@ -35,14 +36,14 @@ public interface AudioRendererEventListener { * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it * remains enabled. */ - void onAudioEnabled(DecoderCounters counters); + default void onAudioEnabled(DecoderCounters counters) {} /** * Called when the audio session is set. * * @param audioSessionId The audio session id. */ - void onAudioSessionId(int audioSessionId); + default void onAudioSessionId(int audioSessionId) {} /** * Called when a decoder is created. @@ -52,15 +53,15 @@ public interface AudioRendererEventListener { * finished. * @param initializationDurationMs The time taken to initialize the decoder in milliseconds. */ - void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, - long initializationDurationMs); + default void onAudioDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) {} /** * Called when the format of the media being consumed by the renderer changes. * * @param format The new format. */ - void onAudioInputFormatChanged(Format format); + default void onAudioInputFormatChanged(Format format) {} /** * Called when an {@link AudioSink} underrun occurs. @@ -71,14 +72,15 @@ public interface AudioRendererEventListener { * as the buffered media can have a variable bitrate so the duration may be unknown. * @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data. */ - void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); + default void onAudioSinkUnderrun( + int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {} /** * Called when the renderer is disabled. * * @param counters {@link DecoderCounters} that were updated by the renderer. */ - void onAudioDisabled(DecoderCounters counters); + default void onAudioDisabled(DecoderCounters counters) {} /** * Dispatches events to a {@link AudioRendererEventListener}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java index 617211afb7..7d78ba03c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java @@ -26,7 +26,8 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.util.Assertions; /** - * Listener of video {@link Renderer} events. + * Listener of video {@link Renderer} events. All methods have no-op default implementations to + * allow selective overrides. */ public interface VideoRendererEventListener { @@ -36,7 +37,7 @@ public interface VideoRendererEventListener { * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it * remains enabled. */ - void onVideoEnabled(DecoderCounters counters); + default void onVideoEnabled(DecoderCounters counters) {} /** * Called when a decoder is created. @@ -46,15 +47,15 @@ public interface VideoRendererEventListener { * finished. * @param initializationDurationMs The time taken to initialize the decoder in milliseconds. */ - void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, - long initializationDurationMs); + default void onVideoDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) {} /** * Called when the format of the media being consumed by the renderer changes. * * @param format The new format. */ - void onVideoInputFormatChanged(Format format); + default void onVideoInputFormatChanged(Format format) {} /** * Called to report the number of frames dropped by the renderer. Dropped frames are reported @@ -62,12 +63,11 @@ public interface VideoRendererEventListener { * reaches a specified threshold whilst the renderer is started. * * @param count The number of dropped frames. - * @param elapsedMs The duration in milliseconds over which the frames were dropped. This - * duration is timed from when the renderer was started or from when dropped frames were - * last reported (whichever was more recent), and not from when the first of the reported - * drops occurred. + * @param elapsedMs The duration in milliseconds over which the frames were dropped. This duration + * is timed from when the renderer was started or from when dropped frames were last reported + * (whichever was more recent), and not from when the first of the reported drops occurred. */ - void onDroppedFrames(int count, long elapsedMs); + default void onDroppedFrames(int count, long elapsedMs) {} /** * Called before a frame is rendered for the first time since setting the surface, and each time @@ -82,12 +82,12 @@ public interface VideoRendererEventListener { * this is not possible. Applications that use {@link TextureView} can apply the rotation by * calling {@link TextureView#setTransform}. Applications that do not expect to encounter * rotated videos can safely ignore this parameter. - * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case - * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic + * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case of + * square pixels this will be equal to 1.0. Different values are indicative of anamorphic * content. */ - void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio); + default void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {} /** * Called when a frame is rendered for the first time since setting the surface, and when a frame @@ -96,14 +96,14 @@ public interface VideoRendererEventListener { * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if * the renderer renders to something that isn't a {@link Surface}. */ - void onRenderedFirstFrame(@Nullable Surface surface); + default void onRenderedFirstFrame(@Nullable Surface surface) {} /** * Called when the renderer is disabled. * * @param counters {@link DecoderCounters} that were updated by the renderer. */ - void onVideoDisabled(DecoderCounters counters); + default void onVideoDisabled(DecoderCounters counters) {} /** * Dispatches events to a {@link VideoRendererEventListener}. From 5f33c7fcf5dee242eb04627720ab2b75d8906a5b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 5 Dec 2018 17:49:05 +0000 Subject: [PATCH 02/24] Merge pull request #5187 from BrainCrumbz:feat/get-tag PiperOrigin-RevId: 224166374 --- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 6 ++++++ .../exoplayer2/source/ClippingMediaSource.java | 6 ++++++ .../exoplayer2/source/ConcatenatingMediaSource.java | 12 ++++++++++++ .../exoplayer2/source/ExtractorMediaSource.java | 6 ++++++ .../exoplayer2/source/LoopingMediaSource.java | 6 ++++++ .../android/exoplayer2/source/MediaSource.java | 6 ++++++ .../exoplayer2/source/MergingMediaSource.java | 6 ++++++ .../exoplayer2/source/SingleSampleMediaSource.java | 8 ++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 6 ++++++ .../exoplayer2/source/dash/DashMediaSource.java | 6 ++++++ .../exoplayer2/source/hls/HlsMediaSource.java | 6 ++++++ .../source/smoothstreaming/SsMediaSource.java | 6 ++++++ .../android/exoplayer2/testutil/FakeMediaSource.java | 7 +++++++ 13 files changed, 87 insertions(+) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 400061d019..85042c4354 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -76,6 +76,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn adUiViewGroup, eventHandler, eventListener); } + @Override + @Nullable + public Object getTag() { + return adsMediaSource.getTag(); + } + @Override public void prepareSourceInternal( final ExoPlayer player, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 78e37c1869..3ed18049bf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -216,6 +216,12 @@ public final class ClippingMediaSource extends CompositeMediaSource { window = new Timeline.Window(); } + @Override + @Nullable + public Object getTag() { + return mediaSource.getTag(); + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 88cd4a1595..af4a9f8000 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -453,6 +453,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource { mediaPeriodToChildMediaPeriodId = new HashMap<>(); } + @Override + @Nullable + public Object getTag() { + return childSource.getTag(); + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 6b0f5c8eeb..74449ba16b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -220,6 +220,12 @@ public interface MediaSource { */ void removeEventListener(MediaSourceEventListener eventListener); + /** Returns the tag set on the media source, or null if none was set. */ + @Nullable + default Object getTag() { + return null; + } + /** @deprecated Will be removed in the next release. */ @Deprecated void prepareSource( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index ecb4b10c6a..573e97cb13 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -98,6 +98,12 @@ public final class MergingMediaSource extends CompositeMediaSource { timelines = new Timeline[mediaSources.length]; } + @Override + @Nullable + public Object getTag() { + return mediaSources.length > 0 ? mediaSources[0].getTag() : null; + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 1ac6207454..66097970c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -185,6 +185,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; + @Nullable private final Object tag; private @Nullable TransferListener transferListener; @@ -287,6 +288,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { this.durationUs = durationUs; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; + this.tag = tag; dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); timeline = @@ -295,6 +297,12 @@ public final class SingleSampleMediaSource extends BaseMediaSource { // MediaSource implementation. + @Override + @Nullable + public Object getTag() { + return tag; + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 7fc0f22bf3..19ddbd2c54 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -319,6 +319,12 @@ public final class AdsMediaSource extends CompositeMediaSource { adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes()); } + @Override + @Nullable + public Object getTag() { + return contentMediaSource.getTag(); + } + @Override public void prepareSourceInternal( final ExoPlayer player, 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 8ee859b8bd..c8de8f02b1 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 @@ -607,6 +607,12 @@ public final class DashMediaSource extends BaseMediaSource { // MediaSource implementation. + @Override + @Nullable + public Object getTag() { + return tag; + } + @Override public void prepareSourceInternal( ExoPlayer player, 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 a075dacf3a..a9b0c579ac 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 @@ -390,6 +390,12 @@ public final class HlsMediaSource extends BaseMediaSource this.tag = tag; } + @Override + @Nullable + public Object getTag() { + return tag; + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index a756b7f4f1..103a52a55a 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -503,6 +503,12 @@ public final class SsMediaSource extends BaseMediaSource // MediaSource implementation. + @Override + @Nullable + public Object getTag() { + return tag; + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 2fca4f42c7..1f0c0c1a40 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -88,6 +88,13 @@ public class FakeMediaSource extends BaseMediaSource { this.trackGroupArray = trackGroupArray; } + @Override + @Nullable + public Object getTag() { + boolean hasTimeline = timeline != null && !timeline.isEmpty(); + return hasTimeline ? timeline.getWindow(0, new Timeline.Window()).tag : null; + } + @Override public synchronized void prepareSourceInternal( ExoPlayer player, From ee1ec8d3d6c89dfa11ca5504fd4eae919fcd83ac Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 6 Dec 2018 09:05:09 +0000 Subject: [PATCH 03/24] Disable post processing on Nvidia devices PiperOrigin-RevId: 224291309 --- .../video/MediaCodecVideoRenderer.java | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 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 79adc87509..176f0f5765 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 @@ -98,7 +98,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private final EventDispatcher eventDispatcher; private final long allowedJoiningTimeMs; private final int maxDroppedFramesToNotify; - private final boolean deviceNeedsAutoFrcWorkaround; + private final boolean deviceNeedsNoPostProcessWorkaround; private final long[] pendingOutputStreamOffsetsUs; private final long[] pendingOutputStreamSwitchTimesUs; @@ -226,7 +226,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { this.context = context.getApplicationContext(); frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context); eventDispatcher = new EventDispatcher(eventHandler, eventListener); - deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround(); + deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround(); pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; outputStreamOffsetUs = C.TIME_UNSET; @@ -471,7 +471,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { format, codecMaxValues, codecOperatingRate, - deviceNeedsAutoFrcWorkaround, + deviceNeedsNoPostProcessWorkaround, tunnelingAudioSessionId); if (surface == null) { Assertions.checkState(shouldUseDummySurface(codecInfo)); @@ -1027,8 +1027,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param codecMaxValues Codec max values that should be used when configuring the decoder. * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if * no codec operating rate should be set. - * @param deviceNeedsAutoFrcWorkaround Whether the device is known to enable frame-rate conversion - * logic that negatively impacts ExoPlayer. + * @param deviceNeedsNoPostProcessWorkaround Whether the device is known to do post processing by + * default that isn't compatible with ExoPlayer. * @param tunnelingAudioSessionId The audio session id to use for tunneling, or {@link * C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. * @return The framework {@link MediaFormat} that should be used to configure the decoder. @@ -1038,7 +1038,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { Format format, CodecMaxValues codecMaxValues, float codecOperatingRate, - boolean deviceNeedsAutoFrcWorkaround, + boolean deviceNeedsNoPostProcessWorkaround, int tunnelingAudioSessionId) { MediaFormat mediaFormat = new MediaFormat(); // Set format parameters that should always be set. @@ -1062,7 +1062,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate); } } - if (deviceNeedsAutoFrcWorkaround) { + if (deviceNeedsNoPostProcessWorkaround) { + mediaFormat.setInteger("no-post-process", 1); mediaFormat.setInteger("auto-frc", 0); } if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { @@ -1256,21 +1257,21 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * Returns whether the device is known to enable frame-rate conversion logic that negatively - * impacts ExoPlayer. - *

- * If true is returned then we explicitly disable the feature. + * Returns whether the device is known to do post processing by default that isn't compatible with + * ExoPlayer. * - * @return True if the device is known to enable frame-rate conversion logic that negatively - * impacts ExoPlayer. False otherwise. + * @return Whether the device is known to do post processing by default that isn't compatible with + * ExoPlayer. */ - private static boolean deviceNeedsAutoFrcWorkaround() { - // nVidia Shield prior to M tries to adjust the playback rate to better map the frame-rate of + private static boolean deviceNeedsNoPostProcessWorkaround() { + // Nvidia devices prior to M try to adjust the playback rate to better map the frame-rate of // content to the refresh rate of the display. For example playback of 23.976fps content is // adjusted to play at 1.001x speed when the output display is 60Hz. Unfortunately the // implementation causes ExoPlayer's reported playback position to drift out of sync. Captions - // also lose sync [Internal: b/26453592]. - return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); + // also lose sync [Internal: b/26453592]. Even after M, the devices may apply post processing + // operations that can modify frame output timestamps, which is incompatible with ExoPlayer's + // logic for skipping decode-only frames. + return "NVIDIA".equals(Util.MANUFACTURER); } /* From 0e139e994544467907a703be6872a60ff44e4c4c Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 6 Dec 2018 13:48:09 +0000 Subject: [PATCH 04/24] Use media source tag in dummy timeline. This is now possible as it's directly accessible from the media source. Issue:#5177 Issue:#5155 PiperOrigin-RevId: 224321917 --- .../source/ConcatenatingMediaSource.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index af4a9f8000..03ccd56645 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -826,7 +826,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); this.uid = new Object(); } @@ -951,10 +951,18 @@ public class ConcatenatingMediaSource extends CompositeMediaSource Date: Sun, 9 Dec 2018 19:05:09 +0000 Subject: [PATCH 05/24] Apply EOS flush workaround to stvm8 devices Issue:#5203 PiperOrigin-RevId: 224726041 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 86bbb330b7..6a813332e3 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 @@ -1622,7 +1622,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static boolean codecNeedsEosFlushWorkaround(String name) { return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name)) - || (Util.SDK_INT <= 19 && "hb2000".equals(Util.DEVICE) + || (Util.SDK_INT <= 19 + && ("hb2000".equals(Util.DEVICE) || "stvm8".equals(Util.DEVICE)) && ("OMX.amlogic.avc.decoder.awesome".equals(name) || "OMX.amlogic.avc.decoder.awesome.secure".equals(name))); } From db5083d18ac3d54874dc48e9eef44317b2fe0591 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 11 Dec 2018 18:19:07 +0000 Subject: [PATCH 06/24] Enable setOutputSurfaceWorkaround for dangal Issue: #5169 PiperOrigin-RevId: 225025357 --- .../video/MediaCodecVideoRenderer.java | 312 +++++++++--------- 1 file changed, 159 insertions(+), 153 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 176f0f5765..5d6fba5b0d 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 @@ -1297,163 +1297,169 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * 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. + if (name.startsWith("OMX.google")) { + // 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, - // https://github.com/google/ExoPlayer/issues/3835, - // https://github.com/google/ExoPlayer/issues/4006, - // 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, - // 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 "ASUS_X00AD_2": - 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 "i9031": - case "iball8735_9806": - case "Infinix-X572": - case "iris60": - case "itel_S41": - case "j2xlteins": - case "JGZ": - case "K50a40": - case "kate": - 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 "PRO7S": - 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: - // Do nothing. - break; - } - switch (Util.MODEL) { - case "AFTA": - case "AFTN": - deviceNeedsSetOutputSurfaceWorkaround = true; - break; - default: - // Do nothing. - break; + if (Util.SDK_INT <= 27 && "dangal".equals(Util.DEVICE)) { + // Dangal is affected on API level 27: https://github.com/google/ExoPlayer/issues/5169. + deviceNeedsSetOutputSurfaceWorkaround = true; + } else if (Util.SDK_INT >= 27) { + // In general, devices running API level 27 or later should be unaffected. Do nothing. + } else { + // Enable the workaround on a per-device basis. Works 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, + // https://github.com/google/ExoPlayer/issues/3835, + // https://github.com/google/ExoPlayer/issues/4006, + // 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, + // https://github.com/google/ExoPlayer/issues/4419, + // https://github.com/google/ExoPlayer/issues/4460, + // https://github.com/google/ExoPlayer/issues/4468. + 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 "ASUS_X00AD_2": + 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 "i9031": + case "iball8735_9806": + case "Infinix-X572": + case "iris60": + case "itel_S41": + case "j2xlteins": + case "JGZ": + case "K50a40": + case "kate": + 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 "PRO7S": + 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: + // Do nothing. + break; + } + switch (Util.MODEL) { + case "AFTA": + case "AFTN": + deviceNeedsSetOutputSurfaceWorkaround = true; + break; + default: + // Do nothing. + break; + } } evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true; } From 84ad3f7b667d6f462d3d91e618f1cfc2c9ebb8dc Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 12 Dec 2018 13:56:21 +0000 Subject: [PATCH 07/24] Fix release notes PiperOrigin-RevId: 225170404 --- RELEASENOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6b3fbc4f27..6435f4209d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -47,10 +47,10 @@ * DASH: Parse ProgramInformation element if present in the manifest. * HLS: * Add constructor to `DefaultHlsExtractorFactory` for adding TS payload - reader factory flags. + reader factory flags + ([#4861](https://github.com/google/ExoPlayer/issues/4861)). * Fix bug in segment sniffing ([#5039](https://github.com/google/ExoPlayer/issues/5039)). - ([#4861](https://github.com/google/ExoPlayer/issues/4861)). * SubRip: Add support for alignment tags, and remove tags from the displayed captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)). * Fix issue with blind seeking to windows with non-zero offset in a From 1851d5e193db3b3050a022bc23f7b08cc5495d3f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 14 Dec 2018 15:40:57 +0000 Subject: [PATCH 08/24] Merge pull request #5245 from natario1:videosize-override PiperOrigin-RevId: 225187852 --- .../android/exoplayer2/ui/PlayerView.java | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 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 310d04a064..bb8395b317 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 @@ -29,7 +29,6 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Looper; import android.support.annotation.IntDef; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; @@ -271,13 +270,13 @@ public class PlayerView extends FrameLayout { private static final int SURFACE_TYPE_MONO360_VIEW = 3; // LINT.ThenChange(../../../../../../res/values/attrs.xml) - private final AspectRatioFrameLayout contentFrame; + @Nullable private final AspectRatioFrameLayout contentFrame; private final View shutterView; - private final View surfaceView; + @Nullable private final View surfaceView; private final ImageView artworkView; private final SubtitleView subtitleView; - private final @Nullable View bufferingView; - private final @Nullable TextView errorMessageView; + @Nullable private final View bufferingView; + @Nullable private final TextView errorMessageView; private final PlayerControlView controller; private final ComponentListener componentListener; private final FrameLayout overlayFrameLayout; @@ -285,11 +284,11 @@ public class PlayerView extends FrameLayout { private Player player; private boolean useController; private boolean useArtwork; - private @Nullable Drawable defaultArtwork; + @Nullable private Drawable defaultArtwork; private @ShowBuffering int showBuffering; private boolean keepContentOnPlayerReset; - private @Nullable ErrorMessageProvider errorMessageProvider; - private @Nullable CharSequence customErrorMessage; + @Nullable private ErrorMessageProvider errorMessageProvider; + @Nullable private CharSequence customErrorMessage; private int controllerShowTimeoutMs; private boolean controllerAutoShow; private boolean controllerHideDuringAds; @@ -474,9 +473,7 @@ public class PlayerView extends FrameLayout { * @param newPlayerView The new view to attach to the player. */ public static void switchTargetView( - @NonNull Player player, - @Nullable PlayerView oldPlayerView, - @Nullable PlayerView newPlayerView) { + Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView) { if (oldPlayerView == newPlayerView) { return; } @@ -1074,6 +1071,26 @@ public class PlayerView extends FrameLayout { } } + /** + * Called when there's a change in the aspect ratio of the content being displayed. The default + * implementation sets the aspect ratio of the content frame to that of the content, unless the + * content view is a {@link SphericalSurfaceView} in which case the frame's aspect ratio is + * cleared. + * + * @param contentAspectRatio The aspect ratio of the content. + * @param contentFrame The content frame, or {@code null}. + * @param contentView The view that holds the content being displayed, or {@code null}. + */ + protected void onContentAspectRatioChanged( + float contentAspectRatio, + @Nullable AspectRatioFrameLayout contentFrame, + @Nullable View contentView) { + if (contentFrame != null) { + contentFrame.setAspectRatio( + contentView instanceof SphericalSurfaceView ? 0 : contentAspectRatio); + } + } + private boolean toggleControllerVisibility() { if (!useController || player == null) { return false; @@ -1187,9 +1204,8 @@ public class PlayerView extends FrameLayout { int drawableWidth = drawable.getIntrinsicWidth(); int drawableHeight = drawable.getIntrinsicHeight(); if (drawableWidth > 0 && drawableHeight > 0) { - if (contentFrame != null) { - contentFrame.setAspectRatio((float) drawableWidth / drawableHeight); - } + float artworkAspectRatio = (float) drawableWidth / drawableHeight; + onContentAspectRatioChanged(artworkAspectRatio, contentFrame, artworkView); artworkView.setImageDrawable(drawable); artworkView.setVisibility(VISIBLE); return true; @@ -1322,9 +1338,6 @@ public class PlayerView extends FrameLayout { @Override public void onVideoSizeChanged( int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (contentFrame == null) { - return; - } float videoAspectRatio = (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; @@ -1345,11 +1358,9 @@ public class PlayerView extends FrameLayout { surfaceView.addOnLayoutChangeListener(this); } applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); - } else if (surfaceView instanceof SphericalSurfaceView) { - videoAspectRatio = 0; } - contentFrame.setAspectRatio(videoAspectRatio); + onContentAspectRatioChanged(videoAspectRatio, contentFrame, surfaceView); } @Override From 942ac78a0c74f0fa16c712317fd7fdf22d88f4c9 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 13 Dec 2018 11:23:55 +0000 Subject: [PATCH 09/24] Add 4K Bravia to output surface workaroud PiperOrigin-RevId: 225344232 --- .../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 5d6fba5b0d..f16d77b250 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 @@ -1339,6 +1339,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { case "Aura_Note_2": case "BLACK-1X": case "BRAVIA_ATV2": + case "BRAVIA_ATV3_4K": case "C1": case "ComioS1": case "CP8676_I02": From fd6874809a7d5383636cbe5e6d5b360118e4f972 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 13 Dec 2018 16:17:14 +0000 Subject: [PATCH 10/24] Clarify that the shutter can prevent flicker PiperOrigin-RevId: 225374071 --- .../java/com/google/android/exoplayer2/ui/PlayerView.java | 5 +++-- 1 file changed, 3 insertions(+), 2 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 bb8395b317..d9989370b4 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 @@ -186,8 +186,9 @@ import java.util.List; *

  • Type: {@link AspectRatioFrameLayout} * *
  • {@code exo_shutter} - A view that's made visible when video should be hidden. This - * view is typically an opaque view that covers the video surface view, thereby obscuring it - * when visible. + * view is typically an opaque view that covers the video surface, thereby obscuring it when + * visible. Obscuring the surface in this way also helps to prevent flicker at the start of + * playback when {@code surface_type="surface_view"} *
      *
    • Type: {@link View} *
    From e7e2cbdc909c4aa8a8cb39d25607dfdc382a7545 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 13 Dec 2018 16:58:39 +0000 Subject: [PATCH 11/24] Add missing .. PiperOrigin-RevId: 225379305 --- .../main/java/com/google/android/exoplayer2/ui/PlayerView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d9989370b4..88eabfed07 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 @@ -188,7 +188,7 @@ import java.util.List; *
  • {@code exo_shutter} - A view that's made visible when video should be hidden. This * view is typically an opaque view that covers the video surface, thereby obscuring it when * visible. Obscuring the surface in this way also helps to prevent flicker at the start of - * playback when {@code surface_type="surface_view"} + * playback when {@code surface_type="surface_view"}. *
      *
    • Type: {@link View} *
    From 479841f65f58d4d5341b5db07a82ee3617b846bb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 13 Dec 2018 17:25:43 +0000 Subject: [PATCH 12/24] Add Nexus Player to output surface workaround PiperOrigin-RevId: 225383173 --- .../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 f16d77b250..40b25c2b2e 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 @@ -1362,6 +1362,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { case "F3215": case "F3311": case "flo": + case "fugu": case "GiONEE_CBL7513": case "GiONEE_GBL7319": case "GIONEE_GBL7360": From f41dadca5851d965cab5f46607de9e4c207b9284 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 14 Dec 2018 13:55:12 +0000 Subject: [PATCH 13/24] Remove unused interface method PiperOrigin-RevId: 225528632 --- .../exoplayer2/ext/mediasession/TimelineQueueEditor.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java index 4f9c553a15..7c00fcdf17 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java @@ -64,13 +64,6 @@ public final class TimelineQueueEditor * {@link MediaSessionConnector}. */ public interface QueueDataAdapter { - /** - * Gets the {@link MediaDescriptionCompat} for a {@code position}. - * - * @param position The position in the queue for which to provide a description. - * @return A {@link MediaDescriptionCompat}. - */ - MediaDescriptionCompat getMediaDescription(int position); /** * Adds a {@link MediaDescriptionCompat} at the given {@code position}. * From abdb58485f9abddafd367c90a730b830df2e1c0a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 17 Dec 2018 10:48:01 +0000 Subject: [PATCH 14/24] Add Player.MetadataComponent for completeness PiperOrigin-RevId: 225795581 --- .../exoplayer2/ext/cast/CastPlayer.java | 9 +++++++ .../android/exoplayer2/ExoPlayerImpl.java | 9 +++++++ .../com/google/android/exoplayer2/Player.java | 25 +++++++++++++++++ .../android/exoplayer2/SimpleExoPlayer.java | 27 +++++++++++-------- .../exoplayer2/testutil/StubExoPlayer.java | 5 ++++ 5 files changed, 64 insertions(+), 11 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 6cf6309796..71322de87e 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -283,20 +283,29 @@ public final class CastPlayer extends BasePlayer { // Player implementation. @Override + @Nullable public AudioComponent getAudioComponent() { return null; } @Override + @Nullable public VideoComponent getVideoComponent() { return null; } @Override + @Nullable public TextComponent getTextComponent() { return null; } + @Override + @Nullable + public MetadataComponent getMetadataComponent() { + return null; + } + @Override public Looper getApplicationLooper() { return Looper.getMainLooper(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index ffdadb78f7..35fa85e467 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -144,20 +144,29 @@ import java.util.concurrent.CopyOnWriteArraySet; } @Override + @Nullable public AudioComponent getAudioComponent() { return null; } @Override + @Nullable public VideoComponent getVideoComponent() { return null; } @Override + @Nullable public TextComponent getTextComponent() { return null; } + @Override + @Nullable + public MetadataComponent getMetadataComponent() { + return null; + } + @Override public Looper getPlaybackLooper() { return internalPlayer.getPlaybackLooper(); 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 16f8aa2878..e3441fb2a7 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 @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.C.VideoScalingMode; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioListener; import com.google.android.exoplayer2.audio.AuxEffectInfo; +import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -299,6 +300,24 @@ public interface Player { void removeTextOutput(TextOutput listener); } + /** The metadata component of a {@link Player}. */ + interface MetadataComponent { + + /** + * Adds a {@link MetadataOutput} to receive metadata. + * + * @param output The output to register. + */ + void addMetadataOutput(MetadataOutput output); + + /** + * Removes a {@link MetadataOutput}. + * + * @param output The output to remove. + */ + void removeMetadataOutput(MetadataOutput output); + } + /** * Listener of changes in player state. All methods have no-op default implementations to allow * selective overrides. @@ -533,6 +552,12 @@ public interface Player { @Nullable TextComponent getTextComponent(); + /** + * Returns the component of this player for metadata output, or null if metadata is not supported. + */ + @Nullable + MetadataComponent getMetadataComponent(); + /** * Returns the {@link Looper} associated with the application thread that's used to access the * player and on which player events are received. 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 8517556887..fe52cc7e8c 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 @@ -65,7 +65,11 @@ import java.util.concurrent.CopyOnWriteArraySet; */ @TargetApi(16) public class SimpleExoPlayer extends BasePlayer - implements ExoPlayer, Player.AudioComponent, Player.VideoComponent, Player.TextComponent { + implements ExoPlayer, + Player.AudioComponent, + Player.VideoComponent, + Player.TextComponent, + Player.MetadataComponent { /** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */ @Deprecated @@ -243,20 +247,29 @@ public class SimpleExoPlayer extends BasePlayer } @Override + @Nullable public AudioComponent getAudioComponent() { return this; } @Override + @Nullable public VideoComponent getVideoComponent() { return this; } @Override + @Nullable public TextComponent getTextComponent() { return this; } + @Override + @Nullable + public MetadataComponent getMetadataComponent() { + return this; + } + /** * Sets the video scaling mode. * @@ -713,20 +726,12 @@ public class SimpleExoPlayer extends BasePlayer removeTextOutput(output); } - /** - * Adds a {@link MetadataOutput} to receive metadata. - * - * @param listener The output to register. - */ + @Override public void addMetadataOutput(MetadataOutput listener) { metadataOutputs.add(listener); } - /** - * Removes a {@link MetadataOutput}. - * - * @param listener The output to remove. - */ + @Override public void removeMetadataOutput(MetadataOutput listener) { metadataOutputs.remove(listener); } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 156b573df8..724ed366bc 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -49,6 +49,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public MetadataComponent getMetadataComponent() { + throw new UnsupportedOperationException(); + } + @Override public Looper getPlaybackLooper() { throw new UnsupportedOperationException(); From 78cdd5fee1ecdb0724620753637a8d719c9482e1 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 18 Dec 2018 19:46:21 +0000 Subject: [PATCH 15/24] Merge pull request #5216 from mseroczynski:dev-v2 PiperOrigin-RevId: 225966289 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 893601a859..f6cc6a8344 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -318,7 +318,20 @@ public final class MediaCodecUtil { } // Work around https://github.com/google/ExoPlayer/issues/4519. - if ("OMX.SEC.mp3.dec".equals(name) && "SM-T530".equals(Util.MODEL)) { + if ("OMX.SEC.mp3.dec".equals(name) + && ("GT-I9152".equals(Util.MODEL) + || "GT-I9515".equals(Util.MODEL) + || "GT-P5220".equals(Util.MODEL) + || "GT-S7580".equals(Util.MODEL) + || "SM-G350".equals(Util.MODEL) + || "SM-T231".equals(Util.MODEL) + || "SM-T530".equals(Util.MODEL))) { + return false; + } + if ("OMX.brcm.audio.mp3.decoder".equals(name) + && ("GT-I9152".equals(Util.MODEL) + || "GT-S7580".equals(Util.MODEL) + || "SM-G350".equals(Util.MODEL))) { return false; } From 975ed6c2772cadf8238f5cf5967f1a7cc92813f4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 18 Dec 2018 14:58:01 +0000 Subject: [PATCH 16/24] Use the true bitrate for CBR MP3 seeking PiperOrigin-RevId: 225989898 --- .../exoplayer2/extractor/MpegAudioHeader.java | 59 ++++++++++++------- .../test/assets/mp3/play-trimmed.mp3.0.dump | 2 +- .../test/assets/mp3/play-trimmed.mp3.1.dump | 2 +- .../test/assets/mp3/play-trimmed.mp3.2.dump | 2 +- .../test/assets/mp3/play-trimmed.mp3.3.dump | 2 +- 5 files changed, 43 insertions(+), 24 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java index ab49ca5454..87bb992082 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java @@ -34,16 +34,26 @@ public final class MpegAudioHeader { private static final String[] MIME_TYPE_BY_LAYER = new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG}; private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000}; - private static final int[] BITRATE_V1_L1 = - {32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448}; - private static final int[] BITRATE_V2_L1 = - {32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256}; - private static final int[] BITRATE_V1_L2 = - {32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384}; - private static final int[] BITRATE_V1_L3 = - {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320}; - private static final int[] BITRATE_V2 = - {8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}; + private static final int[] BITRATE_V1_L1 = { + 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000, + 416000, 448000 + }; + private static final int[] BITRATE_V2_L1 = { + 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000, + 224000, 256000 + }; + private static final int[] BITRATE_V1_L2 = { + 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, + 320000, 384000 + }; + private static final int[] BITRATE_V1_L3 = { + 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, + 320000 + }; + private static final int[] BITRATE_V2 = { + 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, + 160000 + }; /** * Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it @@ -89,7 +99,7 @@ public final class MpegAudioHeader { if (layer == 3) { // Layer I (layer == 3) bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; - return (12000 * bitrate / samplingRate + padding) * 4; + return (12 * bitrate / samplingRate + padding) * 4; } else { // Layer II (layer == 2) or III (layer == 1) if (version == 3) { @@ -102,10 +112,10 @@ public final class MpegAudioHeader { if (version == 3) { // Version 1 - return 144000 * bitrate / samplingRate + padding; + return 144 * bitrate / samplingRate + padding; } else { // Version 2 or 2.5 - return (layer == 1 ? 72000 : 144000) * bitrate / samplingRate + padding; + return (layer == 1 ? 72 : 144) * bitrate / samplingRate + padding; } } @@ -159,7 +169,7 @@ public final class MpegAudioHeader { if (layer == 3) { // Layer I (layer == 3) bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; - frameSize = (12000 * bitrate / sampleRate + padding) * 4; + frameSize = (12 * bitrate / sampleRate + padding) * 4; samplesPerFrame = 384; } else { // Layer II (layer == 2) or III (layer == 1) @@ -167,19 +177,22 @@ public final class MpegAudioHeader { // Version 1 bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; samplesPerFrame = 1152; - frameSize = 144000 * bitrate / sampleRate + padding; + frameSize = 144 * bitrate / sampleRate + padding; } else { // Version 2 or 2.5. bitrate = BITRATE_V2[bitrateIndex - 1]; samplesPerFrame = layer == 1 ? 576 : 1152; - frameSize = (layer == 1 ? 72000 : 144000) * bitrate / sampleRate + padding; + frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding; } } + // Calculate the bitrate in the same way Mp3Extractor calculates sample timestamps so that + // seeking to a given timestamp and playing from the start up to that timestamp give the same + // results for CBR streams. See also [internal: b/120390268]. + bitrate = 8 * frameSize * sampleRate / samplesPerFrame; String mimeType = MIME_TYPE_BY_LAYER[3 - layer]; int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; - header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate * 1000, - samplesPerFrame); + header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame); return true; } @@ -198,8 +211,14 @@ public final class MpegAudioHeader { /** Number of samples stored in the frame. */ public int samplesPerFrame; - private void setValues(int version, String mimeType, int frameSize, int sampleRate, int channels, - int bitrate, int samplesPerFrame) { + private void setValues( + int version, + String mimeType, + int frameSize, + int sampleRate, + int channels, + int bitrate, + int samplesPerFrame) { this.version = version; this.mimeType = mimeType; this.frameSize = frameSize; diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump index 96b0cd259c..d4df3ffeba 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.0.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26125 + duration = 26122 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump index 96b0cd259c..d4df3ffeba 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.1.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26125 + duration = 26122 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump index 96b0cd259c..d4df3ffeba 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.2.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26125 + duration = 26122 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: diff --git a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump index 96b0cd259c..d4df3ffeba 100644 --- a/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/test/assets/mp3/play-trimmed.mp3.3.dump @@ -1,6 +1,6 @@ seekMap: isSeekable = true - duration = 26125 + duration = 26122 getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: From ca9ecaa4480fc74ebd5c32b07e4fd17be10da21c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 19 Dec 2018 19:30:09 +0000 Subject: [PATCH 17/24] Blacklist OMX.SEC.mp3.dec for more devices Issue #4519 PiperOrigin-RevId: 226205245 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index f6cc6a8344..4d971d461e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -319,19 +319,20 @@ public final class MediaCodecUtil { // Work around https://github.com/google/ExoPlayer/issues/4519. if ("OMX.SEC.mp3.dec".equals(name) - && ("GT-I9152".equals(Util.MODEL) - || "GT-I9515".equals(Util.MODEL) - || "GT-P5220".equals(Util.MODEL) - || "GT-S7580".equals(Util.MODEL) - || "SM-G350".equals(Util.MODEL) - || "SM-T231".equals(Util.MODEL) - || "SM-T530".equals(Util.MODEL))) { + && (Util.MODEL.startsWith("GT-I9152") + || Util.MODEL.startsWith("GT-I9515") + || Util.MODEL.startsWith("GT-P5220") + || Util.MODEL.startsWith("GT-S7580") + || Util.MODEL.startsWith("SM-G350") + || Util.MODEL.startsWith("SM-G386") + || Util.MODEL.startsWith("SM-T231") + || Util.MODEL.startsWith("SM-T530"))) { return false; } if ("OMX.brcm.audio.mp3.decoder".equals(name) - && ("GT-I9152".equals(Util.MODEL) - || "GT-S7580".equals(Util.MODEL) - || "SM-G350".equals(Util.MODEL))) { + && (Util.MODEL.startsWith("GT-I9152") + || Util.MODEL.startsWith("GT-S7580") + || Util.MODEL.startsWith("SM-G350"))) { return false; } From fa82004fa3423021a25149bce5f2a50e884ab804 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 14 Dec 2018 15:42:57 +0000 Subject: [PATCH 18/24] Merge pull request #5066 from szaboa:feature/1583_support_png_ttml PiperOrigin-RevId: 225531695 --- .../exoplayer2/text/ttml/TtmlDecoder.java | 140 ++++++++++++-- .../exoplayer2/text/ttml/TtmlNode.java | 117 +++++++++--- .../exoplayer2/text/ttml/TtmlSubtitle.java | 11 +- .../assets/ttml/bitmap_percentage_region.xml | 26 +++ .../test/assets/ttml/bitmap_pixel_region.xml | 23 +++ .../assets/ttml/bitmap_unsupported_region.xml | 23 +++ .../exoplayer2/text/ttml/TtmlDecoderTest.java | 178 +++++++++++++----- 7 files changed, 429 insertions(+), 89 deletions(-) create mode 100644 library/core/src/test/assets/ttml/bitmap_percentage_region.xml create mode 100644 library/core/src/test/assets/ttml/bitmap_pixel_region.xml create mode 100644 library/core/src/test/assets/ttml/bitmap_unsupported_region.xml diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index 2e868077a5..b39f467968 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -68,6 +68,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { private static final String ATTR_END = "end"; private static final String ATTR_STYLE = "style"; private static final String ATTR_REGION = "region"; + private static final String ATTR_IMAGE = "backgroundImage"; private static final Pattern CLOCK_TIME = Pattern.compile("^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])" @@ -77,6 +78,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$"); private static final Pattern PERCENTAGE_COORDINATES = Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$"); + private static final Pattern PIXEL_COORDINATES = + Pattern.compile("^(\\d+\\.?\\d*?)px (\\d+\\.?\\d*?)px$"); private static final Pattern CELL_RESOLUTION = Pattern.compile("^(\\d+) (\\d+)$"); private static final int DEFAULT_FRAME_RATE = 30; @@ -105,6 +108,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); Map globalStyles = new HashMap<>(); Map regionMap = new HashMap<>(); + Map imageMap = new HashMap<>(); regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null)); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length); xmlParser.setInput(inputStream, null); @@ -114,6 +118,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { int eventType = xmlParser.getEventType(); FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE; CellResolution cellResolution = DEFAULT_CELL_RESOLUTION; + TtsExtent ttsExtent = null; while (eventType != XmlPullParser.END_DOCUMENT) { TtmlNode parent = nodeStack.peek(); if (unsupportedNodeDepth == 0) { @@ -122,12 +127,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { if (TtmlNode.TAG_TT.equals(name)) { frameAndTickRate = parseFrameAndTickRates(xmlParser); cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION); + ttsExtent = parseTtsExtent(xmlParser); } if (!isSupportedTag(name)) { Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); unsupportedNodeDepth++; } else if (TtmlNode.TAG_HEAD.equals(name)) { - parseHeader(xmlParser, globalStyles, regionMap, cellResolution); + parseHeader(xmlParser, globalStyles, cellResolution, ttsExtent, regionMap, imageMap); } else { try { TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate); @@ -145,7 +151,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { parent.addChild(TtmlNode.buildTextNode(xmlParser.getText())); } else if (eventType == XmlPullParser.END_TAG) { if (xmlParser.getName().equals(TtmlNode.TAG_TT)) { - ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap); + ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap, imageMap); } nodeStack.pop(); } @@ -226,11 +232,34 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } } + private TtsExtent parseTtsExtent(XmlPullParser xmlParser) { + String ttsExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); + if (ttsExtent == null) { + return null; + } + + Matcher extentMatcher = PIXEL_COORDINATES.matcher(ttsExtent); + if (!extentMatcher.matches()) { + Log.w(TAG, "Ignoring non-pixel tts extent: " + ttsExtent); + return null; + } + try { + int width = Integer.parseInt(extentMatcher.group(1)); + int height = Integer.parseInt(extentMatcher.group(2)); + return new TtsExtent(width, height); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed tts extent: " + ttsExtent); + return null; + } + } + private Map parseHeader( XmlPullParser xmlParser, Map globalStyles, + CellResolution cellResolution, + TtsExtent ttsExtent, Map globalRegions, - CellResolution cellResolution) + Map imageMap) throws IOException, XmlPullParserException { do { xmlParser.next(); @@ -246,23 +275,41 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { globalStyles.put(style.getId(), style); } } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { - TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution); + TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution, ttsExtent); if (ttmlRegion != null) { globalRegions.put(ttmlRegion.id, ttmlRegion); } + } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_METADATA)) { + parseMetadata(xmlParser, imageMap); } } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD)); return globalStyles; } + private void parseMetadata(XmlPullParser xmlParser, Map imageMap) + throws IOException, XmlPullParserException { + do { + xmlParser.next(); + if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_IMAGE)) { + String id = XmlPullParserUtil.getAttributeValue(xmlParser, "id"); + if (id != null) { + String encodedBitmapData = xmlParser.nextText(); + imageMap.put(id, encodedBitmapData); + } + } + } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_METADATA)); + } + /** * Parses a region declaration. * - *

    If the region defines an origin and extent, it is required that they're defined as - * percentages of the viewport. Region declarations that define origin and extent in other formats - * are unsupported, and null is returned. + *

    Supports both percentage and pixel defined regions. In case of pixel defined regions the + * passed {@code ttsExtent} is used as a reference window to convert the pixel values to + * fractions. In case of missing tts:extent the pixel defined regions can't be parsed, and null is + * returned. */ - private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser, CellResolution cellResolution) { + private TtmlRegion parseRegionAttributes( + XmlPullParser xmlParser, CellResolution cellResolution, TtsExtent ttsExtent) { String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); if (regionId == null) { return null; @@ -270,13 +317,30 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { float position; float line; + String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN); if (regionOrigin != null) { - Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); - if (originMatcher.matches()) { + Matcher originPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); + Matcher originPixelMatcher = PIXEL_COORDINATES.matcher(regionOrigin); + if (originPercentageMatcher.matches()) { try { - position = Float.parseFloat(originMatcher.group(1)) / 100f; - line = Float.parseFloat(originMatcher.group(2)) / 100f; + position = Float.parseFloat(originPercentageMatcher.group(1)) / 100f; + line = Float.parseFloat(originPercentageMatcher.group(2)) / 100f; + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin); + return null; + } + } else if (originPixelMatcher.matches()) { + if (ttsExtent == null) { + Log.w(TAG, "Ignoring region with missing tts:extent: " + regionOrigin); + return null; + } + try { + int width = Integer.parseInt(originPixelMatcher.group(1)); + int height = Integer.parseInt(originPixelMatcher.group(2)); + // Convert pixel values to fractions. + position = width / (float) ttsExtent.width; + line = height / (float) ttsExtent.height; } catch (NumberFormatException e) { Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin); return null; @@ -299,11 +363,27 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { float height; String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); if (regionExtent != null) { - Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent); - if (extentMatcher.matches()) { + Matcher extentPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent); + Matcher extentPixelMatcher = PIXEL_COORDINATES.matcher(regionExtent); + if (extentPercentageMatcher.matches()) { try { - width = Float.parseFloat(extentMatcher.group(1)) / 100f; - height = Float.parseFloat(extentMatcher.group(2)) / 100f; + width = Float.parseFloat(extentPercentageMatcher.group(1)) / 100f; + height = Float.parseFloat(extentPercentageMatcher.group(2)) / 100f; + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin); + return null; + } + } else if (extentPixelMatcher.matches()) { + if (ttsExtent == null) { + Log.w(TAG, "Ignoring region with missing tts:extent: " + regionOrigin); + return null; + } + try { + int extentWidth = Integer.parseInt(extentPixelMatcher.group(1)); + int extentHeight = Integer.parseInt(extentPixelMatcher.group(2)); + // Convert pixel values to fractions. + width = extentWidth / (float) ttsExtent.width; + height = extentHeight / (float) ttsExtent.height; } catch (NumberFormatException e) { Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin); return null; @@ -457,6 +537,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { long startTime = C.TIME_UNSET; long endTime = C.TIME_UNSET; String regionId = TtmlNode.ANONYMOUS_REGION_ID; + String imageId = null; String[] styleIds = null; int attributeCount = parser.getAttributeCount(); TtmlStyle style = parseStyleAttributes(parser, null); @@ -487,6 +568,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { regionId = value; } break; + case ATTR_IMAGE: + // Parse URI reference only if refers to an element in the same document (it must start + // with '#'). Resolving URIs from external sources is not supported. + if (value.startsWith("#")) { + imageId = value.substring(1); + } + break; default: // Do nothing. break; @@ -509,7 +597,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { endTime = parent.endTimeUs; } } - return TtmlNode.buildNode(parser.getName(), startTime, endTime, style, styleIds, regionId); + return TtmlNode.buildNode( + parser.getName(), startTime, endTime, style, styleIds, regionId, imageId); } private static boolean isSupportedTag(String tag) { @@ -525,9 +614,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { || tag.equals(TtmlNode.TAG_LAYOUT) || tag.equals(TtmlNode.TAG_REGION) || tag.equals(TtmlNode.TAG_METADATA) - || tag.equals(TtmlNode.TAG_SMPTE_IMAGE) - || tag.equals(TtmlNode.TAG_SMPTE_DATA) - || tag.equals(TtmlNode.TAG_SMPTE_INFORMATION); + || tag.equals(TtmlNode.TAG_IMAGE) + || tag.equals(TtmlNode.TAG_DATA) + || tag.equals(TtmlNode.TAG_INFORMATION); } private static void parseFontSize(String expression, TtmlStyle out) throws @@ -651,4 +740,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { this.rows = rows; } } + + /** Represents the tts:extent for a TTML file. */ + private static final class TtsExtent { + final int width; + final int height; + + TtsExtent(int width, int height) { + this.width = width; + this.height = height; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java index c8b9a59de4..020bbe201b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -15,7 +15,12 @@ */ package com.google.android.exoplayer2.text.ttml; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.support.annotation.Nullable; import android.text.SpannableStringBuilder; +import android.util.Base64; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Assertions; @@ -44,9 +49,9 @@ import java.util.TreeSet; public static final String TAG_LAYOUT = "layout"; public static final String TAG_REGION = "region"; public static final String TAG_METADATA = "metadata"; - public static final String TAG_SMPTE_IMAGE = "smpte:image"; - public static final String TAG_SMPTE_DATA = "smpte:data"; - public static final String TAG_SMPTE_INFORMATION = "smpte:information"; + public static final String TAG_IMAGE = "image"; + public static final String TAG_DATA = "data"; + public static final String TAG_INFORMATION = "information"; public static final String ANONYMOUS_REGION_ID = ""; public static final String ATTR_ID = "id"; @@ -75,34 +80,57 @@ import java.util.TreeSet; public static final String START = "start"; public static final String END = "end"; - public final String tag; - public final String text; + @Nullable public final String tag; + @Nullable public final String text; public final boolean isTextNode; public final long startTimeUs; public final long endTimeUs; - public final TtmlStyle style; + @Nullable public final TtmlStyle style; + @Nullable private final String[] styleIds; public final String regionId; + @Nullable public final String imageId; - private final String[] styleIds; private final HashMap nodeStartsByRegion; private final HashMap nodeEndsByRegion; private List children; public static TtmlNode buildTextNode(String text) { - return new TtmlNode(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), C.TIME_UNSET, - C.TIME_UNSET, null, null, ANONYMOUS_REGION_ID); + return new TtmlNode( + /* tag= */ null, + TtmlRenderUtil.applyTextElementSpacePolicy(text), + /* startTimeUs= */ C.TIME_UNSET, + /* endTimeUs= */ C.TIME_UNSET, + /* style= */ null, + /* styleIds= */ null, + ANONYMOUS_REGION_ID, + /* imageId= */ null); } - public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs, - TtmlStyle style, String[] styleIds, String regionId) { - return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds, regionId); + public static TtmlNode buildNode( + @Nullable String tag, + long startTimeUs, + long endTimeUs, + @Nullable TtmlStyle style, + @Nullable String[] styleIds, + String regionId, + @Nullable String imageId) { + return new TtmlNode( + tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId); } - private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs, - TtmlStyle style, String[] styleIds, String regionId) { + private TtmlNode( + @Nullable String tag, + @Nullable String text, + long startTimeUs, + long endTimeUs, + @Nullable TtmlStyle style, + @Nullable String[] styleIds, + String regionId, + @Nullable String imageId) { this.tag = tag; this.text = text; + this.imageId = imageId; this.style = style; this.styleIds = styleIds; this.isTextNode = text != null; @@ -151,7 +179,8 @@ import java.util.TreeSet; private void getEventTimes(TreeSet out, boolean descendsPNode) { boolean isPNode = TAG_P.equals(tag); - if (descendsPNode || isPNode) { + boolean isDivNode = TAG_DIV.equals(tag); + if (descendsPNode || isPNode || (isDivNode && imageId != null)) { if (startTimeUs != C.TIME_UNSET) { out.add(startTimeUs); } @@ -171,13 +200,46 @@ import java.util.TreeSet; return styleIds; } - public List getCues(long timeUs, Map globalStyles, - Map regionMap) { - TreeMap regionOutputs = new TreeMap<>(); - traverseForText(timeUs, false, regionId, regionOutputs); - traverseForStyle(timeUs, globalStyles, regionOutputs); + public List getCues( + long timeUs, + Map globalStyles, + Map regionMap, + Map imageMap) { + + List> regionImageOutputs = new ArrayList<>(); + traverseForImage(timeUs, regionId, regionImageOutputs); + + TreeMap regionTextOutputs = new TreeMap<>(); + traverseForText(timeUs, false, regionId, regionTextOutputs); + traverseForStyle(timeUs, globalStyles, regionTextOutputs); + List cues = new ArrayList<>(); - for (Entry entry : regionOutputs.entrySet()) { + + // Create image based cues. + for (Pair regionImagePair : regionImageOutputs) { + String encodedBitmapData = imageMap.get(regionImagePair.second); + if (encodedBitmapData == null) { + // Image reference points to an invalid image. Do nothing. + continue; + } + + byte[] bitmapData = Base64.decode(encodedBitmapData, Base64.DEFAULT); + Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, /* offset= */ 0, bitmapData.length); + TtmlRegion region = regionMap.get(regionImagePair.first); + + cues.add( + new Cue( + bitmap, + region.position, + Cue.ANCHOR_TYPE_MIDDLE, + region.line, + region.lineAnchor, + region.width, + /* height= */ Cue.DIMEN_UNSET)); + } + + // Create text based cues. + for (Entry entry : regionTextOutputs.entrySet()) { TtmlRegion region = regionMap.get(entry.getKey()); cues.add( new Cue( @@ -192,9 +254,22 @@ import java.util.TreeSet; region.textSizeType, region.textSize)); } + return cues; } + private void traverseForImage( + long timeUs, String inheritedRegion, List> regionImageList) { + String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId; + if (isActive(timeUs) && TAG_DIV.equals(tag) && imageId != null) { + regionImageList.add(new Pair<>(resolvedRegionId, imageId)); + return; + } + for (int i = 0; i < getChildCount(); ++i) { + getChild(i).traverseForImage(timeUs, resolvedRegionId, regionImageList); + } + } + private void traverseForText( long timeUs, boolean descendsPNode, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java index 50916aa841..7b30461750 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java @@ -32,11 +32,16 @@ import java.util.Map; private final long[] eventTimesUs; private final Map globalStyles; private final Map regionMap; + private final Map imageMap; - public TtmlSubtitle(TtmlNode root, Map globalStyles, - Map regionMap) { + public TtmlSubtitle( + TtmlNode root, + Map globalStyles, + Map regionMap, + Map imageMap) { this.root = root; this.regionMap = regionMap; + this.imageMap = imageMap; this.globalStyles = globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap(); this.eventTimesUs = root.getEventTimesUs(); @@ -65,7 +70,7 @@ import java.util.Map; @Override public List getCues(long timeUs) { - return root.getCues(timeUs, globalStyles, regionMap); + return root.getCues(timeUs, globalStyles, regionMap, imageMap); } /* @VisibleForTesting */ diff --git a/library/core/src/test/assets/ttml/bitmap_percentage_region.xml b/library/core/src/test/assets/ttml/bitmap_percentage_region.xml new file mode 100644 index 0000000000..9631650178 --- /dev/null +++ b/library/core/src/test/assets/ttml/bitmap_percentage_region.xml @@ -0,0 +1,26 @@ + + + + + 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 + + + +