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

r2.9.3
This commit is contained in:
Oliver Woodman 2018-12-21 14:13:59 +00:00 committed by GitHub
commit 71f72c5953
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1183 additions and 475 deletions

View file

@ -1,5 +1,24 @@
# Release notes #
### 2.9.3 ###
* Captions: Support PNG subtitles in SMPTE-TT
([#1583](https://github.com/google/ExoPlayer/issues/1583)).
* MPEG-TS: Use random access indicators to minimize the need for
`FLAG_ALLOW_NON_IDR_KEYFRAMES`.
* Downloading: Reduce time taken to remove downloads
([#5136](https://github.com/google/ExoPlayer/issues/5136)).
* MP3:
* Use the true bitrate for constant-bitrate MP3 seeking.
* Fix issue where streams would play twice on some Samsung devices
([#4519](https://github.com/google/ExoPlayer/issues/4519)).
* Fix regression where some audio formats were incorrectly marked as being
unplayable due to under-reporting of platform decoder capabilities
([#5145](https://github.com/google/ExoPlayer/issues/5145)).
* Fix decode-only frame skipping on Nvidia Shield TV devices.
* Workaround for MiTV (dangal) issue when swapping output surface
([#5169](https://github.com/google/ExoPlayer/issues/5169)).
### 2.9.2 ###
* HLS:
@ -47,10 +66,10 @@
* DASH: Parse ProgramInformation element if present in the manifest.
* HLS:
* Add constructor to `DefaultHlsExtractorFactory` for adding TS payload
reader factory flags.
reader factory flags
([#4861](https://github.com/google/ExoPlayer/issues/4861)).
* Fix bug in segment sniffing
([#5039](https://github.com/google/ExoPlayer/issues/5039)).
([#4861](https://github.com/google/ExoPlayer/issues/4861)).
* SubRip: Add support for alignment tags, and remove tags from the displayed
captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).
* Fix issue with blind seeking to windows with non-zero offset in a

View file

@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.9.2'
releaseVersionCode = 2009002
releaseVersion = '2.9.3'
releaseVersionCode = 2009003
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided

View file

@ -283,20 +283,29 @@ public final class CastPlayer extends BasePlayer {
// Player implementation.
@Override
@Nullable
public AudioComponent getAudioComponent() {
return null;
}
@Override
@Nullable
public VideoComponent getVideoComponent() {
return null;
}
@Override
@Nullable
public TextComponent getTextComponent() {
return null;
}
@Override
@Nullable
public MetadataComponent getMetadataComponent() {
return null;
}
@Override
public Looper getApplicationLooper() {
return Looper.getMainLooper();

View file

@ -76,6 +76,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn
adUiViewGroup, eventHandler, eventListener);
}
@Override
@Nullable
public Object getTag() {
return adsMediaSource.getTag();
}
@Override
public void prepareSourceInternal(
final ExoPlayer player,

View file

@ -64,13 +64,6 @@ public final class TimelineQueueEditor
* {@link MediaSessionConnector}.
*/
public interface QueueDataAdapter {
/**
* Gets the {@link MediaDescriptionCompat} for a {@code position}.
*
* @param position The position in the queue for which to provide a description.
* @return A {@link MediaDescriptionCompat}.
*/
MediaDescriptionCompat getMediaDescription(int position);
/**
* Adds a {@link MediaDescriptionCompat} at the given {@code position}.
*

View file

@ -144,20 +144,29 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
@Override
@Nullable
public AudioComponent getAudioComponent() {
return null;
}
@Override
@Nullable
public VideoComponent getVideoComponent() {
return null;
}
@Override
@Nullable
public TextComponent getTextComponent() {
return null;
}
@Override
@Nullable
public MetadataComponent getMetadataComponent() {
return null;
}
@Override
public Looper getPlaybackLooper() {
return internalPlayer.getPlaybackLooper();

View file

@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.9.2";
public static final String VERSION = "2.9.3";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.2";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.3";
/**
* The version of the library expressed as an integer, for example 1002003.
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2009002;
public static final int VERSION_INT = 2009003;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View file

@ -26,6 +26,7 @@ import com.google.android.exoplayer2.C.VideoScalingMode;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.audio.AudioListener;
import com.google.android.exoplayer2.audio.AuxEffectInfo;
import com.google.android.exoplayer2.metadata.MetadataOutput;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@ -299,6 +300,24 @@ public interface Player {
void removeTextOutput(TextOutput listener);
}
/** The metadata component of a {@link Player}. */
interface MetadataComponent {
/**
* Adds a {@link MetadataOutput} to receive metadata.
*
* @param output The output to register.
*/
void addMetadataOutput(MetadataOutput output);
/**
* Removes a {@link MetadataOutput}.
*
* @param output The output to remove.
*/
void removeMetadataOutput(MetadataOutput output);
}
/**
* Listener of changes in player state. All methods have no-op default implementations to allow
* selective overrides.
@ -533,6 +552,12 @@ public interface Player {
@Nullable
TextComponent getTextComponent();
/**
* Returns the component of this player for metadata output, or null if metadata is not supported.
*/
@Nullable
MetadataComponent getMetadataComponent();
/**
* Returns the {@link Looper} associated with the application thread that's used to access the
* player and on which player events are received.

View file

@ -65,7 +65,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
*/
@TargetApi(16)
public class SimpleExoPlayer extends BasePlayer
implements ExoPlayer, Player.AudioComponent, Player.VideoComponent, Player.TextComponent {
implements ExoPlayer,
Player.AudioComponent,
Player.VideoComponent,
Player.TextComponent,
Player.MetadataComponent {
/** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */
@Deprecated
@ -243,20 +247,29 @@ public class SimpleExoPlayer extends BasePlayer
}
@Override
@Nullable
public AudioComponent getAudioComponent() {
return this;
}
@Override
@Nullable
public VideoComponent getVideoComponent() {
return this;
}
@Override
@Nullable
public TextComponent getTextComponent() {
return this;
}
@Override
@Nullable
public MetadataComponent getMetadataComponent() {
return this;
}
/**
* Sets the video scaling mode.
*
@ -713,20 +726,12 @@ public class SimpleExoPlayer extends BasePlayer
removeTextOutput(output);
}
/**
* Adds a {@link MetadataOutput} to receive metadata.
*
* @param listener The output to register.
*/
@Override
public void addMetadataOutput(MetadataOutput listener) {
metadataOutputs.add(listener);
}
/**
* Removes a {@link MetadataOutput}.
*
* @param listener The output to remove.
*/
@Override
public void removeMetadataOutput(MetadataOutput listener) {
metadataOutputs.remove(listener);
}

View file

@ -25,7 +25,8 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.util.Assertions;
/**
* Listener of audio {@link Renderer} events.
* Listener of audio {@link Renderer} events. All methods have no-op default implementations to
* allow selective overrides.
*/
public interface AudioRendererEventListener {
@ -35,14 +36,14 @@ public interface AudioRendererEventListener {
* @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it
* remains enabled.
*/
void onAudioEnabled(DecoderCounters counters);
default void onAudioEnabled(DecoderCounters counters) {}
/**
* Called when the audio session is set.
*
* @param audioSessionId The audio session id.
*/
void onAudioSessionId(int audioSessionId);
default void onAudioSessionId(int audioSessionId) {}
/**
* Called when a decoder is created.
@ -52,15 +53,15 @@ public interface AudioRendererEventListener {
* finished.
* @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
*/
void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs,
long initializationDurationMs);
default void onAudioDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) {}
/**
* Called when the format of the media being consumed by the renderer changes.
*
* @param format The new format.
*/
void onAudioInputFormatChanged(Format format);
default void onAudioInputFormatChanged(Format format) {}
/**
* Called when an {@link AudioSink} underrun occurs.
@ -71,14 +72,15 @@ public interface AudioRendererEventListener {
* as the buffered media can have a variable bitrate so the duration may be unknown.
* @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data.
*/
void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
default void onAudioSinkUnderrun(
int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {}
/**
* Called when the renderer is disabled.
*
* @param counters {@link DecoderCounters} that were updated by the renderer.
*/
void onAudioDisabled(DecoderCounters counters);
default void onAudioDisabled(DecoderCounters counters) {}
/**
* Dispatches events to a {@link AudioRendererEventListener}.

View file

@ -34,16 +34,26 @@ public final class MpegAudioHeader {
private static final String[] MIME_TYPE_BY_LAYER =
new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG};
private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000};
private static final int[] BITRATE_V1_L1 =
{32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448};
private static final int[] BITRATE_V2_L1 =
{32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256};
private static final int[] BITRATE_V1_L2 =
{32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384};
private static final int[] BITRATE_V1_L3 =
{32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320};
private static final int[] BITRATE_V2 =
{8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160};
private static final int[] BITRATE_V1_L1 = {
32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000,
416000, 448000
};
private static final int[] BITRATE_V2_L1 = {
32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000,
224000, 256000
};
private static final int[] BITRATE_V1_L2 = {
32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000,
320000, 384000
};
private static final int[] BITRATE_V1_L3 = {
32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000,
320000
};
private static final int[] BITRATE_V2 = {
8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000,
160000
};
/**
* Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it
@ -89,7 +99,7 @@ public final class MpegAudioHeader {
if (layer == 3) {
// Layer I (layer == 3)
bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
return (12000 * bitrate / samplingRate + padding) * 4;
return (12 * bitrate / samplingRate + padding) * 4;
} else {
// Layer II (layer == 2) or III (layer == 1)
if (version == 3) {
@ -102,10 +112,10 @@ public final class MpegAudioHeader {
if (version == 3) {
// Version 1
return 144000 * bitrate / samplingRate + padding;
return 144 * bitrate / samplingRate + padding;
} else {
// Version 2 or 2.5
return (layer == 1 ? 72000 : 144000) * bitrate / samplingRate + padding;
return (layer == 1 ? 72 : 144) * bitrate / samplingRate + padding;
}
}
@ -159,7 +169,7 @@ public final class MpegAudioHeader {
if (layer == 3) {
// Layer I (layer == 3)
bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
frameSize = (12000 * bitrate / sampleRate + padding) * 4;
frameSize = (12 * bitrate / sampleRate + padding) * 4;
samplesPerFrame = 384;
} else {
// Layer II (layer == 2) or III (layer == 1)
@ -167,19 +177,22 @@ public final class MpegAudioHeader {
// Version 1
bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
samplesPerFrame = 1152;
frameSize = 144000 * bitrate / sampleRate + padding;
frameSize = 144 * bitrate / sampleRate + padding;
} else {
// Version 2 or 2.5.
bitrate = BITRATE_V2[bitrateIndex - 1];
samplesPerFrame = layer == 1 ? 576 : 1152;
frameSize = (layer == 1 ? 72000 : 144000) * bitrate / sampleRate + padding;
frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding;
}
}
// Calculate the bitrate in the same way Mp3Extractor calculates sample timestamps so that
// seeking to a given timestamp and playing from the start up to that timestamp give the same
// results for CBR streams. See also [internal: b/120390268].
bitrate = 8 * frameSize * sampleRate / samplesPerFrame;
String mimeType = MIME_TYPE_BY_LAYER[3 - layer];
int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate * 1000,
samplesPerFrame);
header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate, samplesPerFrame);
return true;
}
@ -198,8 +211,14 @@ public final class MpegAudioHeader {
/** Number of samples stored in the frame. */
public int samplesPerFrame;
private void setValues(int version, String mimeType, int frameSize, int sampleRate, int channels,
int bitrate, int samplesPerFrame) {
private void setValues(
int version,
String mimeType,
int frameSize,
int sampleRate,
int channels,
int bitrate,
int samplesPerFrame) {
this.version = version;
this.mimeType = mimeType;
this.frameSize = frameSize;

View file

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.audio.Ac3Util;
import com.google.android.exoplayer2.extractor.Extractor;
@ -140,7 +142,7 @@ public final class Ac3Extractor implements Extractor {
if (!startedPacket) {
// Pass data to the reader as though it's contained within a single infinitely long packet.
reader.packetStarted(firstSampleTimestampUs, true);
reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR);
startedPacket = true;
}
// TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes

View file

@ -100,7 +100,7 @@ public final class Ac3Reader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}

View file

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
@ -202,7 +204,7 @@ public final class AdtsExtractor implements Extractor {
if (!startedPacket) {
// Pass data to the reader as though it's contained within a single infinitely long packet.
reader.packetStarted(firstSampleTimestampUs, true);
reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR);
startedPacket = true;
}
// TODO: Make it possible for reader to consume the dataSource directly, so that it becomes

View file

@ -141,7 +141,7 @@ public final class AdtsReader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}

View file

@ -80,7 +80,7 @@ public final class DtsReader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}

View file

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@ -73,8 +75,8 @@ public final class DvbSubtitleReader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
if (!dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
return;
}
writingSample = true;

View file

@ -43,9 +43,9 @@ public interface ElementaryStreamReader {
* Called when a packet starts.
*
* @param pesTimeUs The timestamp associated with the packet.
* @param dataAlignmentIndicator The data alignment indicator associated with the packet.
* @param flags See {@link TsPayloadReader.Flags}.
*/
void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator);
void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags);
/**
* Consumes (possibly partial) data from the current packet.

View file

@ -107,7 +107,8 @@ public final class H262Reader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
// TODO (Internal b/32267012): Consider using random access indicator.
this.pesTimeUs = pesTimeUs;
}

View file

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@ -56,9 +58,12 @@ public final class H264Reader implements ElementaryStreamReader {
// State that should not be reset on seek.
private boolean hasOutputFormat;
// Per packet state that gets reset at the start of each packet.
// Per PES packet state that gets reset at the start of each PES packet.
private long pesTimeUs;
// State inherited from the TS packet header.
private boolean randomAccessIndicator;
// Scratch variables to avoid allocations.
private final ParsableByteArray seiWrapper;
@ -88,6 +93,7 @@ public final class H264Reader implements ElementaryStreamReader {
sei.reset();
sampleReader.reset();
totalBytesWritten = 0;
randomAccessIndicator = false;
}
@Override
@ -100,8 +106,9 @@ public final class H264Reader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
this.pesTimeUs = pesTimeUs;
randomAccessIndicator |= (flags & FLAG_RANDOM_ACCESS_INDICATOR) != 0;
}
@Override
@ -220,12 +227,17 @@ public final class H264Reader implements ElementaryStreamReader {
seiWrapper.setPosition(4); // NAL prefix and nal_unit() header.
seiReader.consume(pesTimeUs, seiWrapper);
}
sampleReader.endNalUnit(position, offset);
boolean sampleIsKeyFrame =
sampleReader.endNalUnit(position, offset, hasOutputFormat, randomAccessIndicator);
if (sampleIsKeyFrame) {
// This is either an IDR frame or the first I-frame since the random access indicator, so mark
// it as a keyframe. Clear the flag so that subsequent non-IDR I-frames are not marked as
// keyframes until we see another random access indicator.
randomAccessIndicator = false;
}
}
/**
* Consumes a stream of NAL units and outputs samples.
*/
/** Consumes a stream of NAL units and outputs samples. */
private static final class SampleReader {
private static final int DEFAULT_BUFFER_SIZE = 128;
@ -430,11 +442,12 @@ public final class H264Reader implements ElementaryStreamReader {
isFilling = false;
}
public void endNalUnit(long position, int offset) {
public boolean endNalUnit(
long position, int offset, boolean hasOutputFormat, boolean randomAccessIndicator) {
if (nalUnitType == NAL_UNIT_TYPE_AUD
|| (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) {
// If the NAL unit ending is the start of a new sample, output the previous one.
if (readingSample) {
if (hasOutputFormat && readingSample) {
int nalUnitLength = (int) (position - nalUnitStartPosition);
outputSample(offset + nalUnitLength);
}
@ -443,8 +456,12 @@ public final class H264Reader implements ElementaryStreamReader {
sampleIsKeyframe = false;
readingSample = true;
}
sampleIsKeyframe |= nalUnitType == NAL_UNIT_TYPE_IDR || (allowNonIdrKeyframes
&& nalUnitType == NAL_UNIT_TYPE_NON_IDR && sliceHeader.isISlice());
boolean treatIFrameAsKeyframe =
allowNonIdrKeyframes ? sliceHeader.isISlice() : randomAccessIndicator;
sampleIsKeyframe |=
nalUnitType == NAL_UNIT_TYPE_IDR
|| (treatIFrameAsKeyframe && nalUnitType == NAL_UNIT_TYPE_NON_IDR);
return sampleIsKeyframe;
}
private void outputSample(int offset) {
@ -486,10 +503,21 @@ public final class H264Reader implements ElementaryStreamReader {
hasSliceType = true;
}
public void setAll(SpsData spsData, int nalRefIdc, int sliceType, int frameNum,
int picParameterSetId, boolean fieldPicFlag, boolean bottomFieldFlagPresent,
boolean bottomFieldFlag, boolean idrPicFlag, int idrPicId, int picOrderCntLsb,
int deltaPicOrderCntBottom, int deltaPicOrderCnt0, int deltaPicOrderCnt1) {
public void setAll(
SpsData spsData,
int nalRefIdc,
int sliceType,
int frameNum,
int picParameterSetId,
boolean fieldPicFlag,
boolean bottomFieldFlagPresent,
boolean bottomFieldFlag,
boolean idrPicFlag,
int idrPicId,
int picOrderCntLsb,
int deltaPicOrderCntBottom,
int deltaPicOrderCnt0,
int deltaPicOrderCnt1) {
this.spsData = spsData;
this.nalRefIdc = nalRefIdc;
this.sliceType = sliceType;
@ -514,23 +542,26 @@ public final class H264Reader implements ElementaryStreamReader {
private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) {
// See ISO 14496-10 subsection 7.4.1.2.4.
return isComplete && (!other.isComplete || frameNum != other.frameNum
|| picParameterSetId != other.picParameterSetId || fieldPicFlag != other.fieldPicFlag
|| (bottomFieldFlagPresent && other.bottomFieldFlagPresent
&& bottomFieldFlag != other.bottomFieldFlag)
|| (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
|| (spsData.picOrderCountType == 0 && other.spsData.picOrderCountType == 0
&& (picOrderCntLsb != other.picOrderCntLsb
|| deltaPicOrderCntBottom != other.deltaPicOrderCntBottom))
|| (spsData.picOrderCountType == 1 && other.spsData.picOrderCountType == 1
&& (deltaPicOrderCnt0 != other.deltaPicOrderCnt0
|| deltaPicOrderCnt1 != other.deltaPicOrderCnt1))
|| idrPicFlag != other.idrPicFlag
|| (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId));
return isComplete
&& (!other.isComplete
|| frameNum != other.frameNum
|| picParameterSetId != other.picParameterSetId
|| fieldPicFlag != other.fieldPicFlag
|| (bottomFieldFlagPresent
&& other.bottomFieldFlagPresent
&& bottomFieldFlag != other.bottomFieldFlag)
|| (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0))
|| (spsData.picOrderCountType == 0
&& other.spsData.picOrderCountType == 0
&& (picOrderCntLsb != other.picOrderCntLsb
|| deltaPicOrderCntBottom != other.deltaPicOrderCntBottom))
|| (spsData.picOrderCountType == 1
&& other.spsData.picOrderCountType == 1
&& (deltaPicOrderCnt0 != other.deltaPicOrderCnt0
|| deltaPicOrderCnt1 != other.deltaPicOrderCnt1))
|| idrPicFlag != other.idrPicFlag
|| (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId));
}
}
}
}

View file

@ -104,7 +104,8 @@ public final class H265Reader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
// TODO (Internal b/32267012): Consider using random access indicator.
this.pesTimeUs = pesTimeUs;
}

View file

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
@ -63,8 +65,8 @@ public final class Id3Reader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
if (!dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
if ((flags & FLAG_DATA_ALIGNMENT_INDICATOR) == 0) {
return;
}
writingSample = true;

View file

@ -93,7 +93,7 @@ public final class LatmReader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}

View file

@ -83,7 +83,7 @@ public final class MpegAudioReader implements ElementaryStreamReader {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {
timeUs = pesTimeUs;
}

View file

@ -78,9 +78,8 @@ public final class PesReader implements TsPayloadReader {
}
@Override
public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator)
throws ParserException {
if (payloadUnitStartIndicator) {
public final void consume(ParsableByteArray data, @Flags int flags) throws ParserException {
if ((flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0) {
switch (state) {
case STATE_FINDING_HEADER:
case STATE_READING_HEADER:
@ -122,7 +121,8 @@ public final class PesReader implements TsPayloadReader {
if (continueRead(data, pesScratch.data, readLength)
&& continueRead(data, null, extendedHeaderLength)) {
parseHeaderExtension();
reader.packetStarted(timeUs, dataAlignmentIndicator);
flags |= dataAlignmentIndicator ? FLAG_DATA_ALIGNMENT_INDICATOR : 0;
reader.packetStarted(timeUs, flags);
setState(STATE_READING_BODY);
}
break;

View file

@ -343,7 +343,7 @@ public final class PsExtractor implements Extractor {
data.readBytes(pesScratch.data, 0, extendedHeaderLength);
pesScratch.setPosition(0);
parseHeaderExtension();
pesPayloadReader.packetStarted(timeUs, true);
pesPayloadReader.packetStarted(timeUs, TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR);
pesPayloadReader.consume(data);
// We always have complete PES packets with program stream.
pesPayloadReader.packetFinished();

View file

@ -57,7 +57,8 @@ public final class SectionReader implements TsPayloadReader {
}
@Override
public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
public void consume(ParsableByteArray data, @Flags int flags) {
boolean payloadUnitStartIndicator = (flags & FLAG_PAYLOAD_UNIT_START_INDICATOR) != 0;
int payloadStartPosition = C.POSITION_UNSET;
if (payloadUnitStartIndicator) {
int payloadStartOffset = data.readUnsignedByte();

View file

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_PAYLOAD_UNIT_START_INDICATOR;
import android.support.annotation.IntDef;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@ -279,6 +281,8 @@ public final class TsExtractor implements Extractor {
return RESULT_CONTINUE;
}
@TsPayloadReader.Flags int packetHeaderFlags = 0;
// Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
int tsPacketHeader = tsPacketBuffer.readInt();
if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator
@ -286,7 +290,7 @@ public final class TsExtractor implements Extractor {
tsPacketBuffer.setPosition(endOfPacket);
return RESULT_CONTINUE;
}
boolean payloadUnitStartIndicator = (tsPacketHeader & 0x400000) != 0;
packetHeaderFlags |= (tsPacketHeader & 0x400000) != 0 ? FLAG_PAYLOAD_UNIT_START_INDICATOR : 0;
// Ignoring transport_priority (tsPacketHeader & 0x200000)
int pid = (tsPacketHeader & 0x1FFF00) >> 8;
// Ignoring transport_scrambling_control (tsPacketHeader & 0xC0)
@ -317,14 +321,20 @@ public final class TsExtractor implements Extractor {
// Skip the adaptation field.
if (adaptationFieldExists) {
int adaptationFieldLength = tsPacketBuffer.readUnsignedByte();
tsPacketBuffer.skipBytes(adaptationFieldLength);
int adaptationFieldFlags = tsPacketBuffer.readUnsignedByte();
packetHeaderFlags |=
(adaptationFieldFlags & 0x40) != 0 // random_access_indicator.
? TsPayloadReader.FLAG_RANDOM_ACCESS_INDICATOR
: 0;
tsPacketBuffer.skipBytes(adaptationFieldLength - 1 /* flags */);
}
// Read the payload.
boolean wereTracksEnded = tracksEnded;
if (shouldConsumePacketPayload(pid)) {
tsPacketBuffer.setLimit(endOfPacket);
payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator);
payloadReader.consume(tsPacketBuffer, packetHeaderFlags);
tsPacketBuffer.setLimit(limit);
}
if (mode != MODE_HLS && !wereTracksEnded && tracksEnded && inputLength != C.LENGTH_UNSET) {

View file

@ -15,12 +15,16 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import android.support.annotation.IntDef;
import android.util.SparseArray;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.List;
@ -174,6 +178,29 @@ public interface TsPayloadReader {
}
/**
* Contextual flags indicating the presence of indicators in the TS packet or PES packet headers.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {
FLAG_PAYLOAD_UNIT_START_INDICATOR,
FLAG_RANDOM_ACCESS_INDICATOR,
FLAG_DATA_ALIGNMENT_INDICATOR
})
@interface Flags {}
/** Indicates the presence of the payload_unit_start_indicator in the TS packet header. */
int FLAG_PAYLOAD_UNIT_START_INDICATOR = 1;
/**
* Indicates the presence of the random_access_indicator in the TS packet header adaptation field.
*/
int FLAG_RANDOM_ACCESS_INDICATOR = 1 << 1;
/** Indicates the presence of the data_alignment_indicator in the PES header. */
int FLAG_DATA_ALIGNMENT_INDICATOR = 1 << 2;
/**
* Initializes the payload reader.
*
@ -187,10 +214,10 @@ public interface TsPayloadReader {
/**
* Notifies the reader that a seek has occurred.
* <p>
* Following a call to this method, the data passed to the next invocation of
* {@link #consume(ParsableByteArray, boolean)} will not be a continuation of the data that was
* previously passed. Hence the reader should reset any internal state.
*
* <p>Following a call to this method, the data passed to the next invocation of {@link #consume}
* will not be a continuation of the data that was previously passed. Hence the reader should
* reset any internal state.
*/
void seek();
@ -198,9 +225,8 @@ public interface TsPayloadReader {
* Consumes the payload of a TS packet.
*
* @param data The TS packet. The position will be set to the start of the payload.
* @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet.
* @param flags See {@link Flags}.
* @throws ParserException If the payload could not be parsed.
*/
void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) throws ParserException;
void consume(ParsableByteArray data, @Flags int flags) throws ParserException;
}

View file

@ -248,9 +248,15 @@ public final class MediaCodecInfo {
// If we don't know any better, we assume that the profile and level are supported.
return true;
}
int profile = codecProfileAndLevel.first;
int level = codecProfileAndLevel.second;
if (!isVideo && profile != CodecProfileLevel.AACObjectXHE) {
// Some devices/builds under-report audio capabilities, so assume support except for xHE-AAC
// which is not widely supported. See https://github.com/google/ExoPlayer/issues/5145.
return true;
}
for (CodecProfileLevel capabilities : getProfileLevels()) {
if (capabilities.profile == codecProfileAndLevel.first
&& capabilities.level >= codecProfileAndLevel.second) {
if (capabilities.profile == profile && capabilities.level >= level) {
return true;
}
}

View file

@ -1622,7 +1622,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/
private static boolean codecNeedsEosFlushWorkaround(String name) {
return (Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name))
|| (Util.SDK_INT <= 19 && "hb2000".equals(Util.DEVICE)
|| (Util.SDK_INT <= 19
&& ("hb2000".equals(Util.DEVICE) || "stvm8".equals(Util.DEVICE))
&& ("OMX.amlogic.avc.decoder.awesome".equals(name)
|| "OMX.amlogic.avc.decoder.awesome.secure".equals(name)));
}

View file

@ -318,7 +318,21 @@ public final class MediaCodecUtil {
}
// Work around https://github.com/google/ExoPlayer/issues/4519.
if ("OMX.SEC.mp3.dec".equals(name) && "SM-T530".equals(Util.MODEL)) {
if ("OMX.SEC.mp3.dec".equals(name)
&& (Util.MODEL.startsWith("GT-I9152")
|| Util.MODEL.startsWith("GT-I9515")
|| Util.MODEL.startsWith("GT-P5220")
|| Util.MODEL.startsWith("GT-S7580")
|| Util.MODEL.startsWith("SM-G350")
|| Util.MODEL.startsWith("SM-G386")
|| Util.MODEL.startsWith("SM-T231")
|| Util.MODEL.startsWith("SM-T530"))) {
return false;
}
if ("OMX.brcm.audio.mp3.decoder".equals(name)
&& (Util.MODEL.startsWith("GT-I9152")
|| Util.MODEL.startsWith("GT-S7580")
|| Util.MODEL.startsWith("SM-G350"))) {
return false;
}

View file

@ -216,6 +216,12 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> {
window = new Timeline.Window();
}
@Override
@Nullable
public Object getTag() {
return mediaSource.getTag();
}
@Override
public void prepareSourceInternal(
ExoPlayer player,

View file

@ -453,6 +453,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
}
}
@Override
@Nullable
public Object getTag() {
return null;
}
@Override
public final synchronized void prepareSourceInternal(
ExoPlayer player,
@ -820,7 +826,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public MediaSourceHolder(MediaSource mediaSource) {
this.mediaSource = mediaSource;
this.timeline = new DeferredTimeline();
this.timeline = DeferredTimeline.createWithDummyTimeline(mediaSource.getTag());
this.activeMediaPeriods = new ArrayList<>();
this.uid = new Object();
}
@ -945,10 +951,18 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
private static final class DeferredTimeline extends ForwardingTimeline {
private static final Object DUMMY_ID = new Object();
private static final DummyTimeline DUMMY_TIMELINE = new DummyTimeline();
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
* assigned dummy period ID.
@ -962,11 +976,6 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
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) {
super(timeline);
this.replacedId = replacedId;
@ -1010,6 +1019,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
/** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */
private static final class DummyTimeline extends Timeline {
@Nullable private final Object tag;
public DummyTimeline(@Nullable Object tag) {
this.tag = tag;
}
@Override
public int getWindowCount() {
return 1;
@ -1019,7 +1034,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public Window getWindow(
int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) {
return window.set(
/* tag= */ null,
tag,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* isSeekable= */ false,
@ -1069,6 +1084,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
// Do nothing.
}
@Override
@Nullable
public Object getTag() {
return null;
}
@Override
protected void releaseSourceInternal() {
// Do nothing.

View file

@ -358,6 +358,12 @@ public final class ExtractorMediaSource extends BaseMediaSource
this.tag = tag;
}
@Override
@Nullable
public Object getTag() {
return tag;
}
@Override
public void prepareSourceInternal(
ExoPlayer player,

View file

@ -64,6 +64,12 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> {
mediaPeriodToChildMediaPeriodId = new HashMap<>();
}
@Override
@Nullable
public Object getTag() {
return childSource.getTag();
}
@Override
public void prepareSourceInternal(
ExoPlayer player,

View file

@ -220,6 +220,12 @@ public interface MediaSource {
*/
void removeEventListener(MediaSourceEventListener eventListener);
/** Returns the tag set on the media source, or null if none was set. */
@Nullable
default Object getTag() {
return null;
}
/** @deprecated Will be removed in the next release. */
@Deprecated
void prepareSource(

View file

@ -98,6 +98,12 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
timelines = new Timeline[mediaSources.length];
}
@Override
@Nullable
public Object getTag() {
return mediaSources.length > 0 ? mediaSources[0].getTag() : null;
}
@Override
public void prepareSourceInternal(
ExoPlayer player,

View file

@ -185,6 +185,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final boolean treatLoadErrorsAsEndOfStream;
private final Timeline timeline;
@Nullable private final Object tag;
private @Nullable TransferListener transferListener;
@ -287,6 +288,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
this.durationUs = durationUs;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
this.tag = tag;
dataSpec =
new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH);
timeline =
@ -295,6 +297,12 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
// MediaSource implementation.
@Override
@Nullable
public Object getTag() {
return tag;
}
@Override
public void prepareSourceInternal(
ExoPlayer player,

View file

@ -319,6 +319,12 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes());
}
@Override
@Nullable
public Object getTag() {
return contentMediaSource.getTag();
}
@Override
public void prepareSourceInternal(
final ExoPlayer player,

View file

@ -68,6 +68,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
private static final String ATTR_END = "end";
private static final String ATTR_STYLE = "style";
private static final String ATTR_REGION = "region";
private static final String ATTR_IMAGE = "backgroundImage";
private static final Pattern CLOCK_TIME =
Pattern.compile("^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])"
@ -77,6 +78,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$");
private static final Pattern PERCENTAGE_COORDINATES =
Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$");
private static final Pattern PIXEL_COORDINATES =
Pattern.compile("^(\\d+\\.?\\d*?)px (\\d+\\.?\\d*?)px$");
private static final Pattern CELL_RESOLUTION = Pattern.compile("^(\\d+) (\\d+)$");
private static final int DEFAULT_FRAME_RATE = 30;
@ -105,6 +108,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
Map<String, TtmlStyle> globalStyles = new HashMap<>();
Map<String, TtmlRegion> regionMap = new HashMap<>();
Map<String, String> imageMap = new HashMap<>();
regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null));
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
xmlParser.setInput(inputStream, null);
@ -114,6 +118,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
int eventType = xmlParser.getEventType();
FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
CellResolution cellResolution = DEFAULT_CELL_RESOLUTION;
TtsExtent ttsExtent = null;
while (eventType != XmlPullParser.END_DOCUMENT) {
TtmlNode parent = nodeStack.peek();
if (unsupportedNodeDepth == 0) {
@ -122,12 +127,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
if (TtmlNode.TAG_TT.equals(name)) {
frameAndTickRate = parseFrameAndTickRates(xmlParser);
cellResolution = parseCellResolution(xmlParser, DEFAULT_CELL_RESOLUTION);
ttsExtent = parseTtsExtent(xmlParser);
}
if (!isSupportedTag(name)) {
Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
unsupportedNodeDepth++;
} else if (TtmlNode.TAG_HEAD.equals(name)) {
parseHeader(xmlParser, globalStyles, regionMap, cellResolution);
parseHeader(xmlParser, globalStyles, cellResolution, ttsExtent, regionMap, imageMap);
} else {
try {
TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
@ -145,7 +151,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
parent.addChild(TtmlNode.buildTextNode(xmlParser.getText()));
} else if (eventType == XmlPullParser.END_TAG) {
if (xmlParser.getName().equals(TtmlNode.TAG_TT)) {
ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap);
ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap, imageMap);
}
nodeStack.pop();
}
@ -226,11 +232,34 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
}
}
private TtsExtent parseTtsExtent(XmlPullParser xmlParser) {
String ttsExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
if (ttsExtent == null) {
return null;
}
Matcher extentMatcher = PIXEL_COORDINATES.matcher(ttsExtent);
if (!extentMatcher.matches()) {
Log.w(TAG, "Ignoring non-pixel tts extent: " + ttsExtent);
return null;
}
try {
int width = Integer.parseInt(extentMatcher.group(1));
int height = Integer.parseInt(extentMatcher.group(2));
return new TtsExtent(width, height);
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring malformed tts extent: " + ttsExtent);
return null;
}
}
private Map<String, TtmlStyle> parseHeader(
XmlPullParser xmlParser,
Map<String, TtmlStyle> globalStyles,
CellResolution cellResolution,
TtsExtent ttsExtent,
Map<String, TtmlRegion> globalRegions,
CellResolution cellResolution)
Map<String, String> imageMap)
throws IOException, XmlPullParserException {
do {
xmlParser.next();
@ -246,23 +275,41 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
globalStyles.put(style.getId(), style);
}
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) {
TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution);
TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser, cellResolution, ttsExtent);
if (ttmlRegion != null) {
globalRegions.put(ttmlRegion.id, ttmlRegion);
}
} else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_METADATA)) {
parseMetadata(xmlParser, imageMap);
}
} while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD));
return globalStyles;
}
private void parseMetadata(XmlPullParser xmlParser, Map<String, String> imageMap)
throws IOException, XmlPullParserException {
do {
xmlParser.next();
if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_IMAGE)) {
String id = XmlPullParserUtil.getAttributeValue(xmlParser, "id");
if (id != null) {
String encodedBitmapData = xmlParser.nextText();
imageMap.put(id, encodedBitmapData);
}
}
} while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_METADATA));
}
/**
* Parses a region declaration.
*
* <p>If the region defines an origin and extent, it is required that they're defined as
* percentages of the viewport. Region declarations that define origin and extent in other formats
* are unsupported, and null is returned.
* <p>Supports both percentage and pixel defined regions. In case of pixel defined regions the
* passed {@code ttsExtent} is used as a reference window to convert the pixel values to
* fractions. In case of missing tts:extent the pixel defined regions can't be parsed, and null is
* returned.
*/
private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser, CellResolution cellResolution) {
private TtmlRegion parseRegionAttributes(
XmlPullParser xmlParser, CellResolution cellResolution, TtsExtent ttsExtent) {
String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
if (regionId == null) {
return null;
@ -270,13 +317,30 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
float position;
float line;
String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
if (regionOrigin != null) {
Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
if (originMatcher.matches()) {
Matcher originPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
Matcher originPixelMatcher = PIXEL_COORDINATES.matcher(regionOrigin);
if (originPercentageMatcher.matches()) {
try {
position = Float.parseFloat(originMatcher.group(1)) / 100f;
line = Float.parseFloat(originMatcher.group(2)) / 100f;
position = Float.parseFloat(originPercentageMatcher.group(1)) / 100f;
line = Float.parseFloat(originPercentageMatcher.group(2)) / 100f;
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin);
return null;
}
} else if (originPixelMatcher.matches()) {
if (ttsExtent == null) {
Log.w(TAG, "Ignoring region with missing tts:extent: " + regionOrigin);
return null;
}
try {
int width = Integer.parseInt(originPixelMatcher.group(1));
int height = Integer.parseInt(originPixelMatcher.group(2));
// Convert pixel values to fractions.
position = width / (float) ttsExtent.width;
line = height / (float) ttsExtent.height;
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin);
return null;
@ -299,11 +363,27 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
float height;
String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
if (regionExtent != null) {
Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);
if (extentMatcher.matches()) {
Matcher extentPercentageMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);
Matcher extentPixelMatcher = PIXEL_COORDINATES.matcher(regionExtent);
if (extentPercentageMatcher.matches()) {
try {
width = Float.parseFloat(extentMatcher.group(1)) / 100f;
height = Float.parseFloat(extentMatcher.group(2)) / 100f;
width = Float.parseFloat(extentPercentageMatcher.group(1)) / 100f;
height = Float.parseFloat(extentPercentageMatcher.group(2)) / 100f;
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin);
return null;
}
} else if (extentPixelMatcher.matches()) {
if (ttsExtent == null) {
Log.w(TAG, "Ignoring region with missing tts:extent: " + regionOrigin);
return null;
}
try {
int extentWidth = Integer.parseInt(extentPixelMatcher.group(1));
int extentHeight = Integer.parseInt(extentPixelMatcher.group(2));
// Convert pixel values to fractions.
width = extentWidth / (float) ttsExtent.width;
height = extentHeight / (float) ttsExtent.height;
} catch (NumberFormatException e) {
Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin);
return null;
@ -457,6 +537,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
long startTime = C.TIME_UNSET;
long endTime = C.TIME_UNSET;
String regionId = TtmlNode.ANONYMOUS_REGION_ID;
String imageId = null;
String[] styleIds = null;
int attributeCount = parser.getAttributeCount();
TtmlStyle style = parseStyleAttributes(parser, null);
@ -487,6 +568,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
regionId = value;
}
break;
case ATTR_IMAGE:
// Parse URI reference only if refers to an element in the same document (it must start
// with '#'). Resolving URIs from external sources is not supported.
if (value.startsWith("#")) {
imageId = value.substring(1);
}
break;
default:
// Do nothing.
break;
@ -509,7 +597,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
endTime = parent.endTimeUs;
}
}
return TtmlNode.buildNode(parser.getName(), startTime, endTime, style, styleIds, regionId);
return TtmlNode.buildNode(
parser.getName(), startTime, endTime, style, styleIds, regionId, imageId);
}
private static boolean isSupportedTag(String tag) {
@ -525,9 +614,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|| tag.equals(TtmlNode.TAG_LAYOUT)
|| tag.equals(TtmlNode.TAG_REGION)
|| tag.equals(TtmlNode.TAG_METADATA)
|| tag.equals(TtmlNode.TAG_SMPTE_IMAGE)
|| tag.equals(TtmlNode.TAG_SMPTE_DATA)
|| tag.equals(TtmlNode.TAG_SMPTE_INFORMATION);
|| tag.equals(TtmlNode.TAG_IMAGE)
|| tag.equals(TtmlNode.TAG_DATA)
|| tag.equals(TtmlNode.TAG_INFORMATION);
}
private static void parseFontSize(String expression, TtmlStyle out) throws
@ -651,4 +740,15 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
this.rows = rows;
}
}
/** Represents the tts:extent for a TTML file. */
private static final class TtsExtent {
final int width;
final int height;
TtsExtent(int width, int height) {
this.width = width;
this.height = height;
}
}
}

View file

@ -15,7 +15,12 @@
*/
package com.google.android.exoplayer2.text.ttml;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.util.Base64;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.Assertions;
@ -44,9 +49,9 @@ import java.util.TreeSet;
public static final String TAG_LAYOUT = "layout";
public static final String TAG_REGION = "region";
public static final String TAG_METADATA = "metadata";
public static final String TAG_SMPTE_IMAGE = "smpte:image";
public static final String TAG_SMPTE_DATA = "smpte:data";
public static final String TAG_SMPTE_INFORMATION = "smpte:information";
public static final String TAG_IMAGE = "image";
public static final String TAG_DATA = "data";
public static final String TAG_INFORMATION = "information";
public static final String ANONYMOUS_REGION_ID = "";
public static final String ATTR_ID = "id";
@ -75,34 +80,57 @@ import java.util.TreeSet;
public static final String START = "start";
public static final String END = "end";
public final String tag;
public final String text;
@Nullable public final String tag;
@Nullable public final String text;
public final boolean isTextNode;
public final long startTimeUs;
public final long endTimeUs;
public final TtmlStyle style;
@Nullable public final TtmlStyle style;
@Nullable private final String[] styleIds;
public final String regionId;
@Nullable public final String imageId;
private final String[] styleIds;
private final HashMap<String, Integer> nodeStartsByRegion;
private final HashMap<String, Integer> nodeEndsByRegion;
private List<TtmlNode> children;
public static TtmlNode buildTextNode(String text) {
return new TtmlNode(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), C.TIME_UNSET,
C.TIME_UNSET, null, null, ANONYMOUS_REGION_ID);
return new TtmlNode(
/* tag= */ null,
TtmlRenderUtil.applyTextElementSpacePolicy(text),
/* startTimeUs= */ C.TIME_UNSET,
/* endTimeUs= */ C.TIME_UNSET,
/* style= */ null,
/* styleIds= */ null,
ANONYMOUS_REGION_ID,
/* imageId= */ null);
}
public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs,
TtmlStyle style, String[] styleIds, String regionId) {
return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds, regionId);
public static TtmlNode buildNode(
@Nullable String tag,
long startTimeUs,
long endTimeUs,
@Nullable TtmlStyle style,
@Nullable String[] styleIds,
String regionId,
@Nullable String imageId) {
return new TtmlNode(
tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId);
}
private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs,
TtmlStyle style, String[] styleIds, String regionId) {
private TtmlNode(
@Nullable String tag,
@Nullable String text,
long startTimeUs,
long endTimeUs,
@Nullable TtmlStyle style,
@Nullable String[] styleIds,
String regionId,
@Nullable String imageId) {
this.tag = tag;
this.text = text;
this.imageId = imageId;
this.style = style;
this.styleIds = styleIds;
this.isTextNode = text != null;
@ -151,7 +179,8 @@ import java.util.TreeSet;
private void getEventTimes(TreeSet<Long> out, boolean descendsPNode) {
boolean isPNode = TAG_P.equals(tag);
if (descendsPNode || isPNode) {
boolean isDivNode = TAG_DIV.equals(tag);
if (descendsPNode || isPNode || (isDivNode && imageId != null)) {
if (startTimeUs != C.TIME_UNSET) {
out.add(startTimeUs);
}
@ -171,13 +200,46 @@ import java.util.TreeSet;
return styleIds;
}
public List<Cue> getCues(long timeUs, Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMap) {
TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>();
traverseForText(timeUs, false, regionId, regionOutputs);
traverseForStyle(timeUs, globalStyles, regionOutputs);
public List<Cue> getCues(
long timeUs,
Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMap,
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<>();
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());
cues.add(
new Cue(
@ -192,9 +254,22 @@ import java.util.TreeSet;
region.textSizeType,
region.textSize));
}
return cues;
}
private void traverseForImage(
long timeUs, String inheritedRegion, List<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(
long timeUs,
boolean descendsPNode,

View file

@ -32,11 +32,16 @@ import java.util.Map;
private final long[] eventTimesUs;
private final Map<String, TtmlStyle> globalStyles;
private final Map<String, TtmlRegion> regionMap;
private final Map<String, String> imageMap;
public TtmlSubtitle(TtmlNode root, Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMap) {
public TtmlSubtitle(
TtmlNode root,
Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMap,
Map<String, String> imageMap) {
this.root = root;
this.regionMap = regionMap;
this.imageMap = imageMap;
this.globalStyles =
globalStyles != null ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap();
this.eventTimesUs = root.getEventTimesUs();
@ -65,7 +70,7 @@ import java.util.Map;
@Override
public List<Cue> getCues(long timeUs) {
return root.getCues(timeUs, globalStyles, regionMap);
return root.getCues(timeUs, globalStyles, regionMap, imageMap);
}
/* @VisibleForTesting */

View file

@ -82,7 +82,7 @@ public interface Cache {
* Releases the cache. This method must be called when the cache is no longer required. The cache
* must not be used after calling this method.
*/
void release() throws CacheException;
void release();
/**
* Registers a listener to listen for changes to a given key.

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.upstream.cache;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.AtomicFile;
@ -41,6 +42,7 @@ import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Maintains the index of cached content. */
/*package*/ class CachedContentIndex {
@ -52,7 +54,30 @@ import javax.crypto.spec.SecretKeySpec;
private static final int FLAG_ENCRYPTED_INDEX = 1;
private final HashMap<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 Cipher cipher;
private final SecretKeySpec secretKeySpec;
@ -104,6 +129,7 @@ import javax.crypto.spec.SecretKeySpec;
}
keyToContent = new HashMap<>();
idToKey = new SparseArray<>();
removedIds = new SparseBooleanArray();
atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME));
}
@ -124,6 +150,12 @@ import javax.crypto.spec.SecretKeySpec;
}
writeFile();
changed = false;
// Make ids that were removed since the index was last stored eligible for re-use.
int removedIdCount = removedIds.size();
for (int i = 0; i < removedIdCount; i++) {
idToKey.remove(removedIds.keyAt(i));
}
removedIds.clear();
}
/**
@ -168,8 +200,11 @@ import javax.crypto.spec.SecretKeySpec;
CachedContent cachedContent = keyToContent.get(key);
if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) {
keyToContent.remove(key);
idToKey.remove(cachedContent.id);
changed = true;
// Keep an entry in idToKey to stop the id from being reused until the index is next stored.
idToKey.put(cachedContent.id, /* value= */ null);
// Track that the entry should be removed from idToKey when the index is next stored.
removedIds.put(cachedContent.id, /* value= */ true);
}
}

View file

@ -146,13 +146,16 @@ public final class SimpleCache implements Cache {
}
@Override
public synchronized void release() throws CacheException {
public synchronized void release() {
if (released) {
return;
}
listeners.clear();
removeStaleSpans();
try {
removeStaleSpansAndCachedContents();
index.store();
} catch (CacheException e) {
Log.e(TAG, "Storing index file failed", e);
} finally {
unlockFolder(cacheDir);
released = true;
@ -265,7 +268,7 @@ public final class SimpleCache implements Cache {
if (!cacheDir.exists()) {
// For some reason the cache directory doesn't exist. Make a best effort to create it.
cacheDir.mkdirs();
removeStaleSpansAndCachedContents();
removeStaleSpans();
}
evictor.onStartFile(this, key, position, maxLength);
return SimpleCacheSpan.getCacheFile(
@ -311,9 +314,9 @@ public final class SimpleCache implements Cache {
}
@Override
public synchronized void removeSpan(CacheSpan span) throws CacheException {
public synchronized void removeSpan(CacheSpan span) {
Assertions.checkState(!released);
removeSpan(span, true);
removeSpanInternal(span);
}
@Override
@ -379,7 +382,7 @@ public final class SimpleCache implements Cache {
if (span.isCached && !span.file.exists()) {
// The file has been deleted from under us. It's likely that other files will have been
// deleted too, so scan the whole in-memory representation.
removeStaleSpansAndCachedContents();
removeStaleSpans();
continue;
}
return span;
@ -431,27 +434,21 @@ public final class SimpleCache implements Cache {
notifySpanAdded(span);
}
private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException {
private void removeSpanInternal(CacheSpan span) {
CachedContent cachedContent = index.get(span.key);
if (cachedContent == null || !cachedContent.removeSpan(span)) {
return;
}
totalSpace -= span.length;
try {
if (removeEmptyCachedContent) {
index.maybeRemove(cachedContent.key);
index.store();
}
} finally {
notifySpanRemoved(span);
}
index.maybeRemove(cachedContent.key);
notifySpanRemoved(span);
}
/**
* Scans all of the cached spans in the in-memory representation, removing any for which files no
* longer exist.
*/
private void removeStaleSpansAndCachedContents() throws CacheException {
private void removeStaleSpans() {
ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>();
for (CachedContent cachedContent : index.getAll()) {
for (CacheSpan span : cachedContent.getSpans()) {
@ -461,11 +458,8 @@ public final class SimpleCache implements Cache {
}
}
for (int i = 0; i < spansToBeRemoved.size(); i++) {
// Remove span but not CachedContent to prevent multiple index.store() calls.
removeSpan(spansToBeRemoved.get(i), false);
removeSpanInternal(spansToBeRemoved.get(i));
}
index.removeEmpty();
index.store();
}
private void notifySpanRemoved(CacheSpan span) {

View file

@ -1436,11 +1436,12 @@ public final class Util {
}
/**
* Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C}
* {@code DEFAULT_*_BUFFER_SIZE} constant.
* Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C} {@code
* DEFAULT_*_BUFFER_SIZE} constant.
*
* @param trackType The track type.
* @return The corresponding default buffer size in bytes.
* @throws IllegalArgumentException If the track type is an unrecognized or custom track type.
*/
public static int getDefaultBufferSize(int trackType) {
switch (trackType) {
@ -1456,8 +1457,10 @@ public final class Util {
return C.DEFAULT_METADATA_BUFFER_SIZE;
case C.TRACK_TYPE_CAMERA_MOTION:
return C.DEFAULT_CAMERA_MOTION_BUFFER_SIZE;
case C.TRACK_TYPE_NONE:
return 0;
default:
throw new IllegalStateException();
throw new IllegalArgumentException();
}
}

View file

@ -98,7 +98,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private final EventDispatcher eventDispatcher;
private final long allowedJoiningTimeMs;
private final int maxDroppedFramesToNotify;
private final boolean deviceNeedsAutoFrcWorkaround;
private final boolean deviceNeedsNoPostProcessWorkaround;
private final long[] pendingOutputStreamOffsetsUs;
private final long[] pendingOutputStreamSwitchTimesUs;
@ -226,7 +226,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
this.context = context.getApplicationContext();
frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround();
deviceNeedsNoPostProcessWorkaround = deviceNeedsNoPostProcessWorkaround();
pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
outputStreamOffsetUs = C.TIME_UNSET;
@ -471,7 +471,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
format,
codecMaxValues,
codecOperatingRate,
deviceNeedsAutoFrcWorkaround,
deviceNeedsNoPostProcessWorkaround,
tunnelingAudioSessionId);
if (surface == null) {
Assertions.checkState(shouldUseDummySurface(codecInfo));
@ -1027,8 +1027,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* @param codecMaxValues Codec max values that should be used when configuring the decoder.
* @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if
* no codec operating rate should be set.
* @param deviceNeedsAutoFrcWorkaround Whether the device is known to enable frame-rate conversion
* logic that negatively impacts ExoPlayer.
* @param deviceNeedsNoPostProcessWorkaround Whether the device is known to do post processing by
* default that isn't compatible with ExoPlayer.
* @param tunnelingAudioSessionId The audio session id to use for tunneling, or {@link
* C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.
* @return The framework {@link MediaFormat} that should be used to configure the decoder.
@ -1038,7 +1038,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
Format format,
CodecMaxValues codecMaxValues,
float codecOperatingRate,
boolean deviceNeedsAutoFrcWorkaround,
boolean deviceNeedsNoPostProcessWorkaround,
int tunnelingAudioSessionId) {
MediaFormat mediaFormat = new MediaFormat();
// Set format parameters that should always be set.
@ -1062,7 +1062,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate);
}
}
if (deviceNeedsAutoFrcWorkaround) {
if (deviceNeedsNoPostProcessWorkaround) {
mediaFormat.setInteger("no-post-process", 1);
mediaFormat.setInteger("auto-frc", 0);
}
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
@ -1256,21 +1257,21 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
/**
* Returns whether the device is known to enable frame-rate conversion logic that negatively
* impacts ExoPlayer.
* <p>
* If true is returned then we explicitly disable the feature.
* Returns whether the device is known to do post processing by default that isn't compatible with
* ExoPlayer.
*
* @return True if the device is known to enable frame-rate conversion logic that negatively
* impacts ExoPlayer. False otherwise.
* @return Whether the device is known to do post processing by default that isn't compatible with
* ExoPlayer.
*/
private static boolean deviceNeedsAutoFrcWorkaround() {
// nVidia Shield prior to M tries to adjust the playback rate to better map the frame-rate of
private static boolean deviceNeedsNoPostProcessWorkaround() {
// Nvidia devices prior to M try to adjust the playback rate to better map the frame-rate of
// content to the refresh rate of the display. For example playback of 23.976fps content is
// adjusted to play at 1.001x speed when the output display is 60Hz. Unfortunately the
// implementation causes ExoPlayer's reported playback position to drift out of sync. Captions
// also lose sync [Internal: b/26453592].
return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER);
// also lose sync [Internal: b/26453592]. Even after M, the devices may apply post processing
// operations that can modify frame output timestamps, which is incompatible with ExoPlayer's
// logic for skipping decode-only frames.
return "NVIDIA".equals(Util.MANUFACTURER);
}
/*
@ -1296,163 +1297,171 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* incorrectly.
*/
protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) {
if (Util.SDK_INT >= 27 || name.startsWith("OMX.google")) {
// Devices running API level 27 or later should also be unaffected. Google OMX decoders are
// not known to have this issue on any API level.
if (name.startsWith("OMX.google")) {
// Google OMX decoders are not known to have this issue on any API level.
return false;
}
// Work around:
// https://github.com/google/ExoPlayer/issues/3236,
// https://github.com/google/ExoPlayer/issues/3355,
// https://github.com/google/ExoPlayer/issues/3439,
// https://github.com/google/ExoPlayer/issues/3724,
// https://github.com/google/ExoPlayer/issues/3835,
// https://github.com/google/ExoPlayer/issues/4006,
// https://github.com/google/ExoPlayer/issues/4084,
// https://github.com/google/ExoPlayer/issues/4104,
// https://github.com/google/ExoPlayer/issues/4134,
// https://github.com/google/ExoPlayer/issues/4315,
// https://github.com/google/ExoPlayer/issues/4419,
// https://github.com/google/ExoPlayer/issues/4460,
// https://github.com/google/ExoPlayer/issues/4468.
synchronized (MediaCodecVideoRenderer.class) {
if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) {
switch (Util.DEVICE) {
case "1601":
case "1713":
case "1714":
case "A10-70F":
case "A1601":
case "A2016a40":
case "A7000-a":
case "A7000plus":
case "A7010a48":
case "A7020a48":
case "AquaPowerM":
case "ASUS_X00AD_2":
case "Aura_Note_2":
case "BLACK-1X":
case "BRAVIA_ATV2":
case "C1":
case "ComioS1":
case "CP8676_I02":
case "CPH1609":
case "CPY83_I00":
case "cv1":
case "cv3":
case "deb":
case "E5643":
case "ELUGA_A3_Pro":
case "ELUGA_Note":
case "ELUGA_Prim":
case "ELUGA_Ray_X":
case "EverStar_S":
case "F3111":
case "F3113":
case "F3116":
case "F3211":
case "F3213":
case "F3215":
case "F3311":
case "flo":
case "GiONEE_CBL7513":
case "GiONEE_GBL7319":
case "GIONEE_GBL7360":
case "GIONEE_SWW1609":
case "GIONEE_SWW1627":
case "GIONEE_SWW1631":
case "GIONEE_WBL5708":
case "GIONEE_WBL7365":
case "GIONEE_WBL7519":
case "griffin":
case "htc_e56ml_dtul":
case "hwALE-H":
case "HWBLN-H":
case "HWCAM-H":
case "HWVNS-H":
case "i9031":
case "iball8735_9806":
case "Infinix-X572":
case "iris60":
case "itel_S41":
case "j2xlteins":
case "JGZ":
case "K50a40":
case "kate":
case "le_x6":
case "LS-5017":
case "M5c":
case "manning":
case "marino_f":
case "MEIZU_M5":
case "mh":
case "mido":
case "MX6":
case "namath":
case "nicklaus_f":
case "NX541J":
case "NX573J":
case "OnePlus5T":
case "p212":
case "P681":
case "P85":
case "panell_d":
case "panell_dl":
case "panell_ds":
case "panell_dt":
case "PB2-670M":
case "PGN528":
case "PGN610":
case "PGN611":
case "Phantom6":
case "Pixi4-7_3G":
case "Pixi5-10_4G":
case "PLE":
case "PRO7S":
case "Q350":
case "Q4260":
case "Q427":
case "Q4310":
case "Q5":
case "QM16XE_U":
case "QX1":
case "santoni":
case "Slate_Pro":
case "SVP-DTV15":
case "s905x018":
case "taido_row":
case "TB3-730F":
case "TB3-730X":
case "TB3-850F":
case "TB3-850M":
case "tcl_eu":
case "V1":
case "V23GB":
case "V5":
case "vernee_M5":
case "watson":
case "whyred":
case "woods_f":
case "woods_fn":
case "X3_HK":
case "XE2X":
case "XT1663":
case "Z12_PRO":
case "Z80":
deviceNeedsSetOutputSurfaceWorkaround = true;
break;
default:
// Do nothing.
break;
}
switch (Util.MODEL) {
case "AFTA":
case "AFTN":
deviceNeedsSetOutputSurfaceWorkaround = true;
break;
default:
// Do nothing.
break;
if (Util.SDK_INT <= 27 && "dangal".equals(Util.DEVICE)) {
// Dangal is affected on API level 27: https://github.com/google/ExoPlayer/issues/5169.
deviceNeedsSetOutputSurfaceWorkaround = true;
} else if (Util.SDK_INT >= 27) {
// In general, devices running API level 27 or later should be unaffected. Do nothing.
} else {
// Enable the workaround on a per-device basis. Works around:
// https://github.com/google/ExoPlayer/issues/3236,
// https://github.com/google/ExoPlayer/issues/3355,
// https://github.com/google/ExoPlayer/issues/3439,
// https://github.com/google/ExoPlayer/issues/3724,
// https://github.com/google/ExoPlayer/issues/3835,
// https://github.com/google/ExoPlayer/issues/4006,
// https://github.com/google/ExoPlayer/issues/4084,
// https://github.com/google/ExoPlayer/issues/4104,
// https://github.com/google/ExoPlayer/issues/4134,
// https://github.com/google/ExoPlayer/issues/4315,
// https://github.com/google/ExoPlayer/issues/4419,
// https://github.com/google/ExoPlayer/issues/4460,
// https://github.com/google/ExoPlayer/issues/4468.
switch (Util.DEVICE) {
case "1601":
case "1713":
case "1714":
case "A10-70F":
case "A1601":
case "A2016a40":
case "A7000-a":
case "A7000plus":
case "A7010a48":
case "A7020a48":
case "AquaPowerM":
case "ASUS_X00AD_2":
case "Aura_Note_2":
case "BLACK-1X":
case "BRAVIA_ATV2":
case "BRAVIA_ATV3_4K":
case "C1":
case "ComioS1":
case "CP8676_I02":
case "CPH1609":
case "CPY83_I00":
case "cv1":
case "cv3":
case "deb":
case "E5643":
case "ELUGA_A3_Pro":
case "ELUGA_Note":
case "ELUGA_Prim":
case "ELUGA_Ray_X":
case "EverStar_S":
case "F3111":
case "F3113":
case "F3116":
case "F3211":
case "F3213":
case "F3215":
case "F3311":
case "flo":
case "fugu":
case "GiONEE_CBL7513":
case "GiONEE_GBL7319":
case "GIONEE_GBL7360":
case "GIONEE_SWW1609":
case "GIONEE_SWW1627":
case "GIONEE_SWW1631":
case "GIONEE_WBL5708":
case "GIONEE_WBL7365":
case "GIONEE_WBL7519":
case "griffin":
case "htc_e56ml_dtul":
case "hwALE-H":
case "HWBLN-H":
case "HWCAM-H":
case "HWVNS-H":
case "i9031":
case "iball8735_9806":
case "Infinix-X572":
case "iris60":
case "itel_S41":
case "j2xlteins":
case "JGZ":
case "K50a40":
case "kate":
case "le_x6":
case "LS-5017":
case "M5c":
case "manning":
case "marino_f":
case "MEIZU_M5":
case "mh":
case "mido":
case "MX6":
case "namath":
case "nicklaus_f":
case "NX541J":
case "NX573J":
case "OnePlus5T":
case "p212":
case "P681":
case "P85":
case "panell_d":
case "panell_dl":
case "panell_ds":
case "panell_dt":
case "PB2-670M":
case "PGN528":
case "PGN610":
case "PGN611":
case "Phantom6":
case "Pixi4-7_3G":
case "Pixi5-10_4G":
case "PLE":
case "PRO7S":
case "Q350":
case "Q4260":
case "Q427":
case "Q4310":
case "Q5":
case "QM16XE_U":
case "QX1":
case "santoni":
case "Slate_Pro":
case "SVP-DTV15":
case "s905x018":
case "taido_row":
case "TB3-730F":
case "TB3-730X":
case "TB3-850F":
case "TB3-850M":
case "tcl_eu":
case "V1":
case "V23GB":
case "V5":
case "vernee_M5":
case "watson":
case "whyred":
case "woods_f":
case "woods_fn":
case "X3_HK":
case "XE2X":
case "XT1663":
case "Z12_PRO":
case "Z80":
deviceNeedsSetOutputSurfaceWorkaround = true;
break;
default:
// Do nothing.
break;
}
switch (Util.MODEL) {
case "AFTA":
case "AFTN":
deviceNeedsSetOutputSurfaceWorkaround = true;
break;
default:
// Do nothing.
break;
}
}
evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true;
}

View file

@ -26,7 +26,8 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.util.Assertions;
/**
* Listener of video {@link Renderer} events.
* Listener of video {@link Renderer} events. All methods have no-op default implementations to
* allow selective overrides.
*/
public interface VideoRendererEventListener {
@ -36,7 +37,7 @@ public interface VideoRendererEventListener {
* @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it
* remains enabled.
*/
void onVideoEnabled(DecoderCounters counters);
default void onVideoEnabled(DecoderCounters counters) {}
/**
* Called when a decoder is created.
@ -46,15 +47,15 @@ public interface VideoRendererEventListener {
* finished.
* @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
*/
void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs,
long initializationDurationMs);
default void onVideoDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) {}
/**
* Called when the format of the media being consumed by the renderer changes.
*
* @param format The new format.
*/
void onVideoInputFormatChanged(Format format);
default void onVideoInputFormatChanged(Format format) {}
/**
* Called to report the number of frames dropped by the renderer. Dropped frames are reported
@ -62,12 +63,11 @@ public interface VideoRendererEventListener {
* reaches a specified threshold whilst the renderer is started.
*
* @param count The number of dropped frames.
* @param elapsedMs The duration in milliseconds over which the frames were dropped. This
* duration is timed from when the renderer was started or from when dropped frames were
* last reported (whichever was more recent), and not from when the first of the reported
* drops occurred.
* @param elapsedMs The duration in milliseconds over which the frames were dropped. This duration
* is timed from when the renderer was started or from when dropped frames were last reported
* (whichever was more recent), and not from when the first of the reported drops occurred.
*/
void onDroppedFrames(int count, long elapsedMs);
default void onDroppedFrames(int count, long elapsedMs) {}
/**
* Called before a frame is rendered for the first time since setting the surface, and each time
@ -82,12 +82,12 @@ public interface VideoRendererEventListener {
* this is not possible. Applications that use {@link TextureView} can apply the rotation by
* calling {@link TextureView#setTransform}. Applications that do not expect to encounter
* rotated videos can safely ignore this parameter.
* @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case
* of square pixels this will be equal to 1.0. Different values are indicative of anamorphic
* @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case of
* square pixels this will be equal to 1.0. Different values are indicative of anamorphic
* content.
*/
void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio);
default void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {}
/**
* Called when a frame is rendered for the first time since setting the surface, and when a frame
@ -96,14 +96,14 @@ public interface VideoRendererEventListener {
* @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if
* the renderer renders to something that isn't a {@link Surface}.
*/
void onRenderedFirstFrame(@Nullable Surface surface);
default void onRenderedFirstFrame(@Nullable Surface surface) {}
/**
* Called when the renderer is disabled.
*
* @param counters {@link DecoderCounters} that were updated by the renderer.
*/
void onVideoDisabled(DecoderCounters counters);
default void onVideoDisabled(DecoderCounters counters) {}
/**
* Dispatches events to a {@link VideoRendererEventListener}.

View file

@ -1,6 +1,6 @@
seekMap:
isSeekable = true
duration = 26125
duration = 26122
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:

View file

@ -1,6 +1,6 @@
seekMap:
isSeekable = true
duration = 26125
duration = 26122
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:

View file

@ -1,6 +1,6 @@
seekMap:
isSeekable = true
duration = 26125
duration = 26122
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:

View file

@ -1,6 +1,6 @@
seekMap:
isSeekable = true
duration = 26125
duration = 26122
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 0:

View file

@ -0,0 +1,26 @@
<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>

View file

@ -0,0 +1,23 @@
<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>

View file

@ -0,0 +1,23 @@
<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>

View file

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.extractor.ts;
import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
@ -198,7 +200,7 @@ public class AdtsReaderTest {
private void maybeStartPacket() {
if (firstFeed) {
adtsReader.packetStarted(0, true);
adtsReader.packetStarted(0, FLAG_DATA_ALIGNMENT_INDICATOR);
firstFeed = false;
}
}

View file

@ -15,6 +15,7 @@
*/
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 java.util.Arrays.asList;
import static java.util.Collections.singletonList;
@ -55,7 +56,7 @@ public final class SectionReaderTest {
public void testSingleOnePacketSection() {
packetPayload[0] = 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));
}
@ -65,12 +66,12 @@ public final class SectionReaderTest {
insertTableSection(4, (byte) 100, 3); // This section header spreads across both packets.
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 5);
reader.consume(firstPacket, true);
reader.consume(firstPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEmpty();
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload);
secondPacket.setPosition(5);
reader.consume(secondPacket, false);
reader.consume(secondPacket, /* flags= */ 0);
assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(100));
}
@ -85,12 +86,12 @@ public final class SectionReaderTest {
insertTableSection(54, (byte) 105, 10);
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));
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload);
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));
}
@ -105,22 +106,22 @@ public final class SectionReaderTest {
insertTableSection(318, (byte) 108, 10);
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100);
reader.consume(firstPacket, true);
reader.consume(firstPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEmpty();
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200);
secondPacket.setPosition(100);
reader.consume(secondPacket, false);
reader.consume(secondPacket, /* flags= */ 0);
assertThat(payloadReader.parsedTableIds).isEmpty();
ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300);
thirdPacket.setPosition(200);
reader.consume(thirdPacket, false);
reader.consume(thirdPacket, /* flags= */ 0);
assertThat(payloadReader.parsedTableIds).isEmpty();
ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload);
fourthPacket.setPosition(300);
reader.consume(fourthPacket, true);
reader.consume(fourthPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEqualTo(asList(107, 108));
}
@ -135,24 +136,24 @@ public final class SectionReaderTest {
insertTableSection(318, (byte) 111, 10);
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100);
reader.consume(firstPacket, true);
reader.consume(firstPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEmpty();
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200);
secondPacket.setPosition(100);
reader.consume(secondPacket, false);
reader.consume(secondPacket, /* flags= */ 0);
assertThat(payloadReader.parsedTableIds).isEmpty();
ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300);
thirdPacket.setPosition(200);
reader.consume(thirdPacket, false);
reader.consume(thirdPacket, /* flags= */ 0);
assertThat(payloadReader.parsedTableIds).isEmpty();
reader.seek();
ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload);
fourthPacket.setPosition(300);
reader.consume(fourthPacket, true);
reader.consume(fourthPacket, FLAG_PAYLOAD_UNIT_START_INDICATOR);
assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(111));
}
@ -165,9 +166,9 @@ public final class SectionReaderTest {
byte[] incorrectCrcPat = Arrays.copyOf(correctCrcPat, correctCrcPat.length);
// Crc field is incorrect, and should not be passed to the payload reader.
incorrectCrcPat[16]--;
reader.consume(new ParsableByteArray(correctCrcPat), true);
reader.consume(new ParsableByteArray(correctCrcPat), FLAG_PAYLOAD_UNIT_START_INDICATOR);
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));
}

View file

@ -202,7 +202,7 @@ public final class TsExtractorTest {
}
@Override
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {}
public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) {}
@Override
public void consume(ParsableByteArray data) {}

View file

@ -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_EMPTY_TTML_FILE = "ttml/font_size_empty.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
public void testInlineAttributes() throws IOException, SubtitleDecoderException {
@ -259,56 +262,56 @@ public final class TtmlDecoderTest {
@Test
public void testMultipleRegions() throws IOException, SubtitleDecoderException {
TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE);
List<Cue> output = subtitle.getCues(1000000);
assertThat(output).hasSize(2);
Cue ttmlCue = output.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("lorem");
assertThat(ttmlCue.position).isEqualTo(10f / 100f);
assertThat(ttmlCue.line).isEqualTo(10f / 100f);
assertThat(ttmlCue.size).isEqualTo(20f / 100f);
List<Cue> cues = subtitle.getCues(1000000);
assertThat(cues).hasSize(2);
Cue cue = cues.get(0);
assertThat(cue.text.toString()).isEqualTo("lorem");
assertThat(cue.position).isEqualTo(10f / 100f);
assertThat(cue.line).isEqualTo(10f / 100f);
assertThat(cue.size).isEqualTo(20f / 100f);
ttmlCue = output.get(1);
assertThat(ttmlCue.text.toString()).isEqualTo("amet");
assertThat(ttmlCue.position).isEqualTo(60f / 100f);
assertThat(ttmlCue.line).isEqualTo(10f / 100f);
assertThat(ttmlCue.size).isEqualTo(20f / 100f);
cue = cues.get(1);
assertThat(cue.text.toString()).isEqualTo("amet");
assertThat(cue.position).isEqualTo(60f / 100f);
assertThat(cue.line).isEqualTo(10f / 100f);
assertThat(cue.size).isEqualTo(20f / 100f);
output = subtitle.getCues(5000000);
assertThat(output).hasSize(1);
ttmlCue = output.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("ipsum");
assertThat(ttmlCue.position).isEqualTo(40f / 100f);
assertThat(ttmlCue.line).isEqualTo(40f / 100f);
assertThat(ttmlCue.size).isEqualTo(20f / 100f);
cues = subtitle.getCues(5000000);
assertThat(cues).hasSize(1);
cue = cues.get(0);
assertThat(cue.text.toString()).isEqualTo("ipsum");
assertThat(cue.position).isEqualTo(40f / 100f);
assertThat(cue.line).isEqualTo(40f / 100f);
assertThat(cue.size).isEqualTo(20f / 100f);
output = subtitle.getCues(9000000);
assertThat(output).hasSize(1);
ttmlCue = output.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("dolor");
assertThat(ttmlCue.position).isEqualTo(Cue.DIMEN_UNSET);
assertThat(ttmlCue.line).isEqualTo(Cue.DIMEN_UNSET);
assertThat(ttmlCue.size).isEqualTo(Cue.DIMEN_UNSET);
cues = subtitle.getCues(9000000);
assertThat(cues).hasSize(1);
cue = cues.get(0);
assertThat(cue.text.toString()).isEqualTo("dolor");
assertThat(cue.position).isEqualTo(Cue.DIMEN_UNSET);
assertThat(cue.line).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.
// assertEquals(10f / 100f, ttmlCue.position);
// assertEquals(80f / 100f, ttmlCue.line);
// assertEquals(1f, ttmlCue.size);
// assertEquals(10f / 100f, cue.position);
// assertEquals(80f / 100f, cue.line);
// assertEquals(1f, cue.size);
output = subtitle.getCues(21000000);
assertThat(output).hasSize(1);
ttmlCue = output.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("She first said this");
assertThat(ttmlCue.position).isEqualTo(45f / 100f);
assertThat(ttmlCue.line).isEqualTo(45f / 100f);
assertThat(ttmlCue.size).isEqualTo(35f / 100f);
output = subtitle.getCues(25000000);
ttmlCue = output.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("She first said this\nThen this");
output = subtitle.getCues(29000000);
assertThat(output).hasSize(1);
ttmlCue = output.get(0);
assertThat(ttmlCue.text.toString()).isEqualTo("She first said this\nThen this\nFinally this");
assertThat(ttmlCue.position).isEqualTo(45f / 100f);
assertThat(ttmlCue.line).isEqualTo(45f / 100f);
cues = subtitle.getCues(21000000);
assertThat(cues).hasSize(1);
cue = cues.get(0);
assertThat(cue.text.toString()).isEqualTo("She first said this");
assertThat(cue.position).isEqualTo(45f / 100f);
assertThat(cue.line).isEqualTo(45f / 100f);
assertThat(cue.size).isEqualTo(35f / 100f);
cues = subtitle.getCues(25000000);
cue = cues.get(0);
assertThat(cue.text.toString()).isEqualTo("She first said this\nThen this");
cues = subtitle.getCues(29000000);
assertThat(cues).hasSize(1);
cue = cues.get(0);
assertThat(cue.text.toString()).isEqualTo("She first said this\nThen this\nFinally this");
assertThat(cue.position).isEqualTo(45f / 100f);
assertThat(cue.line).isEqualTo(45f / 100f);
}
@Test
@ -499,6 +502,91 @@ public final class TtmlDecoderTest {
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(
TtmlSubtitle subtitle,
int second,

View file

@ -489,11 +489,7 @@ public final class CacheDataSourceTest {
NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(expectedCacheKey);
for (CacheSpan cachedSpan : cachedSpans) {
if (cachedSpan.position >= halfDataLength) {
try {
cache.removeSpan(cachedSpan);
} catch (Cache.CacheException e) {
// do nothing
}
cache.removeSpan(cachedSpan);
}
}

View file

@ -47,6 +47,7 @@ import org.robolectric.RuntimeEnvironment;
public class SimpleCacheTest {
private static final String KEY_1 = "key1";
private static final String KEY_2 = "key2";
private File cacheDir;
@ -152,6 +153,40 @@ public class SimpleCacheTest {
assertCachedDataReadCorrect(cacheSpan2);
}
@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 {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key

View file

@ -607,6 +607,12 @@ public final class DashMediaSource extends BaseMediaSource {
// MediaSource implementation.
@Override
@Nullable
public Object getTag() {
return tag;
}
@Override
public void prepareSourceInternal(
ExoPlayer player,

View file

@ -390,6 +390,12 @@ public final class HlsMediaSource extends BaseMediaSource
this.tag = tag;
}
@Override
@Nullable
public Object getTag() {
return tag;
}
@Override
public void prepareSourceInternal(
ExoPlayer player,

View file

@ -503,6 +503,12 @@ public final class SsMediaSource extends BaseMediaSource
// MediaSource implementation.
@Override
@Nullable
public Object getTag() {
return tag;
}
@Override
public void prepareSourceInternal(
ExoPlayer player,

View file

@ -29,7 +29,6 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
@ -187,8 +186,9 @@ import java.util.List;
* <li>Type: {@link AspectRatioFrameLayout}
* </ul>
* <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
* when visible.
* view is typically an opaque view that covers the video surface, thereby obscuring it when
* visible. Obscuring the surface in this way also helps to prevent flicker at the start of
* playback when {@code surface_type="surface_view"}.
* <ul>
* <li>Type: {@link View}
* </ul>
@ -271,13 +271,13 @@ public class PlayerView extends FrameLayout {
private static final int SURFACE_TYPE_MONO360_VIEW = 3;
// LINT.ThenChange(../../../../../../res/values/attrs.xml)
private final AspectRatioFrameLayout contentFrame;
@Nullable private final AspectRatioFrameLayout contentFrame;
private final View shutterView;
private final View surfaceView;
@Nullable private final View surfaceView;
private final ImageView artworkView;
private final SubtitleView subtitleView;
private final @Nullable View bufferingView;
private final @Nullable TextView errorMessageView;
@Nullable private final View bufferingView;
@Nullable private final TextView errorMessageView;
private final PlayerControlView controller;
private final ComponentListener componentListener;
private final FrameLayout overlayFrameLayout;
@ -285,11 +285,11 @@ public class PlayerView extends FrameLayout {
private Player player;
private boolean useController;
private boolean useArtwork;
private @Nullable Drawable defaultArtwork;
@Nullable private Drawable defaultArtwork;
private @ShowBuffering int showBuffering;
private boolean keepContentOnPlayerReset;
private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
private @Nullable CharSequence customErrorMessage;
@Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
@Nullable private CharSequence customErrorMessage;
private int controllerShowTimeoutMs;
private boolean controllerAutoShow;
private boolean controllerHideDuringAds;
@ -474,9 +474,7 @@ public class PlayerView extends FrameLayout {
* @param newPlayerView The new view to attach to the player.
*/
public static void switchTargetView(
@NonNull Player player,
@Nullable PlayerView oldPlayerView,
@Nullable PlayerView newPlayerView) {
Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView) {
if (oldPlayerView == newPlayerView) {
return;
}
@ -1074,6 +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() {
if (!useController || player == null) {
return false;
@ -1187,9 +1205,8 @@ public class PlayerView extends FrameLayout {
int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight();
if (drawableWidth > 0 && drawableHeight > 0) {
if (contentFrame != null) {
contentFrame.setAspectRatio((float) drawableWidth / drawableHeight);
}
float artworkAspectRatio = (float) drawableWidth / drawableHeight;
onContentAspectRatioChanged(artworkAspectRatio, contentFrame, artworkView);
artworkView.setImageDrawable(drawable);
artworkView.setVisibility(VISIBLE);
return true;
@ -1322,9 +1339,6 @@ public class PlayerView extends FrameLayout {
@Override
public void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
if (contentFrame == null) {
return;
}
float videoAspectRatio =
(height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height;
@ -1345,11 +1359,9 @@ public class PlayerView extends FrameLayout {
surfaceView.addOnLayoutChangeListener(this);
}
applyTextureViewRotation((TextureView) surfaceView, textureViewRotation);
} else if (surfaceView instanceof SphericalSurfaceView) {
videoAspectRatio = 0;
}
contentFrame.setAspectRatio(videoAspectRatio);
onContentAspectRatioChanged(videoAspectRatio, contentFrame, surfaceView);
}
@Override

View file

@ -88,6 +88,13 @@ public class FakeMediaSource extends BaseMediaSource {
this.trackGroupArray = trackGroupArray;
}
@Override
@Nullable
public Object getTag() {
boolean hasTimeline = timeline != null && !timeline.isEmpty();
return hasTimeline ? timeline.getWindow(0, new Timeline.Window()).tag : null;
}
@Override
public synchronized void prepareSourceInternal(
ExoPlayer player,

View file

@ -49,6 +49,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
throw new UnsupportedOperationException();
}
@Override
public MetadataComponent getMetadataComponent() {
throw new UnsupportedOperationException();
}
@Override
public Looper getPlaybackLooper() {
throw new UnsupportedOperationException();