mirror of
https://github.com/samsonjs/media.git
synced 2026-04-10 12:05:47 +00:00
commit
71f72c5953
68 changed files with 1183 additions and 475 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 26125
|
||||
duration = 26122
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 26125
|
||||
duration = 26122
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 26125
|
||||
duration = 26122
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
seekMap:
|
||||
isSeekable = true
|
||||
duration = 26125
|
||||
duration = 26122
|
||||
getPosition(0) = [[timeUs=0, position=0]]
|
||||
numberOfTracks = 1
|
||||
track 0:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
23
library/core/src/test/assets/ttml/bitmap_pixel_region.xml
Normal file
23
library/core/src/test/assets/ttml/bitmap_pixel_region.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue