Commit 71f72c59 by Oliver Woodman Committed by GitHub

Merge pull request #5283 from google/dev-v2-r2.9.3

r2.9.3
parents 6b0e1758 f042ae4c
Showing with 888 additions and 289 deletions
# Release notes # # 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 ### ### 2.9.2 ###
* HLS: * HLS:
...@@ -47,10 +66,10 @@ ...@@ -47,10 +66,10 @@
* DASH: Parse ProgramInformation element if present in the manifest. * DASH: Parse ProgramInformation element if present in the manifest.
* HLS: * HLS:
* Add constructor to `DefaultHlsExtractorFactory` for adding TS payload * 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 * Fix bug in segment sniffing
([#5039](https://github.com/google/ExoPlayer/issues/5039)). ([#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 * SubRip: Add support for alignment tags, and remove tags from the displayed
captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)). captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).
* Fix issue with blind seeking to windows with non-zero offset in a * Fix issue with blind seeking to windows with non-zero offset in a
......
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.9.2' releaseVersion = '2.9.3'
releaseVersionCode = 2009002 releaseVersionCode = 2009003
// Important: ExoPlayer specifies a minSdkVersion of 14 because various // Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices. // components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided // However, please note that the core media playback functionality provided
......
...@@ -283,21 +283,30 @@ public final class CastPlayer extends BasePlayer { ...@@ -283,21 +283,30 @@ public final class CastPlayer extends BasePlayer {
// Player implementation. // Player implementation.
@Override @Override
@Nullable
public AudioComponent getAudioComponent() { public AudioComponent getAudioComponent() {
return null; return null;
} }
@Override @Override
@Nullable
public VideoComponent getVideoComponent() { public VideoComponent getVideoComponent() {
return null; return null;
} }
@Override @Override
@Nullable
public TextComponent getTextComponent() { public TextComponent getTextComponent() {
return null; return null;
} }
@Override @Override
@Nullable
public MetadataComponent getMetadataComponent() {
return null;
}
@Override
public Looper getApplicationLooper() { public Looper getApplicationLooper() {
return Looper.getMainLooper(); return Looper.getMainLooper();
} }
......
...@@ -77,6 +77,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn ...@@ -77,6 +77,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn
} }
@Override @Override
@Nullable
public Object getTag() {
return adsMediaSource.getTag();
}
@Override
public void prepareSourceInternal( public void prepareSourceInternal(
final ExoPlayer player, final ExoPlayer player,
boolean isTopLevelSource, boolean isTopLevelSource,
......
...@@ -65,13 +65,6 @@ public final class TimelineQueueEditor ...@@ -65,13 +65,6 @@ public final class TimelineQueueEditor
*/ */
public interface QueueDataAdapter { 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}. * Adds a {@link MediaDescriptionCompat} at the given {@code position}.
* *
* @param position The position at which to add. * @param position The position at which to add.
......
...@@ -144,21 +144,30 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -144,21 +144,30 @@ import java.util.concurrent.CopyOnWriteArraySet;
} }
@Override @Override
@Nullable
public AudioComponent getAudioComponent() { public AudioComponent getAudioComponent() {
return null; return null;
} }
@Override @Override
@Nullable
public VideoComponent getVideoComponent() { public VideoComponent getVideoComponent() {
return null; return null;
} }
@Override @Override
@Nullable
public TextComponent getTextComponent() { public TextComponent getTextComponent() {
return null; return null;
} }
@Override @Override
@Nullable
public MetadataComponent getMetadataComponent() {
return null;
}
@Override
public Looper getPlaybackLooper() { public Looper getPlaybackLooper() {
return internalPlayer.getPlaybackLooper(); return internalPlayer.getPlaybackLooper();
} }
......
...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** 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. // 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}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // 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. * The version of the library expressed as an integer, for example 1002003.
...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // 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} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.C.VideoScalingMode; ...@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.C.VideoScalingMode;
import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioListener; import com.google.android.exoplayer2.audio.AudioListener;
import com.google.android.exoplayer2.audio.AuxEffectInfo; 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.source.TrackGroupArray;
import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
...@@ -299,6 +300,24 @@ public interface Player { ...@@ -299,6 +300,24 @@ public interface Player {
void removeTextOutput(TextOutput listener); 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 * Listener of changes in player state. All methods have no-op default implementations to allow
* selective overrides. * selective overrides.
...@@ -534,6 +553,12 @@ public interface Player { ...@@ -534,6 +553,12 @@ public interface Player {
TextComponent getTextComponent(); 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 * Returns the {@link Looper} associated with the application thread that's used to access the
* player and on which player events are received. * player and on which player events are received.
*/ */
......
...@@ -65,7 +65,11 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -65,7 +65,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
*/ */
@TargetApi(16) @TargetApi(16)
public class SimpleExoPlayer extends BasePlayer 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 Use {@link com.google.android.exoplayer2.video.VideoListener}. */
@Deprecated @Deprecated
...@@ -243,20 +247,29 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -243,20 +247,29 @@ public class SimpleExoPlayer extends BasePlayer
} }
@Override @Override
@Nullable
public AudioComponent getAudioComponent() { public AudioComponent getAudioComponent() {
return this; return this;
} }
@Override @Override
@Nullable
public VideoComponent getVideoComponent() { public VideoComponent getVideoComponent() {
return this; return this;
} }
@Override @Override
@Nullable
public TextComponent getTextComponent() { public TextComponent getTextComponent() {
return this; return this;
} }
@Override
@Nullable
public MetadataComponent getMetadataComponent() {
return this;
}
/** /**
* Sets the video scaling mode. * Sets the video scaling mode.
* *
...@@ -713,20 +726,12 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -713,20 +726,12 @@ public class SimpleExoPlayer extends BasePlayer
removeTextOutput(output); removeTextOutput(output);
} }
/** @Override
* Adds a {@link MetadataOutput} to receive metadata.
*
* @param listener The output to register.
*/
public void addMetadataOutput(MetadataOutput listener) { public void addMetadataOutput(MetadataOutput listener) {
metadataOutputs.add(listener); metadataOutputs.add(listener);
} }
/** @Override
* Removes a {@link MetadataOutput}.
*
* @param listener The output to remove.
*/
public void removeMetadataOutput(MetadataOutput listener) { public void removeMetadataOutput(MetadataOutput listener) {
metadataOutputs.remove(listener); metadataOutputs.remove(listener);
} }
......
...@@ -25,7 +25,8 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; ...@@ -25,7 +25,8 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.util.Assertions; 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 { public interface AudioRendererEventListener {
...@@ -35,14 +36,14 @@ 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 * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it
* remains enabled. * remains enabled.
*/ */
void onAudioEnabled(DecoderCounters counters); default void onAudioEnabled(DecoderCounters counters) {}
/** /**
* Called when the audio session is set. * Called when the audio session is set.
* *
* @param audioSessionId The audio session id. * @param audioSessionId The audio session id.
*/ */
void onAudioSessionId(int audioSessionId); default void onAudioSessionId(int audioSessionId) {}
/** /**
* Called when a decoder is created. * Called when a decoder is created.
...@@ -52,15 +53,15 @@ public interface AudioRendererEventListener { ...@@ -52,15 +53,15 @@ public interface AudioRendererEventListener {
* finished. * finished.
* @param initializationDurationMs The time taken to initialize the decoder in milliseconds. * @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
*/ */
void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, default void onAudioDecoderInitialized(
long initializationDurationMs); String decoderName, long initializedTimestampMs, long initializationDurationMs) {}
/** /**
* Called when the format of the media being consumed by the renderer changes. * Called when the format of the media being consumed by the renderer changes.
* *
* @param format The new format. * @param format The new format.
*/ */
void onAudioInputFormatChanged(Format format); default void onAudioInputFormatChanged(Format format) {}
/** /**
* Called when an {@link AudioSink} underrun occurs. * Called when an {@link AudioSink} underrun occurs.
...@@ -71,14 +72,15 @@ public interface AudioRendererEventListener { ...@@ -71,14 +72,15 @@ public interface AudioRendererEventListener {
* as the buffered media can have a variable bitrate so the duration may be unknown. * 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. * @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. * Called when the renderer is disabled.
* *
* @param counters {@link DecoderCounters} that were updated by the renderer. * @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}. * Dispatches events to a {@link AudioRendererEventListener}.
......
...@@ -34,16 +34,26 @@ public final class MpegAudioHeader { ...@@ -34,16 +34,26 @@ public final class MpegAudioHeader {
private static final String[] MIME_TYPE_BY_LAYER = private static final String[] MIME_TYPE_BY_LAYER =
new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG}; 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[] SAMPLING_RATE_V1 = {44100, 48000, 32000};
private static final int[] BITRATE_V1_L1 = private static final int[] BITRATE_V1_L1 = {
{32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448}; 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000,
private static final int[] BITRATE_V2_L1 = 416000, 448000
{32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256}; };
private static final int[] BITRATE_V1_L2 = private static final int[] BITRATE_V2_L1 = {
{32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384}; 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000,
private static final int[] BITRATE_V1_L3 = 224000, 256000
{32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320}; };
private static final int[] BITRATE_V2 = private static final int[] BITRATE_V1_L2 = {
{8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}; 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 * 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 { ...@@ -89,7 +99,7 @@ public final class MpegAudioHeader {
if (layer == 3) { if (layer == 3) {
// Layer I (layer == 3) // Layer I (layer == 3)
bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; 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 { } else {
// Layer II (layer == 2) or III (layer == 1) // Layer II (layer == 2) or III (layer == 1)
if (version == 3) { if (version == 3) {
...@@ -102,10 +112,10 @@ public final class MpegAudioHeader { ...@@ -102,10 +112,10 @@ public final class MpegAudioHeader {
if (version == 3) { if (version == 3) {
// Version 1 // Version 1
return 144000 * bitrate / samplingRate + padding; return 144 * bitrate / samplingRate + padding;
} else { } else {
// Version 2 or 2.5 // 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 { ...@@ -159,7 +169,7 @@ public final class MpegAudioHeader {
if (layer == 3) { if (layer == 3) {
// Layer I (layer == 3) // Layer I (layer == 3)
bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; 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; samplesPerFrame = 384;
} else { } else {
// Layer II (layer == 2) or III (layer == 1) // Layer II (layer == 2) or III (layer == 1)
...@@ -167,19 +177,22 @@ public final class MpegAudioHeader { ...@@ -167,19 +177,22 @@ public final class MpegAudioHeader {
// Version 1 // Version 1
bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
samplesPerFrame = 1152; samplesPerFrame = 1152;
frameSize = 144000 * bitrate / sampleRate + padding; frameSize = 144 * bitrate / sampleRate + padding;
} else { } else {
// Version 2 or 2.5. // Version 2 or 2.5.
bitrate = BITRATE_V2[bitrateIndex - 1]; bitrate = BITRATE_V2[bitrateIndex - 1];
samplesPerFrame = layer == 1 ? 576 : 1152; 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]; String mimeType = MIME_TYPE_BY_LAYER[3 - layer];
int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate * 1000, header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame);
samplesPerFrame);
return true; return true;
} }
...@@ -198,8 +211,14 @@ public final class MpegAudioHeader { ...@@ -198,8 +211,14 @@ public final class MpegAudioHeader {
/** Number of samples stored in the frame. */ /** Number of samples stored in the frame. */
public int samplesPerFrame; public int samplesPerFrame;
private void setValues(int version, String mimeType, int frameSize, int sampleRate, int channels, private void setValues(
int bitrate, int samplesPerFrame) { int version,
String mimeType,
int frameSize,
int sampleRate,
int channels,
int bitrate,
int samplesPerFrame) {
this.version = version; this.version = version;
this.mimeType = mimeType; this.mimeType = mimeType;
this.frameSize = frameSize; this.frameSize = frameSize;
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; 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.C;
import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.audio.Ac3Util;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
...@@ -140,7 +142,7 @@ public final class Ac3Extractor implements Extractor { ...@@ -140,7 +142,7 @@ public final class Ac3Extractor implements Extractor {
if (!startedPacket) { if (!startedPacket) {
// Pass data to the reader as though it's contained within a single infinitely long packet. // 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; startedPacket = true;
} }
// TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes
......
...@@ -100,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader { ...@@ -100,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
} }
@Override @Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; 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.IntDef;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
...@@ -202,7 +204,7 @@ public final class AdtsExtractor implements Extractor { ...@@ -202,7 +204,7 @@ public final class AdtsExtractor implements Extractor {
if (!startedPacket) { if (!startedPacket) {
// Pass data to the reader as though it's contained within a single infinitely long packet. // 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; startedPacket = true;
} }
// TODO: Make it possible for reader to consume the dataSource directly, so that it becomes // TODO: Make it possible for reader to consume the dataSource directly, so that it becomes
......
...@@ -141,7 +141,7 @@ public final class AdtsReader implements ElementaryStreamReader { ...@@ -141,7 +141,7 @@ public final class AdtsReader implements ElementaryStreamReader {
} }
@Override @Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
......
...@@ -80,7 +80,7 @@ public final class DtsReader implements ElementaryStreamReader { ...@@ -80,7 +80,7 @@ public final class DtsReader implements ElementaryStreamReader {
} }
@Override @Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; 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.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
...@@ -73,8 +75,8 @@ public final class DvbSubtitleReader implements ElementaryStreamReader { ...@@ -73,8 +75,8 @@ public final class DvbSubtitleReader implements ElementaryStreamReader {
} }
@Override @Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
if (!dataAlignmentIndicator) { if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
return; return;
} }
writingSample = true; writingSample = true;
......
...@@ -43,9 +43,9 @@ public interface ElementaryStreamReader { ...@@ -43,9 +43,9 @@ public interface ElementaryStreamReader {
* Called when a packet starts. * Called when a packet starts.
* *
* @param pesTimeUs The timestamp associated with the packet. * @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. * Consumes (possibly partial) data from the current packet.
......
...@@ -107,7 +107,8 @@ public final class H262Reader implements ElementaryStreamReader { ...@@ -107,7 +107,8 @@ public final class H262Reader implements ElementaryStreamReader {
} }
@Override @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; this.pesTimeUs = pesTimeUs;
} }
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; 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 android.util.SparseArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
...@@ -56,9 +58,12 @@ public final class H264Reader implements ElementaryStreamReader { ...@@ -56,9 +58,12 @@ public final class H264Reader implements ElementaryStreamReader {
// State that should not be reset on seek. // State that should not be reset on seek.
private boolean hasOutputFormat; 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; private long pesTimeUs;
// State inherited from the TS packet header.
private boolean randomAccessIndicator;
// Scratch variables to avoid allocations. // Scratch variables to avoid allocations.
private final ParsableByteArray seiWrapper; private final ParsableByteArray seiWrapper;
...@@ -88,6 +93,7 @@ public final class H264Reader implements ElementaryStreamReader { ...@@ -88,6 +93,7 @@ public final class H264Reader implements ElementaryStreamReader {
sei.reset(); sei.reset();
sampleReader.reset(); sampleReader.reset();
totalBytesWritten = 0; totalBytesWritten = 0;
randomAccessIndicator = false;
} }
@Override @Override
...@@ -100,8 +106,9 @@ public final class H264Reader implements ElementaryStreamReader { ...@@ -100,8 +106,9 @@ public final class H264Reader implements ElementaryStreamReader {
} }
@Override @Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
this.pesTimeUs = pesTimeUs; this.pesTimeUs = pesTimeUs;
randomAccessIndicator |= (flags & FLAG_RANDOM_ACCESS_INDICATOR) != 0;
} }
@Override @Override
...@@ -220,12 +227,17 @@ public final class H264Reader implements ElementaryStreamReader { ...@@ -220,12 +227,17 @@ public final class H264Reader implements ElementaryStreamReader {
seiWrapper.setPosition(4); // NAL prefix and nal_unit() header. seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.
seiReader.consume(pesTimeUs, seiWrapper); 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 class SampleReader {
private static final int DEFAULT_BUFFER_SIZE = 128; private static final int DEFAULT_BUFFER_SIZE = 128;
...@@ -430,11 +442,12 @@ public final class H264Reader implements ElementaryStreamReader { ...@@ -430,11 +442,12 @@ public final class H264Reader implements ElementaryStreamReader {
isFilling = false; 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 if (nalUnitType == NAL_UNIT_TYPE_AUD
|| (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) { || (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) {
// If the NAL unit ending is the start of a new sample, output the previous one. // 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); int nalUnitLength = (int) (position - nalUnitStartPosition);
outputSample(offset + nalUnitLength); outputSample(offset + nalUnitLength);
} }
...@@ -443,8 +456,12 @@ public final class H264Reader implements ElementaryStreamReader { ...@@ -443,8 +456,12 @@ public final class H264Reader implements ElementaryStreamReader {
sampleIsKeyframe = false; sampleIsKeyframe = false;
readingSample = true; readingSample = true;
} }
sampleIsKeyframe |= nalUnitType == NAL_UNIT_TYPE_IDR || (allowNonIdrKeyframes boolean treatIFrameAsKeyframe =
&& nalUnitType == NAL_UNIT_TYPE_NON_IDR && sliceHeader.isISlice()); allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator;
sampleIsKeyframe |=
nalUnitType == NAL_UNIT_TYPE_IDR
|| (treatIFrameAsKeyframe && nalUnitType == NAL_UNIT_TYPE_NON_IDR);
return sampleIsKeyframe;
} }
private void outputSample(int offset) { private void outputSample(int offset) {
...@@ -486,10 +503,21 @@ public final class H264Reader implements ElementaryStreamReader { ...@@ -486,10 +503,21 @@ public final class H264Reader implements ElementaryStreamReader {
hasSliceType = true; hasSliceType = true;
} }
public void setAll(SpsData spsData, int nalRefIdc, int sliceType, int frameNum, public void setAll(
int picParameterSetId, boolean fieldPicFlag, boolean bottomFieldFlagPresent, SpsData spsData,
boolean bottomFieldFlag, boolean idrPicFlag, int idrPicId, int picOrderCntLsb, int nalRefIdc,
int deltaPicOrderCntBottom, int deltaPicOrderCnt0, int deltaPicOrderCnt1) { 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.spsData = spsData;
this.nalRefIdc = nalRefIdc; this.nalRefIdc = nalRefIdc;
this.sliceType = sliceType; this.sliceType = sliceType;
...@@ -514,23 +542,26 @@ public final class H264Reader implements ElementaryStreamReader { ...@@ -514,23 +542,26 @@ public final class H264Reader implements ElementaryStreamReader {
private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) { private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) {
// See ISO 14496-10 subsection 7.4.1.2.4. // See ISO 14496-10 subsection 7.4.1.2.4.
return isComplete && (!other.isComplete || frameNum != other.frameNum return isComplete
|| picParameterSetId != other.picParameterSetId || fieldPicFlag != other.fieldPicFlag && (!other.isComplete
|| (bottomFieldFlagPresent && other.bottomFieldFlagPresent || frameNum != other.frameNum
&& bottomFieldFlag != other.bottomFieldFlag) || picParameterSetId != other.picParameterSetId
|| (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) || fieldPicFlag != other.fieldPicFlag
|| (spsData.picOrderCountType == 0 && other.spsData.picOrderCountType == 0 || (bottomFieldFlagPresent
&& (picOrderCntLsb != other.picOrderCntLsb && other.bottomFieldFlagPresent
|| deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) && bottomFieldFlag != other.bottomFieldFlag)
|| (spsData.picOrderCountType == 1 && other.spsData.picOrderCountType == 1 || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
&& (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 || (spsData.picOrderCountType == 0
|| deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) && other.spsData.picOrderCountType == 0
|| idrPicFlag != other.idrPicFlag && (picOrderCntLsb != other.picOrderCntLsb
|| (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId)); || 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));
} }
} }
} }
} }
...@@ -104,7 +104,8 @@ public final class H265Reader implements ElementaryStreamReader { ...@@ -104,7 +104,8 @@ public final class H265Reader implements ElementaryStreamReader {
} }
@Override @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; this.pesTimeUs = pesTimeUs;
} }
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; 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.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
...@@ -63,8 +65,8 @@ public final class Id3Reader implements ElementaryStreamReader { ...@@ -63,8 +65,8 @@ public final class Id3Reader implements ElementaryStreamReader {
} }
@Override @Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
if (!dataAlignmentIndicator) { if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
return; return;
} }
writingSample = true; writingSample = true;
......
...@@ -93,7 +93,7 @@ public final class LatmReader implements ElementaryStreamReader { ...@@ -93,7 +93,7 @@ public final class LatmReader implements ElementaryStreamReader {
} }
@Override @Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
......
...@@ -83,7 +83,7 @@ public final class MpegAudioReader implements ElementaryStreamReader { ...@@ -83,7 +83,7 @@ public final class MpegAudioReader implements ElementaryStreamReader {
} }
@Override @Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs; timeUs = pesTimeUs;
} }
......
...@@ -78,9 +78,8 @@ public final class PesReader implements TsPayloadReader { ...@@ -78,9 +78,8 @@ public final class PesReader implements TsPayloadReader {
} }
@Override @Override
public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException {
throws ParserException { if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) {
if (payloadUnitStartIndicator) {
switch (state) { switch (state) {
case STATE_FINDING_HEADER: case STATE_FINDING_HEADER:
case STATE_READING_HEADER: case STATE_READING_HEADER:
...@@ -122,7 +121,8 @@ public final class PesReader implements TsPayloadReader { ...@@ -122,7 +121,8 @@ public final class PesReader implements TsPayloadReader {
if (continueRead(data, pesScratch.data, readLength) if (continueRead(data, pesScratch.data, readLength)
&& continueRead(data, null, extendedHeaderLength)) { && continueRead(data, null, extendedHeaderLength)) {
parseHeaderExtension(); parseHeaderExtension();
reader.packetStarted(timeUs, dataAlignmentIndicator); flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;
reader.packetStarted(timeUs, flags);
setState(STATE_READING_BODY); setState(STATE_READING_BODY);
} }
break; break;
......
...@@ -343,7 +343,7 @@ public final class PsExtractor implements Extractor { ...@@ -343,7 +343,7 @@ public final class PsExtractor implements Extractor {
data.readBytes(pesScratch.data, 0, extendedHeaderLength); data.readBytes(pesScratch.data, 0, extendedHeaderLength);
pesScratch.setPosition(0); pesScratch.setPosition(0);
parseHeaderExtension(); parseHeaderExtension();
pesPayloadReader.packetStarted(timeUs, true); pesPayloadReader.packetStarted(timeUs, TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR);
pesPayloadReader.consume(data); pesPayloadReader.consume(data);
// We always have complete PES packets with program stream. // We always have complete PES packets with program stream.
pesPayloadReader.packetFinished(); pesPayloadReader.packetFinished();
......
...@@ -57,7 +57,8 @@ public final class SectionReader implements TsPayloadReader { ...@@ -57,7 +57,8 @@ public final class SectionReader implements TsPayloadReader {
} }
@Override @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; int payloadStartPosition = C.POSITION_UNSET;
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
int payloadStartOffset = data.readUnsignedByte(); int payloadStartOffset = data.readUnsignedByte();
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; 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.support.annotation.IntDef;
import android.util.SparseArray; import android.util.SparseArray;
import android.util.SparseBooleanArray; import android.util.SparseBooleanArray;
...@@ -279,6 +281,8 @@ public final class TsExtractor implements Extractor { ...@@ -279,6 +281,8 @@ public final class TsExtractor implements Extractor {
return RESULT_CONTINUE; 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. // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
int tsPacketHeader = tsPacketBuffer.readInt(); int tsPacketHeader = tsPacketBuffer.readInt();
if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator
...@@ -286,7 +290,7 @@ public final class TsExtractor implements Extractor { ...@@ -286,7 +290,7 @@ public final class TsExtractor implements Extractor {
tsPacketBuffer.setPosition(endOfPacket); tsPacketBuffer.setPosition(endOfPacket);
return RESULT_CONTINUE; return RESULT_CONTINUE;
} }
boolean payloadUnitStartIndicator = (tsPacketHeader & 0x400000) != 0; packetHeaderFlags |= (tsPacketHeader & 0x400000) != 0 ? FLAG_PAYLOAD_UNIT_START_INDICATOR : 0;
// Ignoring transport_priority (tsPacketHeader & 0x200000) // Ignoring transport_priority (tsPacketHeader & 0x200000)
int pid = (tsPacketHeader & 0x1FFF00) >> 8; int pid = (tsPacketHeader & 0x1FFF00) >> 8;
// Ignoring transport_scrambling_control (tsPacketHeader & 0xC0) // Ignoring transport_scrambling_control (tsPacketHeader & 0xC0)
...@@ -317,14 +321,20 @@ public final class TsExtractor implements Extractor { ...@@ -317,14 +321,20 @@ public final class TsExtractor implements Extractor {
// Skip the adaptation field. // Skip the adaptation field.
if (adaptationFieldExists) { if (adaptationFieldExists) {
int adaptationFieldLength = tsPacketBuffer.readUnsignedByte(); 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. // Read the payload.
boolean wereTracksEnded = tracksEnded; boolean wereTracksEnded = tracksEnded;
if (shouldConsumePacketPayload(pid)) { if (shouldConsumePacketPayload(pid)) {
tsPacketBuffer.setLimit(endOfPacket); tsPacketBuffer.setLimit(endOfPacket);
payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); payloadReader.consume(tsPacketBuffer, packetHeaderFlags);
tsPacketBuffer.setLimit(limit); tsPacketBuffer.setLimit(limit);
} }
if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) { if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) {
......
...@@ -15,12 +15,16 @@ ...@@ -15,12 +15,16 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import android.support.annotation.IntDef;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster; 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.Collections;
import java.util.List; import java.util.List;
...@@ -175,6 +179,29 @@ public interface TsPayloadReader { ...@@ -175,6 +179,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. * Initializes the payload reader.
* *
* @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
...@@ -187,10 +214,10 @@ public interface TsPayloadReader { ...@@ -187,10 +214,10 @@ public interface TsPayloadReader {
/** /**
* Notifies the reader that a seek has occurred. * Notifies the reader that a seek has occurred.
* <p> *
* Following a call to this method, the data passed to the next invocation of * <p>Following a call to this method, the data passed to the next invocation of {@link #consume}
* {@link #consume(ParsableByteArray, boolean)} will not be a continuation of the data that was * will not be a continuation of the data that was previously passed. Hence the reader should
* previously passed. Hence the reader should reset any internal state. * reset any internal state.
*/ */
void seek(); void seek();
...@@ -198,9 +225,8 @@ public interface TsPayloadReader { ...@@ -198,9 +225,8 @@ public interface TsPayloadReader {
* Consumes the payload of a TS packet. * Consumes the payload of a TS packet.
* *
* @param data The TS packet. The position will be set to the start of the payload. * @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. * @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;
} }
...@@ -248,9 +248,15 @@ public final class MediaCodecInfo { ...@@ -248,9 +248,15 @@ public final class MediaCodecInfo {
// If we don't know any better, we assume that the profile and level are supported. // If we don't know any better, we assume that the profile and level are supported.
return true; 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()) { for (CodecProfileLevel capabilities : getProfileLevels()) {
if (capabilities.profile == codecProfileAndLevel.first if (capabilities.profile == profile && capabilities.level >= level) {
&& capabilities.level >= codecProfileAndLevel.second) {
return true; return true;
} }
} }
......
...@@ -1622,7 +1622,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1622,7 +1622,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/ */
private static boolean codecNeedsEosFlushWorkaround(String name) { private static boolean codecNeedsEosFlushWorkaround(String name) {
return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(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".equals(name)
|| "OMX.amlogic.avc.decoder.awesome.secure".equals(name))); || "OMX.amlogic.avc.decoder.awesome.secure".equals(name)));
} }
......
...@@ -318,7 +318,21 @@ public final class MediaCodecUtil { ...@@ -318,7 +318,21 @@ public final class MediaCodecUtil {
} }
// Work around https://github.com/google/ExoPlayer/issues/4519. // 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; return false;
} }
......
...@@ -217,6 +217,12 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> { ...@@ -217,6 +217,12 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> {
} }
@Override @Override
@Nullable
public Object getTag() {
return mediaSource.getTag();
}
@Override
public void prepareSourceInternal( public void prepareSourceInternal(
ExoPlayer player, ExoPlayer player,
boolean isTopLevelSource, boolean isTopLevelSource,
......
...@@ -454,6 +454,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -454,6 +454,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
} }
@Override @Override
@Nullable
public Object getTag() {
return null;
}
@Override
public final synchronized void prepareSourceInternal( public final synchronized void prepareSourceInternal(
ExoPlayer player, ExoPlayer player,
boolean isTopLevelSource, boolean isTopLevelSource,
...@@ -820,7 +826,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -820,7 +826,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public MediaSourceHolder(MediaSource mediaSource) { public MediaSourceHolder(MediaSource mediaSource) {
this.mediaSource = mediaSource; this.mediaSource = mediaSource;
this.timeline = new DeferredTimeline(); this.timeline = DeferredTimeline.createWithDummyTimeline(mediaSource.getTag());
this.activeMediaPeriods = new ArrayList<>(); this.activeMediaPeriods = new ArrayList<>();
this.uid = new Object(); this.uid = new Object();
} }
...@@ -945,11 +951,19 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -945,11 +951,19 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
private static final class DeferredTimeline extends ForwardingTimeline { private static final class DeferredTimeline extends ForwardingTimeline {
private static final Object DUMMY_ID = new Object(); private static final Object DUMMY_ID = new Object();
private static final DummyTimeline DUMMY_TIMELINE = new DummyTimeline();
private final Object replacedId; private final Object replacedId;
/** /**
* Returns an instance with a dummy timeline using the provided window tag.
*
* @param windowTag A window tag.
*/
public static DeferredTimeline createWithDummyTimeline(@Nullable Object windowTag) {
return new DeferredTimeline(new DummyTimeline(windowTag), DUMMY_ID);
}
/**
* Returns an instance with a real timeline, replacing the provided period ID with the already * Returns an instance with a real timeline, replacing the provided period ID with the already
* assigned dummy period ID. * assigned dummy period ID.
* *
...@@ -962,11 +976,6 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -962,11 +976,6 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
return new DeferredTimeline(timeline, firstPeriodUid); return new DeferredTimeline(timeline, firstPeriodUid);
} }
/** Creates deferred timeline exposing a {@link DummyTimeline}. */
public DeferredTimeline() {
this(DUMMY_TIMELINE, DUMMY_ID);
}
private DeferredTimeline(Timeline timeline, Object replacedId) { private DeferredTimeline(Timeline timeline, Object replacedId) {
super(timeline); super(timeline);
this.replacedId = replacedId; this.replacedId = replacedId;
...@@ -1010,6 +1019,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -1010,6 +1019,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
/** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */
private static final class DummyTimeline extends Timeline { private static final class DummyTimeline extends Timeline {
@Nullable private final Object tag;
public DummyTimeline(@Nullable Object tag) {
this.tag = tag;
}
@Override @Override
public int getWindowCount() { public int getWindowCount() {
return 1; return 1;
...@@ -1019,7 +1034,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -1019,7 +1034,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public Window getWindow( public Window getWindow(
int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
return window.set( return window.set(
/* tag= */ null, tag,
/* presentationStartTimeMs= */ C.TIME_UNSET, /* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET,
/* isSeekable= */ false, /* isSeekable= */ false,
...@@ -1070,6 +1085,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -1070,6 +1085,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
} }
@Override @Override
@Nullable
public Object getTag() {
return null;
}
@Override
protected void releaseSourceInternal() { protected void releaseSourceInternal() {
// Do nothing. // Do nothing.
} }
......
...@@ -359,6 +359,12 @@ public final class ExtractorMediaSource extends BaseMediaSource ...@@ -359,6 +359,12 @@ public final class ExtractorMediaSource extends BaseMediaSource
} }
@Override @Override
@Nullable
public Object getTag() {
return tag;
}
@Override
public void prepareSourceInternal( public void prepareSourceInternal(
ExoPlayer player, ExoPlayer player,
boolean isTopLevelSource, boolean isTopLevelSource,
......
...@@ -65,6 +65,12 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> { ...@@ -65,6 +65,12 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> {
} }
@Override @Override
@Nullable
public Object getTag() {
return childSource.getTag();
}
@Override
public void prepareSourceInternal( public void prepareSourceInternal(
ExoPlayer player, ExoPlayer player,
boolean isTopLevelSource, boolean isTopLevelSource,
......
...@@ -220,6 +220,12 @@ public interface MediaSource { ...@@ -220,6 +220,12 @@ public interface MediaSource {
*/ */
void removeEventListener(MediaSourceEventListener eventListener); 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 Will be removed in the next release. */
@Deprecated @Deprecated
void prepareSource( void prepareSource(
......
...@@ -99,6 +99,12 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> { ...@@ -99,6 +99,12 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
} }
@Override @Override
@Nullable
public Object getTag() {
return mediaSources.length > 0 ? mediaSources[0].getTag() : null;
}
@Override
public void prepareSourceInternal( public void prepareSourceInternal(
ExoPlayer player, ExoPlayer player,
boolean isTopLevelSource, boolean isTopLevelSource,
......
...@@ -185,6 +185,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { ...@@ -185,6 +185,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final boolean treatLoadErrorsAsEndOfStream; private final boolean treatLoadErrorsAsEndOfStream;
private final Timeline timeline; private final Timeline timeline;
@Nullable private final Object tag;
private @Nullable TransferListener transferListener; private @Nullable TransferListener transferListener;
...@@ -287,6 +288,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { ...@@ -287,6 +288,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
this.durationUs = durationUs; this.durationUs = durationUs;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
this.tag = tag;
dataSpec = dataSpec =
new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH);
timeline = timeline =
...@@ -296,6 +298,12 @@ public final class SingleSampleMediaSource extends BaseMediaSource { ...@@ -296,6 +298,12 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
// MediaSource implementation. // MediaSource implementation.
@Override @Override
@Nullable
public Object getTag() {
return tag;
}
@Override
public void prepareSourceInternal( public void prepareSourceInternal(
ExoPlayer player, ExoPlayer player,
boolean isTopLevelSource, boolean isTopLevelSource,
......
...@@ -320,6 +320,12 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> { ...@@ -320,6 +320,12 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
} }
@Override @Override
@Nullable
public Object getTag() {
return contentMediaSource.getTag();
}
@Override
public void prepareSourceInternal( public void prepareSourceInternal(
final ExoPlayer player, final ExoPlayer player,
boolean isTopLevelSource, boolean isTopLevelSource,
......
...@@ -15,7 +15,12 @@ ...@@ -15,7 +15,12 @@
*/ */
package com.google.android.exoplayer2.text.ttml; 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.text.SpannableStringBuilder;
import android.util.Base64;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -44,9 +49,9 @@ import java.util.TreeSet; ...@@ -44,9 +49,9 @@ import java.util.TreeSet;
public static final String TAG_LAYOUT = "layout"; public static final String TAG_LAYOUT = "layout";
public static final String TAG_REGION = "region"; public static final String TAG_REGION = "region";
public static final String TAG_METADATA = "metadata"; public static final String TAG_METADATA = "metadata";
public static final String TAG_SMPTE_IMAGE = "smpte:image"; public static final String TAG_IMAGE = "image";
public static final String TAG_SMPTE_DATA = "smpte:data"; public static final String TAG_DATA = "data";
public static final String TAG_SMPTE_INFORMATION = "smpte:information"; public static final String TAG_INFORMATION = "information";
public static final String ANONYMOUS_REGION_ID = ""; public static final String ANONYMOUS_REGION_ID = "";
public static final String ATTR_ID = "id"; public static final String ATTR_ID = "id";
...@@ -75,34 +80,57 @@ import java.util.TreeSet; ...@@ -75,34 +80,57 @@ import java.util.TreeSet;
public static final String START = "start"; public static final String START = "start";
public static final String END = "end"; public static final String END = "end";
public final String tag; @Nullable public final String tag;
public final String text; @Nullable public final String text;
public final boolean isTextNode; public final boolean isTextNode;
public final long startTimeUs; public final long startTimeUs;
public final long endTimeUs; public final long endTimeUs;
public final TtmlStyle style; @Nullable public final TtmlStyle style;
@Nullable private final String[] styleIds;
public final String regionId; public final String regionId;
@Nullable public final String imageId;
private final String[] styleIds;
private final HashMap<String, Integer> nodeStartsByRegion; private final HashMap<String, Integer> nodeStartsByRegion;
private final HashMap<String, Integer> nodeEndsByRegion; private final HashMap<String, Integer> nodeEndsByRegion;
private List<TtmlNode> children; private List<TtmlNode> children;
public static TtmlNode buildTextNode(String text) { public static TtmlNode buildTextNode(String text) {
return new TtmlNode(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), C.TIME_UNSET, return new TtmlNode(
C.TIME_UNSET, null, null, ANONYMOUS_REGION_ID); /* 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, public static TtmlNode buildNode(
TtmlStyle style, String[] styleIds, String regionId) { @Nullable String tag,
return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds, regionId); 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, private TtmlNode(
TtmlStyle style, String[] styleIds, String regionId) { @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.tag = tag;
this.text = text; this.text = text;
this.imageId = imageId;
this.style = style; this.style = style;
this.styleIds = styleIds; this.styleIds = styleIds;
this.isTextNode = text != null; this.isTextNode = text != null;
...@@ -151,7 +179,8 @@ import java.util.TreeSet; ...@@ -151,7 +179,8 @@ import java.util.TreeSet;
private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) { private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) {
boolean isPNode = TAG_P.equals(tag); 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) { if (startTimeUs != C.TIME_UNSET) {
out.add(startTimeUs); out.add(startTimeUs);
} }
...@@ -171,13 +200,46 @@ import java.util.TreeSet; ...@@ -171,13 +200,46 @@ import java.util.TreeSet;
return styleIds; return styleIds;
} }
public List<Cue> getCues(long timeUs, Map<String, TtmlStyle> globalStyles, public List<Cue> getCues(
Map<String, TtmlRegion> regionMap) { long timeUs,
TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>(); Map<String, TtmlStyle> globalStyles,
traverseForText(timeUs, false, regionId, regionOutputs); Map<String, TtmlRegion> regionMap,
traverseForStyle(timeUs, globalStyles, regionOutputs); Map<String, String> imageMap) {
List<Pair<String, String>> regionImageOutputs = new ArrayList<>();
traverseForImage(timeUs, regionId, regionImageOutputs);
TreeMap<String, SpannableStringBuilder> regionTextOutputs = new TreeMap<>();
traverseForText(timeUs, false, regionId, regionTextOutputs);
traverseForStyle(timeUs, globalStyles, regionTextOutputs);
List<Cue> cues = new ArrayList<>(); List<Cue> cues = new ArrayList<>();
for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
// Create image based cues.
for (Pair<String, String> 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<String, SpannableStringBuilder> entry : regionTextOutputs.entrySet()) {
TtmlRegion region = regionMap.get(entry.getKey()); TtmlRegion region = regionMap.get(entry.getKey());
cues.add( cues.add(
new Cue( new Cue(
...@@ -192,9 +254,22 @@ import java.util.TreeSet; ...@@ -192,9 +254,22 @@ import java.util.TreeSet;
region.textSizeType, region.textSizeType,
region.textSize)); region.textSize));
} }
return cues; return cues;
} }
private void traverseForImage(
long timeUs, String inheritedRegion, List<Pair<String, String>> 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( private void traverseForText(
long timeUs, long timeUs,
boolean descendsPNode, boolean descendsPNode,
......
...@@ -32,11 +32,16 @@ import java.util.Map; ...@@ -32,11 +32,16 @@ import java.util.Map;
private final long[] eventTimesUs; private final long[] eventTimesUs;
private final Map<String, TtmlStyle> globalStyles; private final Map<String, TtmlStyle> globalStyles;
private final Map<String, TtmlRegion> regionMap; private final Map<String, TtmlRegion> regionMap;
private final Map<String, String> imageMap;
public TtmlSubtitle(TtmlNode root, Map<String, TtmlStyle> globalStyles, public TtmlSubtitle(
Map<String, TtmlRegion> regionMap) { TtmlNode root,
Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMap,
Map<String, String> imageMap) {
this.root = root; this.root = root;
this.regionMap = regionMap; this.regionMap = regionMap;
this.imageMap = imageMap;
this.globalStyles = this.globalStyles =
globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap(); globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap();
this.eventTimesUs = root.getEventTimesUs(); this.eventTimesUs = root.getEventTimesUs();
...@@ -65,7 +70,7 @@ import java.util.Map; ...@@ -65,7 +70,7 @@ import java.util.Map;
@Override @Override
public List<Cue> getCues(long timeUs) { public List<Cue> getCues(long timeUs) {
return root.getCues(timeUs, globalStyles, regionMap); return root.getCues(timeUs, globalStyles, regionMap, imageMap);
} }
/* @VisibleForTesting */ /* @VisibleForTesting */
......
...@@ -82,7 +82,7 @@ public interface Cache { ...@@ -82,7 +82,7 @@ public interface Cache {
* Releases the cache. This method must be called when the cache is no longer required. The 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. * 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. * Registers a listener to listen for changes to a given key.
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.util.SparseArray; import android.util.SparseArray;
import android.util.SparseBooleanArray;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.AtomicFile; import com.google.android.exoplayer2.util.AtomicFile;
...@@ -41,6 +42,7 @@ import javax.crypto.CipherOutputStream; ...@@ -41,6 +42,7 @@ import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Maintains the index of cached content. */ /** Maintains the index of cached content. */
/*package*/ class CachedContentIndex { /*package*/ class CachedContentIndex {
...@@ -52,7 +54,30 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -52,7 +54,30 @@ import javax.crypto.spec.SecretKeySpec;
private static final int FLAG_ENCRYPTED_INDEX = 1; private static final int FLAG_ENCRYPTED_INDEX = 1;
private final HashMap<String, CachedContent> keyToContent; private final HashMap<String, CachedContent> keyToContent;
private final SparseArray<String> 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:
*
* <p>[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
*
* <p>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.
*
* <p>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 AtomicFile atomicFile;
private final Cipher cipher; private final Cipher cipher;
private final SecretKeySpec secretKeySpec; private final SecretKeySpec secretKeySpec;
...@@ -104,6 +129,7 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -104,6 +129,7 @@ import javax.crypto.spec.SecretKeySpec;
} }
keyToContent = new HashMap<>(); keyToContent = new HashMap<>();
idToKey = new SparseArray<>(); idToKey = new SparseArray<>();
removedIds = new SparseBooleanArray();
atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME)); atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME));
} }
...@@ -124,6 +150,12 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -124,6 +150,12 @@ import javax.crypto.spec.SecretKeySpec;
} }
writeFile(); writeFile();
changed = false; 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; ...@@ -168,8 +200,11 @@ import javax.crypto.spec.SecretKeySpec;
CachedContent cachedContent = keyToContent.get(key); CachedContent cachedContent = keyToContent.get(key);
if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) { if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) {
keyToContent.remove(key); keyToContent.remove(key);
idToKey.remove(cachedContent.id);
changed = true; 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);
} }
} }
......
...@@ -146,13 +146,16 @@ public final class SimpleCache implements Cache { ...@@ -146,13 +146,16 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized void release() throws CacheException { public synchronized void release() {
if (released) { if (released) {
return; return;
} }
listeners.clear(); listeners.clear();
removeStaleSpans();
try { try {
removeStaleSpansAndCachedContents(); index.store();
} catch (CacheException e) {
Log.e(TAG, "Storing index file failed", e);
} finally { } finally {
unlockFolder(cacheDir); unlockFolder(cacheDir);
released = true; released = true;
...@@ -265,7 +268,7 @@ public final class SimpleCache implements Cache { ...@@ -265,7 +268,7 @@ public final class SimpleCache implements Cache {
if (!cacheDir.exists()) { if (!cacheDir.exists()) {
// For some reason the cache directory doesn't exist. Make a best effort to create it. // For some reason the cache directory doesn't exist. Make a best effort to create it.
cacheDir.mkdirs(); cacheDir.mkdirs();
removeStaleSpansAndCachedContents(); removeStaleSpans();
} }
evictor.onStartFile(this, key, position, maxLength); evictor.onStartFile(this, key, position, maxLength);
return SimpleCacheSpan.getCacheFile( return SimpleCacheSpan.getCacheFile(
...@@ -311,9 +314,9 @@ public final class SimpleCache implements Cache { ...@@ -311,9 +314,9 @@ public final class SimpleCache implements Cache {
} }
@Override @Override
public synchronized void removeSpan(CacheSpan span) throws CacheException { public synchronized void removeSpan(CacheSpan span) {
Assertions.checkState(!released); Assertions.checkState(!released);
removeSpan(span, true); removeSpanInternal(span);
} }
@Override @Override
...@@ -379,7 +382,7 @@ public final class SimpleCache implements Cache { ...@@ -379,7 +382,7 @@ public final class SimpleCache implements Cache {
if (span.isCached && !span.file.exists()) { if (span.isCached && !span.file.exists()) {
// The file has been deleted from under us. It's likely that other files will have been // 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. // deleted too, so scan the whole in-memory representation.
removeStaleSpansAndCachedContents(); removeStaleSpans();
continue; continue;
} }
return span; return span;
...@@ -431,27 +434,21 @@ public final class SimpleCache implements Cache { ...@@ -431,27 +434,21 @@ public final class SimpleCache implements Cache {
notifySpanAdded(span); notifySpanAdded(span);
} }
private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException { private void removeSpanInternal(CacheSpan span) {
CachedContent cachedContent = index.get(span.key); CachedContent cachedContent = index.get(span.key);
if (cachedContent == null || !cachedContent.removeSpan(span)) { if (cachedContent == null || !cachedContent.removeSpan(span)) {
return; return;
} }
totalSpace -= span.length; totalSpace -= span.length;
try { index.maybeRemove(cachedContent.key);
if (removeEmptyCachedContent) { notifySpanRemoved(span);
index.maybeRemove(cachedContent.key);
index.store();
}
} finally {
notifySpanRemoved(span);
}
} }
/** /**
* Scans all of the cached spans in the in-memory representation, removing any for which files no * Scans all of the cached spans in the in-memory representation, removing any for which files no
* longer exist. * longer exist.
*/ */
private void removeStaleSpansAndCachedContents() throws CacheException { private void removeStaleSpans() {
ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>(); ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>();
for (CachedContent cachedContent : index.getAll()) { for (CachedContent cachedContent : index.getAll()) {
for (CacheSpan span : cachedContent.getSpans()) { for (CacheSpan span : cachedContent.getSpans()) {
...@@ -461,11 +458,8 @@ public final class SimpleCache implements Cache { ...@@ -461,11 +458,8 @@ public final class SimpleCache implements Cache {
} }
} }
for (int i = 0; i < spansToBeRemoved.size(); i++) { for (int i = 0; i < spansToBeRemoved.size(); i++) {
// Remove span but not CachedContent to prevent multiple index.store() calls. removeSpanInternal(spansToBeRemoved.get(i));
removeSpan(spansToBeRemoved.get(i), false);
} }
index.removeEmpty();
index.store();
} }
private void notifySpanRemoved(CacheSpan span) { private void notifySpanRemoved(CacheSpan span) {
......
...@@ -1436,11 +1436,12 @@ public final class Util { ...@@ -1436,11 +1436,12 @@ public final class Util {
} }
/** /**
* Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C} * Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C} {@code
* {@code DEFAULT_*_BUFFER_SIZE} constant. * DEFAULT_*_BUFFER_SIZE} constant.
* *
* @param trackType The track type. * @param trackType The track type.
* @return The corresponding default buffer size in bytes. * @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) { public static int getDefaultBufferSize(int trackType) {
switch (trackType) { switch (trackType) {
...@@ -1456,8 +1457,10 @@ public final class Util { ...@@ -1456,8 +1457,10 @@ public final class Util {
return C.DEFAULT_METADATA_BUFFER_SIZE; return C.DEFAULT_METADATA_BUFFER_SIZE;
case C.TRACK_TYPE_CAMERA_MOTION: case C.TRACK_TYPE_CAMERA_MOTION:
return C.DEFAULT_CAMERA_MOTION_BUFFER_SIZE; return C.DEFAULT_CAMERA_MOTION_BUFFER_SIZE;
case C.TRACK_TYPE_NONE:
return 0;
default: default:
throw new IllegalStateException(); throw new IllegalArgumentException();
} }
} }
......
...@@ -26,7 +26,8 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; ...@@ -26,7 +26,8 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.util.Assertions; 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 { public interface VideoRendererEventListener {
...@@ -36,7 +37,7 @@ 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 * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it
* remains enabled. * remains enabled.
*/ */
void onVideoEnabled(DecoderCounters counters); default void onVideoEnabled(DecoderCounters counters) {}
/** /**
* Called when a decoder is created. * Called when a decoder is created.
...@@ -46,15 +47,15 @@ public interface VideoRendererEventListener { ...@@ -46,15 +47,15 @@ public interface VideoRendererEventListener {
* finished. * finished.
* @param initializationDurationMs The time taken to initialize the decoder in milliseconds. * @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
*/ */
void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, default void onVideoDecoderInitialized(
long initializationDurationMs); String decoderName, long initializedTimestampMs, long initializationDurationMs) {}
/** /**
* Called when the format of the media being consumed by the renderer changes. * Called when the format of the media being consumed by the renderer changes.
* *
* @param format The new format. * @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 * Called to report the number of frames dropped by the renderer. Dropped frames are reported
...@@ -62,12 +63,11 @@ public interface VideoRendererEventListener { ...@@ -62,12 +63,11 @@ public interface VideoRendererEventListener {
* reaches a specified threshold whilst the renderer is started. * reaches a specified threshold whilst the renderer is started.
* *
* @param count The number of dropped frames. * @param count The number of dropped frames.
* @param elapsedMs The duration in milliseconds over which the frames were dropped. This * @param elapsedMs The duration in milliseconds over which the frames were dropped. This duration
* duration is timed from when the renderer was started or from when dropped frames were * is timed from when the renderer was started or from when dropped frames were last reported
* last reported (whichever was more recent), and not from when the first of the reported * (whichever was more recent), and not from when the first of the reported drops occurred.
* 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 * Called before a frame is rendered for the first time since setting the surface, and each time
...@@ -82,12 +82,12 @@ public interface VideoRendererEventListener { ...@@ -82,12 +82,12 @@ public interface VideoRendererEventListener {
* this is not possible. Applications that use {@link TextureView} can apply the rotation by * 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 * calling {@link TextureView#setTransform}. Applications that do not expect to encounter
* rotated videos can safely ignore this parameter. * rotated videos can safely ignore this parameter.
* @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case of
* of square pixels this will be equal to 1.0. Different values are indicative of anamorphic * square pixels this will be equal to 1.0. Different values are indicative of anamorphic
* content. * content.
*/ */
void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, default void onVideoSizeChanged(
float pixelWidthHeightRatio); 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 * 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 { ...@@ -96,14 +96,14 @@ public interface VideoRendererEventListener {
* @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if * @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}. * 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. * Called when the renderer is disabled.
* *
* @param counters {@link DecoderCounters} that were updated by the renderer. * @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}. * Dispatches events to a {@link VideoRendererEventListener}.
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 26125 duration = 26122
getPosition(0) = [[timeUs=0, position=0]] getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 26125 duration = 26122
getPosition(0) = [[timeUs=0, position=0]] getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 26125 duration = 26122
getPosition(0) = [[timeUs=0, position=0]] getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
......
seekMap: seekMap:
isSeekable = true isSeekable = true
duration = 26125 duration = 26122
getPosition(0) = [[timeUs=0, position=0]] getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1 numberOfTracks = 1
track 0: track 0:
......
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:smpte="http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt" xml:lang="eng">
<head>
<metadata>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_0">
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
</smpte:image>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_1">
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
</smpte:image>
</metadata>
<styling>
<style/>
</styling>
<layout>
<region xml:id="region_0" tts:extent="51% 12%" tts:origin="24% 78%"/>
<region xml:id="region_1" tts:extent="57% 6%" tts:origin="21% 85%"/>
<region xml:id="region_2" tts:extent="51% 12%" tts:origin="24% 28%"/>
<region xml:id="region_3" tts:extent="57% 6%" tts:origin="21% 35%"/>
</layout>
</head>
<body>
<div begin="00:00:00.200" end="00:00:03.000" region="region_2" smpte:backgroundImage="#img_0"/>
<div begin="00:00:03.200" end="00:00:06.937" region="region_3" smpte:backgroundImage="#img_1"/>
<div begin="00:00:07.200" end="00:59:03.000" region="region_2" smpte:backgroundImage="#img_0"/>
</body>
</tt>
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:smpte="http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt" xml:lang="eng" tts:extent="1280px 720px">
<head>
<metadata>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_0">
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
</smpte:image>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_1">
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
</smpte:image>
</metadata>
<styling>
<style/>
</styling>
<layout>
<region xml:id="region_0" tts:extent="653px 86px" tts:origin="307px 562px"/>
<region xml:id="region_1" tts:extent="730px 43px" tts:origin="269px 612px"/>
</layout>
</head>
<body>
<div begin="00:00:00.200" end="00:00:03.000" region="region_0" smpte:backgroundImage="#img_0"/>
<div begin="00:00:03.200" end="00:00:06.937" region="region_1" smpte:backgroundImage="#img_1"/>
</body>
</tt>
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:ttm="http://www.w3.org/ns/ttml#metadata" xmlns:tts="http://www.w3.org/ns/ttml#styling" xmlns:smpte="http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt" xml:lang="eng">
<head>
<metadata>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_0">
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
</smpte:image>
<smpte:image imagetype="PNG" encoding="Base64" xml:id="img_1">
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
</smpte:image>
</metadata>
<styling>
<style/>
</styling>
<layout>
<region xml:id="region_0" tts:extent="653px 86px" tts:origin="307px 562px"/>
<region xml:id="region_1" tts:extent="730px 43px" tts:origin="269px 612px"/>
</layout>
</head>
<body>
<div begin="00:00:00.200" end="00:00:03.000" region="region_0" smpte:backgroundImage="#img_0"/>
<div begin="00:00:03.200" end="00:00:06.937" region="region_1" smpte:backgroundImage="#img_1"/>
</body>
</tt>
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; 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.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
...@@ -198,7 +200,7 @@ public class AdtsReaderTest { ...@@ -198,7 +200,7 @@ public class AdtsReaderTest {
private void maybeStartPacket() { private void maybeStartPacket() {
if (firstFeed) { if (firstFeed) {
adtsReader.packetStarted(0, true); adtsReader.packetStarted(0, FLAG_DATA_ALIGNMENT_INDICATOR);
firstFeed = false; firstFeed = false;
} }
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_PAYLOAD_UNIT_START_INDICATOR;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
...@@ -55,7 +56,7 @@ public final class SectionReaderTest { ...@@ -55,7 +56,7 @@ public final class SectionReaderTest {
public void testSingleOnePacketSection() { public void testSingleOnePacketSection() {
packetPayload[0] = 3; packetPayload[0] = 3;
insertTableSection(4, (byte) 99, 3); insertTableSection(4, (byte) 99, 3);
reader.consume(new ParsableByteArray(packetPayload), true); reader.consume(new ParsableByteArray(packetPayload), FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(99)); assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(99));
} }
...@@ -65,12 +66,12 @@ public final class SectionReaderTest { ...@@ -65,12 +66,12 @@ public final class SectionReaderTest {
insertTableSection(4, (byte) 100, 3); // This section header spreads across both packets. insertTableSection(4, (byte) 100, 3); // This section header spreads across both packets.
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 5); ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 5);
reader.consume(firstPacket, true); reader.consume(firstPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEmpty(); assertThat(payloadReader.parsedTableIds).isEmpty();
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload); ParsableByteArray secondPacket = new ParsableByteArray(packetPayload);
secondPacket.setPosition(5); secondPacket.setPosition(5);
reader.consume(secondPacket, false); reader.consume(secondPacket, /* flags= */ 0);
assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(100)); assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(100));
} }
...@@ -85,12 +86,12 @@ public final class SectionReaderTest { ...@@ -85,12 +86,12 @@ public final class SectionReaderTest {
insertTableSection(54, (byte) 105, 10); insertTableSection(54, (byte) 105, 10);
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 40); ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 40);
reader.consume(firstPacket, true); reader.consume(firstPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEqualTo(asList(101, 102, 103)); assertThat(payloadReader.parsedTableIds).isEqualTo(asList(101, 102, 103));
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload); ParsableByteArray secondPacket = new ParsableByteArray(packetPayload);
secondPacket.setPosition(40); secondPacket.setPosition(40);
reader.consume(secondPacket, true); reader.consume(secondPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEqualTo(asList(101, 102, 103, 104, 105)); assertThat(payloadReader.parsedTableIds).isEqualTo(asList(101, 102, 103, 104, 105));
} }
...@@ -105,22 +106,22 @@ public final class SectionReaderTest { ...@@ -105,22 +106,22 @@ public final class SectionReaderTest {
insertTableSection(318, (byte) 108, 10); insertTableSection(318, (byte) 108, 10);
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100); ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100);
reader.consume(firstPacket, true); reader.consume(firstPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEmpty(); assertThat(payloadReader.parsedTableIds).isEmpty();
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200); ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200);
secondPacket.setPosition(100); secondPacket.setPosition(100);
reader.consume(secondPacket, false); reader.consume(secondPacket, /* flags= */ 0);
assertThat(payloadReader.parsedTableIds).isEmpty(); assertThat(payloadReader.parsedTableIds).isEmpty();
ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300); ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300);
thirdPacket.setPosition(200); thirdPacket.setPosition(200);
reader.consume(thirdPacket, false); reader.consume(thirdPacket, /* flags= */ 0);
assertThat(payloadReader.parsedTableIds).isEmpty(); assertThat(payloadReader.parsedTableIds).isEmpty();
ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload); ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload);
fourthPacket.setPosition(300); fourthPacket.setPosition(300);
reader.consume(fourthPacket, true); reader.consume(fourthPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEqualTo(asList(107, 108)); assertThat(payloadReader.parsedTableIds).isEqualTo(asList(107, 108));
} }
...@@ -135,24 +136,24 @@ public final class SectionReaderTest { ...@@ -135,24 +136,24 @@ public final class SectionReaderTest {
insertTableSection(318, (byte) 111, 10); insertTableSection(318, (byte) 111, 10);
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100); ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100);
reader.consume(firstPacket, true); reader.consume(firstPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEmpty(); assertThat(payloadReader.parsedTableIds).isEmpty();
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200); ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200);
secondPacket.setPosition(100); secondPacket.setPosition(100);
reader.consume(secondPacket, false); reader.consume(secondPacket, /* flags= */ 0);
assertThat(payloadReader.parsedTableIds).isEmpty(); assertThat(payloadReader.parsedTableIds).isEmpty();
ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300); ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300);
thirdPacket.setPosition(200); thirdPacket.setPosition(200);
reader.consume(thirdPacket, false); reader.consume(thirdPacket, /* flags= */ 0);
assertThat(payloadReader.parsedTableIds).isEmpty(); assertThat(payloadReader.parsedTableIds).isEmpty();
reader.seek(); reader.seek();
ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload); ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload);
fourthPacket.setPosition(300); fourthPacket.setPosition(300);
reader.consume(fourthPacket, true); reader.consume(fourthPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(111)); assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(111));
} }
...@@ -165,9 +166,9 @@ public final class SectionReaderTest { ...@@ -165,9 +166,9 @@ public final class SectionReaderTest {
byte[] incorrectCrcPat = Arrays.copyOf(correctCrcPat, correctCrcPat.length); byte[] incorrectCrcPat = Arrays.copyOf(correctCrcPat, correctCrcPat.length);
// Crc field is incorrect, and should not be passed to the payload reader. // Crc field is incorrect, and should not be passed to the payload reader.
incorrectCrcPat[16]--; incorrectCrcPat[16]--;
reader.consume(new ParsableByteArray(correctCrcPat), true); reader.consume(new ParsableByteArray(correctCrcPat), FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(0)); assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(0));
reader.consume(new ParsableByteArray(incorrectCrcPat), true); reader.consume(new ParsableByteArray(incorrectCrcPat), FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(0)); assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(0));
} }
......
...@@ -202,7 +202,7 @@ public final class TsExtractorTest { ...@@ -202,7 +202,7 @@ public final class TsExtractorTest {
} }
@Override @Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {} public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {}
@Override @Override
public void consume(ParsableByteArray data) {} public void consume(ParsableByteArray data) {}
......
...@@ -63,6 +63,9 @@ public final class TtmlDecoderTest { ...@@ -63,6 +63,9 @@ public final class TtmlDecoderTest {
private static final String FONT_SIZE_INVALID_TTML_FILE = "ttml/font_size_invalid.xml"; private static final String FONT_SIZE_INVALID_TTML_FILE = "ttml/font_size_invalid.xml";
private static final String FONT_SIZE_EMPTY_TTML_FILE = "ttml/font_size_empty.xml"; private static final String FONT_SIZE_EMPTY_TTML_FILE = "ttml/font_size_empty.xml";
private static final String FRAME_RATE_TTML_FILE = "ttml/frame_rate.xml"; private static final String FRAME_RATE_TTML_FILE = "ttml/frame_rate.xml";
private static final String BITMAP_REGION_FILE = "ttml/bitmap_percentage_region.xml";
private static final String BITMAP_PIXEL_REGION_FILE = "ttml/bitmap_pixel_region.xml";
private static final String BITMAP_UNSUPPORTED_REGION_FILE = "ttml/bitmap_unsupported_region.xml";
@Test @Test
public void testInlineAttributes() throws IOException, SubtitleDecoderException { public void testInlineAttributes() throws IOException, SubtitleDecoderException {
...@@ -259,56 +262,56 @@ public final class TtmlDecoderTest { ...@@ -259,56 +262,56 @@ public final class TtmlDecoderTest {
@Test @Test
public void testMultipleRegions() throws IOException, SubtitleDecoderException { public void testMultipleRegions() throws IOException, SubtitleDecoderException {
TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE); TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE);
List<Cue> output = subtitle.getCues(1000000); List<Cue> cues = subtitle.getCues(1000000);
assertThat(output).hasSize(2); assertThat(cues).hasSize(2);
Cue ttmlCue = output.get(0); Cue cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("lorem"); assertThat(cue.text.toString()).isEqualTo("lorem");
assertThat(ttmlCue.position).isEqualTo(10f / 100f); assertThat(cue.position).isEqualTo(10f / 100f);
assertThat(ttmlCue.line).isEqualTo(10f / 100f); assertThat(cue.line).isEqualTo(10f / 100f);
assertThat(ttmlCue.size).isEqualTo(20f / 100f); assertThat(cue.size).isEqualTo(20f / 100f);
ttmlCue = output.get(1); cue = cues.get(1);
assertThat(ttmlCue.text.toString()).isEqualTo("amet"); assertThat(cue.text.toString()).isEqualTo("amet");
assertThat(ttmlCue.position).isEqualTo(60f / 100f); assertThat(cue.position).isEqualTo(60f / 100f);
assertThat(ttmlCue.line).isEqualTo(10f / 100f); assertThat(cue.line).isEqualTo(10f / 100f);
assertThat(ttmlCue.size).isEqualTo(20f / 100f); assertThat(cue.size).isEqualTo(20f / 100f);
output = subtitle.getCues(5000000); cues = subtitle.getCues(5000000);
assertThat(output).hasSize(1); assertThat(cues).hasSize(1);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("ipsum"); assertThat(cue.text.toString()).isEqualTo("ipsum");
assertThat(ttmlCue.position).isEqualTo(40f / 100f); assertThat(cue.position).isEqualTo(40f / 100f);
assertThat(ttmlCue.line).isEqualTo(40f / 100f); assertThat(cue.line).isEqualTo(40f / 100f);
assertThat(ttmlCue.size).isEqualTo(20f / 100f); assertThat(cue.size).isEqualTo(20f / 100f);
output = subtitle.getCues(9000000); cues = subtitle.getCues(9000000);
assertThat(output).hasSize(1); assertThat(cues).hasSize(1);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("dolor"); assertThat(cue.text.toString()).isEqualTo("dolor");
assertThat(ttmlCue.position).isEqualTo(Cue.DIMEN_UNSET); assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
assertThat(ttmlCue.line).isEqualTo(Cue.DIMEN_UNSET); assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
assertThat(ttmlCue.size).isEqualTo(Cue.DIMEN_UNSET); assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
// TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed. // TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed.
// assertEquals(10f / 100f, ttmlCue.position); // assertEquals(10f / 100f, cue.position);
// assertEquals(80f / 100f, ttmlCue.line); // assertEquals(80f / 100f, cue.line);
// assertEquals(1f, ttmlCue.size); // assertEquals(1f, cue.size);
output = subtitle.getCues(21000000); cues = subtitle.getCues(21000000);
assertThat(output).hasSize(1); assertThat(cues).hasSize(1);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("She first said this"); assertThat(cue.text.toString()).isEqualTo("She first said this");
assertThat(ttmlCue.position).isEqualTo(45f / 100f); assertThat(cue.position).isEqualTo(45f / 100f);
assertThat(ttmlCue.line).isEqualTo(45f / 100f); assertThat(cue.line).isEqualTo(45f / 100f);
assertThat(ttmlCue.size).isEqualTo(35f / 100f); assertThat(cue.size).isEqualTo(35f / 100f);
output = subtitle.getCues(25000000); cues = subtitle.getCues(25000000);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("She first said this\nThen this"); assertThat(cue.text.toString()).isEqualTo("She first said this\nThen this");
output = subtitle.getCues(29000000); cues = subtitle.getCues(29000000);
assertThat(output).hasSize(1); assertThat(cues).hasSize(1);
ttmlCue = output.get(0); cue = cues.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("She first said this\nThen this\nFinally this"); assertThat(cue.text.toString()).isEqualTo("She first said this\nThen this\nFinally this");
assertThat(ttmlCue.position).isEqualTo(45f / 100f); assertThat(cue.position).isEqualTo(45f / 100f);
assertThat(ttmlCue.line).isEqualTo(45f / 100f); assertThat(cue.line).isEqualTo(45f / 100f);
} }
@Test @Test
...@@ -499,6 +502,91 @@ public final class TtmlDecoderTest { ...@@ -499,6 +502,91 @@ public final class TtmlDecoderTest {
assertThat((double) subtitle.getEventTime(3)).isWithin(2000).of(2_002_000_000); assertThat((double) subtitle.getEventTime(3)).isWithin(2000).of(2_002_000_000);
} }
@Test
public void testBitmapPercentageRegion() throws IOException, SubtitleDecoderException {
TtmlSubtitle subtitle = getSubtitle(BITMAP_REGION_FILE);
List<Cue> cues = subtitle.getCues(1000000);
assertThat(cues).hasSize(1);
Cue cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(24f / 100f);
assertThat(cue.line).isEqualTo(28f / 100f);
assertThat(cue.size).isEqualTo(51f / 100f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
cues = subtitle.getCues(4000000);
assertThat(cues).hasSize(1);
cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(21f / 100f);
assertThat(cue.line).isEqualTo(35f / 100f);
assertThat(cue.size).isEqualTo(57f / 100f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
cues = subtitle.getCues(7500000);
assertThat(cues).hasSize(1);
cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(24f / 100f);
assertThat(cue.line).isEqualTo(28f / 100f);
assertThat(cue.size).isEqualTo(51f / 100f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
}
@Test
public void testBitmapPixelRegion() throws IOException, SubtitleDecoderException {
TtmlSubtitle subtitle = getSubtitle(BITMAP_PIXEL_REGION_FILE);
List<Cue> cues = subtitle.getCues(1000000);
assertThat(cues).hasSize(1);
Cue cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(307f / 1280f);
assertThat(cue.line).isEqualTo(562f / 720f);
assertThat(cue.size).isEqualTo(653f / 1280f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
cues = subtitle.getCues(4000000);
assertThat(cues).hasSize(1);
cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(269f / 1280f);
assertThat(cue.line).isEqualTo(612f / 720f);
assertThat(cue.size).isEqualTo(730f / 1280f);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
}
@Test
public void testBitmapUnsupportedRegion() throws IOException, SubtitleDecoderException {
TtmlSubtitle subtitle = getSubtitle(BITMAP_UNSUPPORTED_REGION_FILE);
List<Cue> cues = subtitle.getCues(1000000);
assertThat(cues).hasSize(1);
Cue cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
cues = subtitle.getCues(4000000);
assertThat(cues).hasSize(1);
cue = cues.get(0);
assertThat(cue.text).isNull();
assertThat(cue.bitmap).isNotNull();
assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.line).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.size).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.bitmapHeight).isEqualTo(Cue.DIMEN_UNSET);
}
private void assertSpans( private void assertSpans(
TtmlSubtitle subtitle, TtmlSubtitle subtitle,
int second, int second,
......
...@@ -489,11 +489,7 @@ public final class CacheDataSourceTest { ...@@ -489,11 +489,7 @@ public final class CacheDataSourceTest {
NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(expectedCacheKey); NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(expectedCacheKey);
for (CacheSpan cachedSpan : cachedSpans) { for (CacheSpan cachedSpan : cachedSpans) {
if (cachedSpan.position >= halfDataLength) { if (cachedSpan.position >= halfDataLength) {
try { cache.removeSpan(cachedSpan);
cache.removeSpan(cachedSpan);
} catch (Cache.CacheException e) {
// do nothing
}
} }
} }
......
...@@ -47,6 +47,7 @@ import org.robolectric.RuntimeEnvironment; ...@@ -47,6 +47,7 @@ import org.robolectric.RuntimeEnvironment;
public class SimpleCacheTest { public class SimpleCacheTest {
private static final String KEY_1 = "key1"; private static final String KEY_1 = "key1";
private static final String KEY_2 = "key2";
private File cacheDir; private File cacheDir;
...@@ -153,6 +154,40 @@ public class SimpleCacheTest { ...@@ -153,6 +154,40 @@ public class SimpleCacheTest {
} }
@Test @Test
public void testReloadCacheWithoutRelease() throws Exception {
SimpleCache simpleCache = getSimpleCache();
// Write data for KEY_1.
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
// Write and remove data for KEY_2.
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_2, 0);
addCache(simpleCache, KEY_2, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan2);
simpleCache.removeSpan(simpleCache.getCachedSpans(KEY_2).first());
// Don't release the cache. This means the index file wont have been written to disk after the
// data for KEY_2 was removed. Move the cache instead, so we can reload it without failing the
// folder locking check.
File cacheDir2 = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest");
cacheDir2.delete();
cacheDir.renameTo(cacheDir2);
// Reload the cache from its new location.
simpleCache = new SimpleCache(cacheDir2, new NoOpCacheEvictor());
// Read data back for KEY_1.
CacheSpan cacheSpan3 = simpleCache.startReadWrite(KEY_1, 0);
assertCachedDataReadCorrect(cacheSpan3);
// Check the entry for KEY_2 was removed when the cache was reloaded.
assertThat(simpleCache.getCachedSpans(KEY_2)).isEmpty();
Util.recursiveDelete(cacheDir2);
}
@Test
public void testEncryptedIndex() throws Exception { public void testEncryptedIndex() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key); SimpleCache simpleCache = getEncryptedSimpleCache(key);
......
...@@ -608,6 +608,12 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -608,6 +608,12 @@ public final class DashMediaSource extends BaseMediaSource {
// MediaSource implementation. // MediaSource implementation.
@Override @Override
@Nullable
public Object getTag() {
return tag;
}
@Override
public void prepareSourceInternal( public void prepareSourceInternal(
ExoPlayer player, ExoPlayer player,
boolean isTopLevelSource, boolean isTopLevelSource,
......
...@@ -391,6 +391,12 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -391,6 +391,12 @@ public final class HlsMediaSource extends BaseMediaSource
} }
@Override @Override
@Nullable
public Object getTag() {
return tag;
}
@Override
public void prepareSourceInternal( public void prepareSourceInternal(
ExoPlayer player, ExoPlayer player,
boolean isTopLevelSource, boolean isTopLevelSource,
......
...@@ -504,6 +504,12 @@ public final class SsMediaSource extends BaseMediaSource ...@@ -504,6 +504,12 @@ public final class SsMediaSource extends BaseMediaSource
// MediaSource implementation. // MediaSource implementation.
@Override @Override
@Nullable
public Object getTag() {
return tag;
}
@Override
public void prepareSourceInternal( public void prepareSourceInternal(
ExoPlayer player, ExoPlayer player,
boolean isTopLevelSource, boolean isTopLevelSource,
......
...@@ -29,7 +29,6 @@ import android.graphics.drawable.BitmapDrawable; ...@@ -29,7 +29,6 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.util.AttributeSet; import android.util.AttributeSet;
...@@ -187,8 +186,9 @@ import java.util.List; ...@@ -187,8 +186,9 @@ import java.util.List;
* <li>Type: {@link AspectRatioFrameLayout} * <li>Type: {@link AspectRatioFrameLayout}
* </ul> * </ul>
* <li><b>{@code exo_shutter}</b> - A view that's made visible when video should be hidden. This * <li><b>{@code exo_shutter}</b> - A view that's made visible when video should be hidden. This
* view is typically an opaque view that covers the video surface view, thereby obscuring it * view is typically an opaque view that covers the video surface, thereby obscuring it when
* when visible. * visible. Obscuring the surface in this way also helps to prevent flicker at the start of
* playback when {@code surface_type="surface_view"}.
* <ul> * <ul>
* <li>Type: {@link View} * <li>Type: {@link View}
* </ul> * </ul>
...@@ -271,13 +271,13 @@ public class PlayerView extends FrameLayout { ...@@ -271,13 +271,13 @@ public class PlayerView extends FrameLayout {
private static final int SURFACE_TYPE_MONO360_VIEW = 3; private static final int SURFACE_TYPE_MONO360_VIEW = 3;
// LINT.ThenChange(../../../../../../res/values/attrs.xml) // LINT.ThenChange(../../../../../../res/values/attrs.xml)
private final AspectRatioFrameLayout contentFrame; @Nullable private final AspectRatioFrameLayout contentFrame;
private final View shutterView; private final View shutterView;
private final View surfaceView; @Nullable private final View surfaceView;
private final ImageView artworkView; private final ImageView artworkView;
private final SubtitleView subtitleView; private final SubtitleView subtitleView;
private final @Nullable View bufferingView; @Nullable private final View bufferingView;
private final @Nullable TextView errorMessageView; @Nullable private final TextView errorMessageView;
private final PlayerControlView controller; private final PlayerControlView controller;
private final ComponentListener componentListener; private final ComponentListener componentListener;
private final FrameLayout overlayFrameLayout; private final FrameLayout overlayFrameLayout;
...@@ -285,11 +285,11 @@ public class PlayerView extends FrameLayout { ...@@ -285,11 +285,11 @@ public class PlayerView extends FrameLayout {
private Player player; private Player player;
private boolean useController; private boolean useController;
private boolean useArtwork; private boolean useArtwork;
private @Nullable Drawable defaultArtwork; @Nullable private Drawable defaultArtwork;
private @ShowBuffering int showBuffering; private @ShowBuffering int showBuffering;
private boolean keepContentOnPlayerReset; private boolean keepContentOnPlayerReset;
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider; @Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private @Nullable CharSequence customErrorMessage; @Nullable private CharSequence customErrorMessage;
private int controllerShowTimeoutMs; private int controllerShowTimeoutMs;
private boolean controllerAutoShow; private boolean controllerAutoShow;
private boolean controllerHideDuringAds; private boolean controllerHideDuringAds;
...@@ -474,9 +474,7 @@ public class PlayerView extends FrameLayout { ...@@ -474,9 +474,7 @@ public class PlayerView extends FrameLayout {
* @param newPlayerView The new view to attach to the player. * @param newPlayerView The new view to attach to the player.
*/ */
public static void switchTargetView( public static void switchTargetView(
@NonNull Player player, Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView) {
@Nullable PlayerView oldPlayerView,
@Nullable PlayerView newPlayerView) {
if (oldPlayerView == newPlayerView) { if (oldPlayerView == newPlayerView) {
return; return;
} }
...@@ -1074,6 +1072,26 @@ public class PlayerView extends FrameLayout { ...@@ -1074,6 +1072,26 @@ public class PlayerView extends FrameLayout {
} }
} }
/**
* Called when there's a change in the aspect ratio of the content being displayed. The default
* implementation sets the aspect ratio of the content frame to that of the content, unless the
* content view is a {@link SphericalSurfaceView} in which case the frame's aspect ratio is
* cleared.
*
* @param contentAspectRatio The aspect ratio of the content.
* @param contentFrame The content frame, or {@code null}.
* @param contentView The view that holds the content being displayed, or {@code null}.
*/
protected void onContentAspectRatioChanged(
float contentAspectRatio,
@Nullable AspectRatioFrameLayout contentFrame,
@Nullable View contentView) {
if (contentFrame != null) {
contentFrame.setAspectRatio(
contentView instanceof SphericalSurfaceView ? 0 : contentAspectRatio);
}
}
private boolean toggleControllerVisibility() { private boolean toggleControllerVisibility() {
if (!useController || player == null) { if (!useController || player == null) {
return false; return false;
...@@ -1187,9 +1205,8 @@ public class PlayerView extends FrameLayout { ...@@ -1187,9 +1205,8 @@ public class PlayerView extends FrameLayout {
int drawableWidth = drawable.getIntrinsicWidth(); int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight(); int drawableHeight = drawable.getIntrinsicHeight();
if (drawableWidth > 0 && drawableHeight > 0) { if (drawableWidth > 0 && drawableHeight > 0) {
if (contentFrame != null) { float artworkAspectRatio = (float) drawableWidth / drawableHeight;
contentFrame.setAspectRatio((float) drawableWidth / drawableHeight); onContentAspectRatioChanged(artworkAspectRatio, contentFrame, artworkView);
}
artworkView.setImageDrawable(drawable); artworkView.setImageDrawable(drawable);
artworkView.setVisibility(VISIBLE); artworkView.setVisibility(VISIBLE);
return true; return true;
...@@ -1322,9 +1339,6 @@ public class PlayerView extends FrameLayout { ...@@ -1322,9 +1339,6 @@ public class PlayerView extends FrameLayout {
@Override @Override
public void onVideoSizeChanged( public void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
if (contentFrame == null) {
return;
}
float videoAspectRatio = float videoAspectRatio =
(height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height;
...@@ -1345,11 +1359,9 @@ public class PlayerView extends FrameLayout { ...@@ -1345,11 +1359,9 @@ public class PlayerView extends FrameLayout {
surfaceView.addOnLayoutChangeListener(this); surfaceView.addOnLayoutChangeListener(this);
} }
applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); applyTextureViewRotation((TextureView) surfaceView, textureViewRotation);
} else if (surfaceView instanceof SphericalSurfaceView) {
videoAspectRatio = 0;
} }
contentFrame.setAspectRatio(videoAspectRatio); onContentAspectRatioChanged(videoAspectRatio, contentFrame, surfaceView);
} }
@Override @Override
......
...@@ -89,6 +89,13 @@ public class FakeMediaSource extends BaseMediaSource { ...@@ -89,6 +89,13 @@ public class FakeMediaSource extends BaseMediaSource {
} }
@Override @Override
@Nullable
public Object getTag() {
boolean hasTimeline = timeline != null && !timeline.isEmpty();
return hasTimeline ? timeline.getWindow(0, new Timeline.Window()).tag : null;
}
@Override
public synchronized void prepareSourceInternal( public synchronized void prepareSourceInternal(
ExoPlayer player, ExoPlayer player,
boolean isTopLevelSource, boolean isTopLevelSource,
......
...@@ -50,6 +50,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { ...@@ -50,6 +50,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
} }
@Override @Override
public MetadataComponent getMetadataComponent() {
throw new UnsupportedOperationException();
}
@Override
public Looper getPlaybackLooper() { public Looper getPlaybackLooper() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment