diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6b3fbc4f27..1699f2c09b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,24 @@ # Release notes # +### 2.9.3 ### + +* Captions: Support PNG subtitles in SMPTE-TT + ([#1583](https://github.com/google/ExoPlayer/issues/1583)). +* MPEG-TS: Use random access indicators to minimize the need for + `FLAG_ALLOW_NON_IDR_KEYFRAMES`. +* Downloading: Reduce time taken to remove downloads + ([#5136](https://github.com/google/ExoPlayer/issues/5136)). +* MP3: + * Use the true bitrate for constant-bitrate MP3 seeking. + * Fix issue where streams would play twice on some Samsung devices + ([#4519](https://github.com/google/ExoPlayer/issues/4519)). +* Fix regression where some audio formats were incorrectly marked as being + unplayable due to under-reporting of platform decoder capabilities + ([#5145](https://github.com/google/ExoPlayer/issues/5145)). +* Fix decode-only frame skipping on Nvidia Shield TV devices. +* Workaround for MiTV (dangal) issue when swapping output surface + ([#5169](https://github.com/google/ExoPlayer/issues/5169)). + ### 2.9.2 ### * HLS: @@ -47,10 +66,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 diff --git a/constants.gradle b/constants.gradle index cac4f6d78b..ac801d2d3b 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.9.2' - releaseVersionCode = 2009002 + releaseVersion = '2.9.3' + releaseVersionCode = 2009003 // Important: ExoPlayer specifies a minSdkVersion of 14 because various // components provided by the library may be of use on older devices. // However, please note that the core media playback functionality provided diff --git a/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/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/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}. * 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/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index c30fe160c9..792f6cf651 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.9.2"; + public static final String VERSION = "2.9.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.9.2"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.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 = 2009002; + public static final int VERSION_INT = 2009003; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} 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/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/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/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 93ce15a7ab..3741d52294 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor.ts; +import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.extractor.Extractor; @@ -140,7 +142,7 @@ public final class Ac3Extractor implements Extractor { if (!startedPacket) { // Pass data to the reader as though it's contained within a single infinitely long packet. - reader.packetStarted(firstSampleTimestampUs, true); + reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR); startedPacket = true; } // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index 2ef9704a7a..93724be92d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -100,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { timeUs = pesTimeUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index 04a6b571bd..77b79fa19f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor.ts; +import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; + import android.support.annotation.IntDef; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -202,7 +204,7 @@ public final class AdtsExtractor implements Extractor { if (!startedPacket) { // Pass data to the reader as though it's contained within a single infinitely long packet. - reader.packetStarted(firstSampleTimestampUs, true); + reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR); startedPacket = true; } // TODO: Make it possible for reader to consume the dataSource directly, so that it becomes diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index e31f67c77c..589b543170 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -141,7 +141,7 @@ public final class AdtsReader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { timeUs = pesTimeUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index 2e45853951..1f9b0e79d4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -80,7 +80,7 @@ public final class DtsReader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { timeUs = pesTimeUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java index 0944d1810e..3f0a772b1c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor.ts; +import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -73,8 +75,8 @@ public final class DvbSubtitleReader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { - if (!dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) { return; } writingSample = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java index fa7f78c8c0..e022fc237b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java @@ -43,9 +43,9 @@ public interface ElementaryStreamReader { * Called when a packet starts. * * @param pesTimeUs The timestamp associated with the packet. - * @param dataAlignmentIndicator The data alignment indicator associated with the packet. + * @param flags See {@link TsPayloadReader.Flags}. */ - void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator); + void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags); /** * Consumes (possibly partial) data from the current packet. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index e9827893ee..1564157d44 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -107,7 +107,8 @@ public final class H262Reader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + // TODO (Internal b/32267012): Consider using random access indicator. this.pesTimeUs = pesTimeUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index 45e094f69d..d249c1b9da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor.ts; +import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR; + import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -56,9 +58,12 @@ public final class H264Reader implements ElementaryStreamReader { // State that should not be reset on seek. private boolean hasOutputFormat; - // Per packet state that gets reset at the start of each packet. + // Per PES packet state that gets reset at the start of each PES packet. private long pesTimeUs; + // State inherited from the TS packet header. + private boolean randomAccessIndicator; + // Scratch variables to avoid allocations. private final ParsableByteArray seiWrapper; @@ -88,6 +93,7 @@ public final class H264Reader implements ElementaryStreamReader { sei.reset(); sampleReader.reset(); totalBytesWritten = 0; + randomAccessIndicator = false; } @Override @@ -100,8 +106,9 @@ public final class H264Reader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { this.pesTimeUs = pesTimeUs; + randomAccessIndicator |= (flags & FLAG_RANDOM_ACCESS_INDICATOR) != 0; } @Override @@ -220,12 +227,17 @@ public final class H264Reader implements ElementaryStreamReader { seiWrapper.setPosition(4); // NAL prefix and nal_unit() header. seiReader.consume(pesTimeUs, seiWrapper); } - sampleReader.endNalUnit(position, offset); + boolean sampleIsKeyFrame = + sampleReader.endNalUnit(position, offset, hasOutputFormat, randomAccessIndicator); + if (sampleIsKeyFrame) { + // This is either an IDR frame or the first I-frame since the random access indicator, so mark + // it as a keyframe. Clear the flag so that subsequent non-IDR I-frames are not marked as + // keyframes until we see another random access indicator. + randomAccessIndicator = false; + } } - /** - * Consumes a stream of NAL units and outputs samples. - */ + /** Consumes a stream of NAL units and outputs samples. */ private static final class SampleReader { private static final int DEFAULT_BUFFER_SIZE = 128; @@ -430,11 +442,12 @@ public final class H264Reader implements ElementaryStreamReader { isFilling = false; } - public void endNalUnit(long position, int offset) { + public boolean endNalUnit( + long position, int offset, boolean hasOutputFormat, boolean randomAccessIndicator) { if (nalUnitType == NAL_UNIT_TYPE_AUD || (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) { // If the NAL unit ending is the start of a new sample, output the previous one. - if (readingSample) { + if (hasOutputFormat && readingSample) { int nalUnitLength = (int) (position - nalUnitStartPosition); outputSample(offset + nalUnitLength); } @@ -443,8 +456,12 @@ public final class H264Reader implements ElementaryStreamReader { sampleIsKeyframe = false; readingSample = true; } - sampleIsKeyframe |= nalUnitType == NAL_UNIT_TYPE_IDR || (allowNonIdrKeyframes - && nalUnitType == NAL_UNIT_TYPE_NON_IDR && sliceHeader.isISlice()); + boolean treatIFrameAsKeyframe = + allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator; + sampleIsKeyframe |= + nalUnitType == NAL_UNIT_TYPE_IDR + || (treatIFrameAsKeyframe && nalUnitType == NAL_UNIT_TYPE_NON_IDR); + return sampleIsKeyframe; } private void outputSample(int offset) { @@ -486,10 +503,21 @@ public final class H264Reader implements ElementaryStreamReader { hasSliceType = true; } - public void setAll(SpsData spsData, int nalRefIdc, int sliceType, int frameNum, - int picParameterSetId, boolean fieldPicFlag, boolean bottomFieldFlagPresent, - boolean bottomFieldFlag, boolean idrPicFlag, int idrPicId, int picOrderCntLsb, - int deltaPicOrderCntBottom, int deltaPicOrderCnt0, int deltaPicOrderCnt1) { + public void setAll( + SpsData spsData, + int nalRefIdc, + int sliceType, + int frameNum, + int picParameterSetId, + boolean fieldPicFlag, + boolean bottomFieldFlagPresent, + boolean bottomFieldFlag, + boolean idrPicFlag, + int idrPicId, + int picOrderCntLsb, + int deltaPicOrderCntBottom, + int deltaPicOrderCnt0, + int deltaPicOrderCnt1) { this.spsData = spsData; this.nalRefIdc = nalRefIdc; this.sliceType = sliceType; @@ -514,23 +542,26 @@ public final class H264Reader implements ElementaryStreamReader { private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) { // See ISO 14496-10 subsection 7.4.1.2.4. - return isComplete && (!other.isComplete || frameNum != other.frameNum - || picParameterSetId != other.picParameterSetId || fieldPicFlag != other.fieldPicFlag - || (bottomFieldFlagPresent && other.bottomFieldFlagPresent - && bottomFieldFlag != other.bottomFieldFlag) - || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) - || (spsData.picOrderCountType == 0 && other.spsData.picOrderCountType == 0 - && (picOrderCntLsb != other.picOrderCntLsb - || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) - || (spsData.picOrderCountType == 1 && other.spsData.picOrderCountType == 1 - && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 - || deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) - || idrPicFlag != other.idrPicFlag - || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId)); + return isComplete + && (!other.isComplete + || frameNum != other.frameNum + || picParameterSetId != other.picParameterSetId + || fieldPicFlag != other.fieldPicFlag + || (bottomFieldFlagPresent + && other.bottomFieldFlagPresent + && bottomFieldFlag != other.bottomFieldFlag) + || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) + || (spsData.picOrderCountType == 0 + && other.spsData.picOrderCountType == 0 + && (picOrderCntLsb != other.picOrderCntLsb + || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) + || (spsData.picOrderCountType == 1 + && other.spsData.picOrderCountType == 1 + && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 + || deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) + || idrPicFlag != other.idrPicFlag + || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId)); } - } - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 13d679c47c..88bde53746 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -104,7 +104,8 @@ public final class H265Reader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + // TODO (Internal b/32267012): Consider using random access indicator. this.pesTimeUs = pesTimeUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index 0f0f2ad981..f936fb9e43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor.ts; +import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -63,8 +65,8 @@ public final class Id3Reader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { - if (!dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { + if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) { return; } writingSample = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java index f401a6e736..2a633c191d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -93,7 +93,7 @@ public final class LatmReader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { timeUs = pesTimeUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index effa7d7c96..393e297818 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -83,7 +83,7 @@ public final class MpegAudioReader implements ElementaryStreamReader { } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { timeUs = pesTimeUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index 91cd548367..ff755f4ece 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -78,9 +78,8 @@ public final class PesReader implements TsPayloadReader { } @Override - public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) - throws ParserException { - if (payloadUnitStartIndicator) { + public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException { + if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) { switch (state) { case STATE_FINDING_HEADER: case STATE_READING_HEADER: @@ -122,7 +121,8 @@ public final class PesReader implements TsPayloadReader { if (continueRead(data, pesScratch.data, readLength) && continueRead(data, null, extendedHeaderLength)) { parseHeaderExtension(); - reader.packetStarted(timeUs, dataAlignmentIndicator); + flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0; + reader.packetStarted(timeUs, flags); setState(STATE_READING_BODY); } break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index c7a082aeac..f453a9cc43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -343,7 +343,7 @@ public final class PsExtractor implements Extractor { data.readBytes(pesScratch.data, 0, extendedHeaderLength); pesScratch.setPosition(0); parseHeaderExtension(); - pesPayloadReader.packetStarted(timeUs, true); + pesPayloadReader.packetStarted(timeUs, TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR); pesPayloadReader.consume(data); // We always have complete PES packets with program stream. pesPayloadReader.packetFinished(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java index d217cfcb7a..101a1f74d9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java @@ -57,7 +57,8 @@ public final class SectionReader implements TsPayloadReader { } @Override - public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + public void consume(ParsableByteArray data, @Flags int flags) { + boolean payloadUnitStartIndicator = (flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0; int payloadStartPosition = C.POSITION_UNSET; if (payloadUnitStartIndicator) { int payloadStartOffset = data.readUnsignedByte(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index f47a481d7e..d91842423d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor.ts; +import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_PAYLOAD_UNIT_START_INDICATOR; + import android.support.annotation.IntDef; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -279,6 +281,8 @@ public final class TsExtractor implements Extractor { return RESULT_CONTINUE; } + @TsPayloadReader.Flags int packetHeaderFlags = 0; + // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format. int tsPacketHeader = tsPacketBuffer.readInt(); if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator @@ -286,7 +290,7 @@ public final class TsExtractor implements Extractor { tsPacketBuffer.setPosition(endOfPacket); return RESULT_CONTINUE; } - boolean payloadUnitStartIndicator = (tsPacketHeader & 0x400000) != 0; + packetHeaderFlags |= (tsPacketHeader & 0x400000) != 0 ? FLAG_PAYLOAD_UNIT_START_INDICATOR : 0; // Ignoring transport_priority (tsPacketHeader & 0x200000) int pid = (tsPacketHeader & 0x1FFF00) >> 8; // Ignoring transport_scrambling_control (tsPacketHeader & 0xC0) @@ -317,14 +321,20 @@ public final class TsExtractor implements Extractor { // Skip the adaptation field. if (adaptationFieldExists) { int adaptationFieldLength = tsPacketBuffer.readUnsignedByte(); - tsPacketBuffer.skipBytes(adaptationFieldLength); + int adaptationFieldFlags = tsPacketBuffer.readUnsignedByte(); + + packetHeaderFlags |= + (adaptationFieldFlags & 0x40) != 0 // random_access_indicator. + ? TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR + : 0; + tsPacketBuffer.skipBytes(adaptationFieldLength - 1 /* flags */); } // Read the payload. boolean wereTracksEnded = tracksEnded; if (shouldConsumePacketPayload(pid)) { tsPacketBuffer.setLimit(endOfPacket); - payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); + payloadReader.consume(tsPacketBuffer, packetHeaderFlags); tsPacketBuffer.setLimit(limit); } if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index 2ea25bb2e0..a034b05696 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -15,12 +15,16 @@ */ package com.google.android.exoplayer2.extractor.ts; +import android.support.annotation.IntDef; import android.util.SparseArray; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; @@ -174,6 +178,29 @@ public interface TsPayloadReader { } + /** + * Contextual flags indicating the presence of indicators in the TS packet or PES packet headers. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + FLAG_PAYLOAD_UNIT_START_INDICATOR, + FLAG_RANDOM_ACCESS_INDICATOR, + FLAG_DATA_ALIGNMENT_INDICATOR + }) + @interface Flags {} + + /** Indicates the presence of the payload_unit_start_indicator in the TS packet header. */ + int FLAG_PAYLOAD_UNIT_START_INDICATOR = 1; + /** + * Indicates the presence of the random_access_indicator in the TS packet header adaptation field. + */ + int FLAG_RANDOM_ACCESS_INDICATOR = 1 << 1; + /** Indicates the presence of the data_alignment_indicator in the PES header. */ + int FLAG_DATA_ALIGNMENT_INDICATOR = 1 << 2; + /** * Initializes the payload reader. * @@ -187,10 +214,10 @@ public interface TsPayloadReader { /** * Notifies the reader that a seek has occurred. - *

- * Following a call to this method, the data passed to the next invocation of - * {@link #consume(ParsableByteArray, boolean)} will not be a continuation of the data that was - * previously passed. Hence the reader should reset any internal state. + * + *

Following a call to this method, the data passed to the next invocation of {@link #consume} + * will not be a continuation of the data that was previously passed. Hence the reader should + * reset any internal state. */ void seek(); @@ -198,9 +225,8 @@ public interface TsPayloadReader { * Consumes the payload of a TS packet. * * @param data The TS packet. The position will be set to the start of the payload. - * @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet. + * @param flags See {@link Flags}. * @throws ParserException If the payload could not be parsed. */ - void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) throws ParserException; - + void consume(ParsableByteArray data, @Flags int flags) throws ParserException; } 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 32f6bd5409..8cec75a66d 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 @@ -248,9 +248,15 @@ public final class MediaCodecInfo { // If we don't know any better, we assume that the profile and level are supported. return true; } + int profile = codecProfileAndLevel.first; + int level = codecProfileAndLevel.second; + if (!isVideo && profile != CodecProfileLevel.AACObjectXHE) { + // Some devices/builds under-report audio capabilities, so assume support except for xHE-AAC + // which is not widely supported. See https://github.com/google/ExoPlayer/issues/5145. + return true; + } for (CodecProfileLevel capabilities : getProfileLevels()) { - if (capabilities.profile == codecProfileAndLevel.first - && capabilities.level >= codecProfileAndLevel.second) { + if (capabilities.profile == profile && capabilities.level >= level) { return true; } } 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))); } 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..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 @@ -318,7 +318,21 @@ 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) + && (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) + && (Util.MODEL.startsWith("GT-I9152") + || Util.MODEL.startsWith("GT-S7580") + || Util.MODEL.startsWith("SM-G350"))) { return false; } 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..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 @@ -453,6 +453,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); this.uid = new Object(); } @@ -945,10 +951,18 @@ 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/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/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index a769e9acac..b5b5dc64e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -82,7 +82,7 @@ public interface Cache { * Releases the cache. This method must be called when the cache is no longer required. The cache * must not be used after calling this method. */ - void release() throws CacheException; + void release(); /** * Registers a listener to listen for changes to a given key. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 3bcfac5053..43e6730844 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.util.SparseArray; +import android.util.SparseBooleanArray; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.AtomicFile; @@ -41,6 +42,7 @@ import javax.crypto.CipherOutputStream; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** Maintains the index of cached content. */ /*package*/ class CachedContentIndex { @@ -52,7 +54,30 @@ import javax.crypto.spec.SecretKeySpec; private static final int FLAG_ENCRYPTED_INDEX = 1; private final HashMap keyToContent; - private final SparseArray idToKey; + /** + * Maps assigned ids to their corresponding keys. Also contains (id -> null) entries for ids that + * have been removed from the index since it was last stored. This prevents reuse of these ids, + * which is necessary to avoid clashes that could otherwise occur as a result of the sequence: + * + *

[1] (key1, id1) is removed from the in-memory index ... the index is not stored to disk ... + * [2] id1 is reused for a different key2 ... the index is not stored to disk ... [3] A file for + * key2 is partially written using a path corresponding to id1 ... the process is killed before + * the index is stored to disk ... [4] The index is read from disk, causing the partially written + * file to be incorrectly associated to key1 + * + *

By avoiding id reuse in step [2], a new id2 will be used instead. Step [4] will then delete + * the partially written file because the index does not contain an entry for id2. + * + *

When the index is next stored (id -> null) entries are removed, making the ids eligible for + * reuse. + */ + private final SparseArray<@NullableType String> idToKey; + /** + * Tracks ids for which (id -> null) entries are present in idToKey, so that they can be removed + * efficiently when the index is next stored. + */ + private final SparseBooleanArray removedIds; + private final AtomicFile atomicFile; private final Cipher cipher; private final SecretKeySpec secretKeySpec; @@ -104,6 +129,7 @@ import javax.crypto.spec.SecretKeySpec; } keyToContent = new HashMap<>(); idToKey = new SparseArray<>(); + removedIds = new SparseBooleanArray(); atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME)); } @@ -124,6 +150,12 @@ import javax.crypto.spec.SecretKeySpec; } writeFile(); changed = false; + // Make ids that were removed since the index was last stored eligible for re-use. + int removedIdCount = removedIds.size(); + for (int i = 0; i < removedIdCount; i++) { + idToKey.remove(removedIds.keyAt(i)); + } + removedIds.clear(); } /** @@ -168,8 +200,11 @@ import javax.crypto.spec.SecretKeySpec; CachedContent cachedContent = keyToContent.get(key); if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) { keyToContent.remove(key); - idToKey.remove(cachedContent.id); changed = true; + // Keep an entry in idToKey to stop the id from being reused until the index is next stored. + idToKey.put(cachedContent.id, /* value= */ null); + // Track that the entry should be removed from idToKey when the index is next stored. + removedIds.put(cachedContent.id, /* value= */ true); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index ca2983c891..ab60be2b4b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -146,13 +146,16 @@ public final class SimpleCache implements Cache { } @Override - public synchronized void release() throws CacheException { + public synchronized void release() { if (released) { return; } listeners.clear(); + removeStaleSpans(); try { - removeStaleSpansAndCachedContents(); + index.store(); + } catch (CacheException e) { + Log.e(TAG, "Storing index file failed", e); } finally { unlockFolder(cacheDir); released = true; @@ -265,7 +268,7 @@ public final class SimpleCache implements Cache { if (!cacheDir.exists()) { // For some reason the cache directory doesn't exist. Make a best effort to create it. cacheDir.mkdirs(); - removeStaleSpansAndCachedContents(); + removeStaleSpans(); } evictor.onStartFile(this, key, position, maxLength); return SimpleCacheSpan.getCacheFile( @@ -311,9 +314,9 @@ public final class SimpleCache implements Cache { } @Override - public synchronized void removeSpan(CacheSpan span) throws CacheException { + public synchronized void removeSpan(CacheSpan span) { Assertions.checkState(!released); - removeSpan(span, true); + removeSpanInternal(span); } @Override @@ -379,7 +382,7 @@ public final class SimpleCache implements Cache { if (span.isCached && !span.file.exists()) { // The file has been deleted from under us. It's likely that other files will have been // deleted too, so scan the whole in-memory representation. - removeStaleSpansAndCachedContents(); + removeStaleSpans(); continue; } return span; @@ -431,27 +434,21 @@ public final class SimpleCache implements Cache { notifySpanAdded(span); } - private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException { + private void removeSpanInternal(CacheSpan span) { CachedContent cachedContent = index.get(span.key); if (cachedContent == null || !cachedContent.removeSpan(span)) { return; } totalSpace -= span.length; - try { - if (removeEmptyCachedContent) { - index.maybeRemove(cachedContent.key); - index.store(); - } - } finally { - notifySpanRemoved(span); - } + index.maybeRemove(cachedContent.key); + notifySpanRemoved(span); } /** * Scans all of the cached spans in the in-memory representation, removing any for which files no * longer exist. */ - private void removeStaleSpansAndCachedContents() throws CacheException { + private void removeStaleSpans() { ArrayList spansToBeRemoved = new ArrayList<>(); for (CachedContent cachedContent : index.getAll()) { for (CacheSpan span : cachedContent.getSpans()) { @@ -461,11 +458,8 @@ public final class SimpleCache implements Cache { } } for (int i = 0; i < spansToBeRemoved.size(); i++) { - // Remove span but not CachedContent to prevent multiple index.store() calls. - removeSpan(spansToBeRemoved.get(i), false); + removeSpanInternal(spansToBeRemoved.get(i)); } - index.removeEmpty(); - index.store(); } private void notifySpanRemoved(CacheSpan span) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index eafb05226c..f7c045dbb0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -1436,11 +1436,12 @@ public final class Util { } /** - * Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C} - * {@code DEFAULT_*_BUFFER_SIZE} constant. + * Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C} {@code + * DEFAULT_*_BUFFER_SIZE} constant. * * @param trackType The track type. * @return The corresponding default buffer size in bytes. + * @throws IllegalArgumentException If the track type is an unrecognized or custom track type. */ public static int getDefaultBufferSize(int trackType) { switch (trackType) { @@ -1456,8 +1457,10 @@ public final class Util { return C.DEFAULT_METADATA_BUFFER_SIZE; case C.TRACK_TYPE_CAMERA_MOTION: return C.DEFAULT_CAMERA_MOTION_BUFFER_SIZE; + case C.TRACK_TYPE_NONE: + return 0; default: - throw new IllegalStateException(); + throw new IllegalArgumentException(); } } 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..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 @@ -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); } /* @@ -1296,163 +1297,171 @@ 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 "BRAVIA_ATV3_4K": + 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 "fugu": + 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; } 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}. 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: 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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 + + + +