Merge pull request #6150 from google/dev-v2-r2.10.3

r2.10.3
This commit is contained in:
Oliver Woodman 2019-07-14 18:01:01 +01:00 committed by GitHub
commit 51acd8150c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 422 additions and 246 deletions

View file

@ -1,5 +1,24 @@
# Release notes #
### 2.10.3 ###
* Display last frame when seeking to end of stream
([#2568](https://github.com/google/ExoPlayer/issues/2568)).
* Audio:
* Fix an issue where not all audio was played out when the configuration
for the underlying track was changing (e.g., at some period transitions).
* Fix an issue where playback speed was applied inaccurately in playlists
([#6117](https://github.com/google/ExoPlayer/issues/6117)).
* UI: Fix `PlayerView` incorrectly consuming touch events if no controller is
attached ([#6109](https://github.com/google/ExoPlayer/issues/6109)).
* CEA608: Fix repetition of special North American characters
([#6133](https://github.com/google/ExoPlayer/issues/6133)).
* FLV: Fix bug that caused playback of some live streams to not start
([#6111](https://github.com/google/ExoPlayer/issues/6111)).
* SmoothStreaming: Parse text stream `Subtype` into `Format.roleFlags`.
* MediaSession extension: Fix `MediaSessionConnector.play()` not resuming
playback ([#6093](https://github.com/google/ExoPlayer/issues/6093)).
### 2.10.2 ###
* Add `ResolvingDataSource` for just-in-time resolution of `DataSpec`s

View file

@ -44,6 +44,7 @@ allprojects {
}
buildDir = "${externalBuildDir}/${project.name}"
}
group = 'com.google.android.exoplayer'
}
apply from: 'javadoc_combined.gradle'

View file

@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.10.2'
releaseVersionCode = 2010002
releaseVersion = '2.10.3'
releaseVersionCode = 2010003
minSdkVersion = 16
targetSdkVersion = 28
compileSdkVersion = 28

View file

@ -306,7 +306,7 @@ public final class TrackSelectionDialog extends DialogFragment {
}
}
/** Fragment to show a track seleciton in tab of the track selection dialog. */
/** Fragment to show a track selection in tab of the track selection dialog. */
public static final class TrackSelectionViewFragment extends Fragment
implements TrackSelectionView.TrackSelectionListener {

View file

@ -377,6 +377,13 @@ public final class MediaSessionConnector {
/**
* Gets the {@link MediaMetadataCompat} to be published to the session.
*
* <p>An app may need to load metadata resources like artwork bitmaps asynchronously. In such a
* case the app should return a {@link MediaMetadataCompat} object that does not contain these
* resources as a placeholder. The app should start an asynchronous operation to download the
* bitmap and put it into a cache. Finally, the app should call {@link
* #invalidateMediaSessionMetadata()}. This causes this callback to be called again and the app
* can now return a {@link MediaMetadataCompat} object with all the resources included.
*
* @param player The player connected to the media session.
* @return The {@link MediaMetadataCompat} to be published to the session.
*/
@ -1066,8 +1073,9 @@ public final class MediaSessionConnector {
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
controlDispatcher.dispatchSetPlayWhenReady(player, /* playWhenReady= */ true);
}
controlDispatcher.dispatchSetPlayWhenReady(
Assertions.checkNotNull(player), /* playWhenReady= */ true);
}
}

View file

@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.10.2";
public static final String VERSION = "2.10.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.10.2";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.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 = 2010002;
public static final int VERSION_INT = 2010003;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View file

@ -1231,8 +1231,7 @@ public class SimpleExoPlayer extends BasePlayer
Log.w(
TAG,
"Player is accessed on the wrong thread. See "
+ "https://exoplayer.dev/troubleshooting.html#"
+ "what-do-player-is-accessed-on-the-wrong-thread-warnings-mean",
+ "https://exoplayer.dev/issues/player-accessed-on-wrong-thread",
hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException());
hasNotifiedFullWrongThreadWarning = true;
}

View file

@ -272,6 +272,7 @@ public final class DefaultAudioSink implements AudioSink {
private int preV21OutputBufferOffset;
private int drainingAudioProcessorIndex;
private boolean handledEndOfStream;
private boolean stoppedAudioTrack;
private boolean playing;
private int audioSessionId;
@ -465,19 +466,15 @@ public final class DefaultAudioSink implements AudioSink {
processingEnabled,
canApplyPlaybackParameters,
availableAudioProcessors);
if (isInitialized()) {
if (!pendingConfiguration.canReuseAudioTrack(configuration)) {
// We need a new AudioTrack before we can handle more input. We should first stop() the
// track and wait for audio to play out (tracked by [Internal: b/33161961]), but for now we
// discard the audio track immediately.
flush();
} else if (flushAudioProcessors) {
// We don't need a new AudioTrack but audio processors need to be drained and flushed.
this.pendingConfiguration = pendingConfiguration;
return;
}
// If we have a pending configuration already, we always drain audio processors as the preceding
// configuration may have required it (even if this one doesn't).
boolean drainAudioProcessors = flushAudioProcessors || this.pendingConfiguration != null;
if (isInitialized()
&& (!pendingConfiguration.canReuseAudioTrack(configuration) || drainAudioProcessors)) {
this.pendingConfiguration = pendingConfiguration;
} else {
configuration = pendingConfiguration;
}
configuration = pendingConfiguration;
}
private void setupAudioProcessors() {
@ -504,7 +501,7 @@ public final class DefaultAudioSink implements AudioSink {
}
}
private void initialize() throws InitializationException {
private void initialize(long presentationTimeUs) throws InitializationException {
// If we're asynchronously releasing a previous audio track then we block until it has been
// released. This guarantees that we cannot end up in a state where we have multiple audio
// track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
@ -536,11 +533,7 @@ public final class DefaultAudioSink implements AudioSink {
}
}
playbackParameters =
configuration.canApplyPlaybackParameters
? audioProcessorChain.applyPlaybackParameters(playbackParameters)
: PlaybackParameters.DEFAULT;
setupAudioProcessors();
applyPlaybackParameters(playbackParameters, presentationTimeUs);
audioTrackPositionTracker.setAudioTrack(
audioTrack,
@ -579,21 +572,27 @@ public final class DefaultAudioSink implements AudioSink {
Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer);
if (pendingConfiguration != null) {
// We are waiting for audio processors to drain before applying a the new configuration.
if (!drainAudioProcessorsToEndOfStream()) {
// There's still pending data in audio processors to write to the track.
return false;
} else if (!pendingConfiguration.canReuseAudioTrack(configuration)) {
playPendingData();
if (hasPendingData()) {
// We're waiting for playout on the current audio track to finish.
return false;
}
flush();
} else {
// The current audio track can be reused for the new configuration.
configuration = pendingConfiguration;
pendingConfiguration = null;
}
configuration = pendingConfiguration;
pendingConfiguration = null;
playbackParameters =
configuration.canApplyPlaybackParameters
? audioProcessorChain.applyPlaybackParameters(playbackParameters)
: PlaybackParameters.DEFAULT;
setupAudioProcessors();
// Re-apply playback parameters.
applyPlaybackParameters(playbackParameters, presentationTimeUs);
}
if (!isInitialized()) {
initialize();
initialize(presentationTimeUs);
if (playing) {
play();
}
@ -629,15 +628,7 @@ public final class DefaultAudioSink implements AudioSink {
}
PlaybackParameters newPlaybackParameters = afterDrainPlaybackParameters;
afterDrainPlaybackParameters = null;
newPlaybackParameters = audioProcessorChain.applyPlaybackParameters(newPlaybackParameters);
// Store the position and corresponding media time from which the parameters will apply.
playbackParametersCheckpoints.add(
new PlaybackParametersCheckpoint(
newPlaybackParameters,
Math.max(0, presentationTimeUs),
configuration.framesToDurationUs(getWrittenFrames())));
// Update the set of active audio processors to take into account the new parameters.
setupAudioProcessors();
applyPlaybackParameters(newPlaybackParameters, presentationTimeUs);
}
if (startMediaTimeState == START_NOT_SET) {
@ -786,15 +777,8 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public void playToEndOfStream() throws WriteException {
if (handledEndOfStream || !isInitialized()) {
return;
}
if (drainAudioProcessorsToEndOfStream()) {
// The audio processors have drained, so drain the underlying audio track.
audioTrackPositionTracker.handleEndOfStream(getWrittenFrames());
audioTrack.stop();
bytesUntilNextAvSync = 0;
if (!handledEndOfStream && isInitialized() && drainAudioProcessorsToEndOfStream()) {
playPendingData();
handledEndOfStream = true;
}
}
@ -858,8 +842,9 @@ public final class DefaultAudioSink implements AudioSink {
// parameters apply.
afterDrainPlaybackParameters = playbackParameters;
} else {
// Update the playback parameters now.
this.playbackParameters = audioProcessorChain.applyPlaybackParameters(playbackParameters);
// Update the playback parameters now. They will be applied to the audio processors during
// initialization.
this.playbackParameters = playbackParameters;
}
}
return this.playbackParameters;
@ -976,6 +961,7 @@ public final class DefaultAudioSink implements AudioSink {
flushAudioProcessors();
inputBuffer = null;
outputBuffer = null;
stoppedAudioTrack = false;
handledEndOfStream = false;
drainingAudioProcessorIndex = C.INDEX_UNSET;
avSyncHeader = null;
@ -1040,6 +1026,21 @@ public final class DefaultAudioSink implements AudioSink {
}.start();
}
private void applyPlaybackParameters(
PlaybackParameters playbackParameters, long presentationTimeUs) {
PlaybackParameters newPlaybackParameters =
configuration.canApplyPlaybackParameters
? audioProcessorChain.applyPlaybackParameters(playbackParameters)
: PlaybackParameters.DEFAULT;
// Store the position and corresponding media time from which the parameters will apply.
playbackParametersCheckpoints.add(
new PlaybackParametersCheckpoint(
newPlaybackParameters,
/* mediaTimeUs= */ Math.max(0, presentationTimeUs),
/* positionUs= */ configuration.framesToDurationUs(getWrittenFrames())));
setupAudioProcessors();
}
private long applySpeedup(long positionUs) {
@Nullable PlaybackParametersCheckpoint checkpoint = null;
while (!playbackParametersCheckpoints.isEmpty()
@ -1223,6 +1224,15 @@ public final class DefaultAudioSink implements AudioSink {
audioTrack.setStereoVolume(volume, volume);
}
private void playPendingData() {
if (!stoppedAudioTrack) {
stoppedAudioTrack = true;
audioTrackPositionTracker.handleEndOfStream(getWrittenFrames());
audioTrack.stop();
bytesUntilNextAvSync = 0;
}
}
/** Stores playback parameters with the position and media time at which they apply. */
private static final class PlaybackParametersCheckpoint {

View file

@ -695,7 +695,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
int bufferIndex,
int bufferFlags,
long bufferPresentationTimeUs,
boolean shouldSkip,
boolean isDecodeOnlyBuffer,
boolean isLastBuffer,
Format format)
throws ExoPlaybackException {
if (codecNeedsEosBufferTimestampWorkaround
@ -711,7 +712,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return true;
}
if (shouldSkip) {
if (isDecodeOnlyBuffer) {
codec.releaseOutputBuffer(bufferIndex, false);
decoderCounters.skippedOutputBufferCount++;
audioSink.handleDiscontinuity();

View file

@ -544,7 +544,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
@Override
public void onEvent(
ExoMediaDrm<? extends T> md,
byte[] sessionId,
@Nullable byte[] sessionId,
int event,
int extra,
@Nullable byte[] data) {

View file

@ -80,7 +80,7 @@ public interface ExoMediaDrm<T extends ExoMediaCrypto> {
*/
void onEvent(
ExoMediaDrm<? extends T> mediaDrm,
byte[] sessionId,
@Nullable byte[] sessionId,
int event,
int extra,
@Nullable byte[] data);
@ -215,6 +215,7 @@ public interface ExoMediaDrm<T extends ExoMediaCrypto> {
throws NotProvisionedException;
/** @see MediaDrm#provideKeyResponse(byte[], byte[]) */
@Nullable
byte[] provideKeyResponse(byte[] scope, byte[] response)
throws NotProvisionedException, DeniedByServerException;

View file

@ -84,8 +84,6 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
}
}
// FIXME: incompatible types in argument.
@SuppressWarnings("nullness:argument.type.incompatible")
@Override
public void setOnEventListener(
final ExoMediaDrm.OnEventListener<? super FrameworkMediaCrypto> listener) {
@ -160,8 +158,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
return new KeyRequest(requestData, licenseServerUrl);
}
// FIXME: incompatible types in return.
@SuppressWarnings("nullness:return.type.incompatible")
@Nullable
@Override
public byte[] provideKeyResponse(byte[] scope, byte[] response)
throws NotProvisionedException, DeniedByServerException {

View file

@ -86,11 +86,12 @@ import java.util.Collections;
}
@Override
protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
if (audioFormat == AUDIO_FORMAT_MP3) {
int sampleSize = data.bytesLeft();
output.sampleData(data, sampleSize);
output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
return true;
} else {
int packetType = data.readUnsignedByte();
if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {
@ -104,12 +105,15 @@ import java.util.Collections;
Collections.singletonList(audioSpecificConfig), null, 0, null);
output.format(format);
hasOutputFormat = true;
return false;
} else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) {
int sampleSize = data.bytesLeft();
output.sampleData(data, sampleSize);
output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
return true;
} else {
return false;
}
}
}
}

View file

@ -74,6 +74,7 @@ public final class FlvExtractor implements Extractor {
private ExtractorOutput extractorOutput;
private @States int state;
private boolean outputFirstSample;
private long mediaTagTimestampOffsetUs;
private int bytesToNextTagHeader;
private int tagType;
@ -90,7 +91,6 @@ public final class FlvExtractor implements Extractor {
tagData = new ParsableByteArray();
metadataReader = new ScriptTagPayloadReader();
state = STATE_READING_FLV_HEADER;
mediaTagTimestampOffsetUs = C.TIME_UNSET;
}
@Override
@ -132,7 +132,7 @@ public final class FlvExtractor implements Extractor {
@Override
public void seek(long position, long timeUs) {
state = STATE_READING_FLV_HEADER;
mediaTagTimestampOffsetUs = C.TIME_UNSET;
outputFirstSample = false;
bytesToNextTagHeader = 0;
}
@ -253,14 +253,16 @@ public final class FlvExtractor implements Extractor {
*/
private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException {
boolean wasConsumed = true;
boolean wasSampleOutput = false;
long timestampUs = getCurrentTimestampUs();
if (tagType == TAG_TYPE_AUDIO && audioReader != null) {
ensureReadyForMediaOutput();
audioReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs);
wasSampleOutput = audioReader.consume(prepareTagData(input), timestampUs);
} else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {
ensureReadyForMediaOutput();
videoReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs);
wasSampleOutput = videoReader.consume(prepareTagData(input), timestampUs);
} else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) {
metadataReader.consume(prepareTagData(input), tagTimestampUs);
wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs);
long durationUs = metadataReader.getDurationUs();
if (durationUs != C.TIME_UNSET) {
extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
@ -270,6 +272,11 @@ public final class FlvExtractor implements Extractor {
input.skipFully(tagDataSize);
wasConsumed = false;
}
if (!outputFirstSample && wasSampleOutput) {
outputFirstSample = true;
mediaTagTimestampOffsetUs =
metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0;
}
bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header.
state = STATE_SKIPPING_TO_TAG_HEADER;
return wasConsumed;
@ -292,10 +299,11 @@ public final class FlvExtractor implements Extractor {
extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
outputSeekMap = true;
}
if (mediaTagTimestampOffsetUs == C.TIME_UNSET) {
mediaTagTimestampOffsetUs =
metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0;
}
}
private long getCurrentTimestampUs() {
return outputFirstSample
? (mediaTagTimestampOffsetUs + tagTimestampUs)
: (metadataReader.getDurationUs() == C.TIME_UNSET ? 0 : tagTimestampUs);
}
}

View file

@ -63,7 +63,7 @@ import java.util.Map;
}
@Override
protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
int nameType = readAmfType(data);
if (nameType != AMF_TYPE_STRING) {
// Should never happen.
@ -72,12 +72,12 @@ import java.util.Map;
String name = readAmfString(data);
if (!NAME_METADATA.equals(name)) {
// We're only interested in metadata.
return;
return false;
}
int type = readAmfType(data);
if (type != AMF_TYPE_ECMA_ARRAY) {
// We're not interested in this metadata.
return;
return false;
}
// Set the duration to the value contained in the metadata, if present.
Map<String, Object> metadata = readAmfEcmaArray(data);
@ -87,6 +87,7 @@ import java.util.Map;
durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND);
}
}
return false;
}
private static int readAmfType(ParsableByteArray data) {

View file

@ -58,12 +58,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
*
* @param data The payload data to consume.
* @param timeUs The timestamp associated with the payload.
* @return Whether a sample was output.
* @throws ParserException If an error occurs parsing the data.
*/
public final void consume(ParsableByteArray data, long timeUs) throws ParserException {
if (parseHeader(data)) {
parsePayload(data, timeUs);
}
public final boolean consume(ParsableByteArray data, long timeUs) throws ParserException {
return parseHeader(data) && parsePayload(data, timeUs);
}
/**
@ -78,10 +77,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
/**
* Parses tag payload.
*
* @param data Buffer where tag payload is stored
* @param timeUs Time position of the frame
* @param data Buffer where tag payload is stored.
* @param timeUs Time position of the frame.
* @return Whether a sample was output.
* @throws ParserException If an error occurs parsing the payload.
*/
protected abstract void parsePayload(ParsableByteArray data, long timeUs) throws ParserException;
protected abstract boolean parsePayload(ParsableByteArray data, long timeUs)
throws ParserException;
}

View file

@ -47,6 +47,7 @@ import com.google.android.exoplayer2.video.AvcConfig;
// State variables.
private boolean hasOutputFormat;
private boolean hasOutputKeyframe;
private int frameType;
/**
@ -60,7 +61,7 @@ import com.google.android.exoplayer2.video.AvcConfig;
@Override
public void seek() {
// Do nothing.
hasOutputKeyframe = false;
}
@Override
@ -77,7 +78,7 @@ import com.google.android.exoplayer2.video.AvcConfig;
}
@Override
protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
protected boolean parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
int packetType = data.readUnsignedByte();
int compositionTimeMs = data.readInt24();
@ -94,7 +95,12 @@ import com.google.android.exoplayer2.video.AvcConfig;
avcConfig.initializationData, Format.NO_VALUE, avcConfig.pixelWidthAspectRatio, null);
output.format(format);
hasOutputFormat = true;
return false;
} else if (packetType == AVC_PACKET_TYPE_AVC_NALU && hasOutputFormat) {
boolean isKeyframe = frameType == VIDEO_FRAME_KEYFRAME;
if (!hasOutputKeyframe && !isKeyframe) {
return false;
}
// TODO: Deduplicate with Mp4Extractor.
// Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case
// they're only 1 or 2 bytes long.
@ -123,8 +129,12 @@ import com.google.android.exoplayer2.video.AvcConfig;
output.sampleData(data, bytesToWrite);
bytesWritten += bytesToWrite;
}
output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.BUFFER_FLAG_KEY_FRAME : 0,
bytesWritten, 0, null);
output.sampleMetadata(
timeUs, isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0, bytesWritten, 0, null);
hasOutputKeyframe = true;
return true;
} else {
return false;
}
}

View file

@ -518,9 +518,15 @@ public final class MediaCodecInfo {
@TargetApi(21)
private static boolean areSizeAndRateSupportedV21(VideoCapabilities capabilities, int width,
int height, double frameRate) {
return frameRate == Format.NO_VALUE || frameRate <= 0
? capabilities.isSizeSupported(width, height)
: capabilities.areSizeAndRateSupported(width, height, frameRate);
if (frameRate == Format.NO_VALUE || frameRate <= 0) {
return capabilities.isSizeSupported(width, height);
} else {
// The signaled frame rate may be slightly higher than the actual frame rate, so we take the
// floor to avoid situations where a range check in areSizeAndRateSupported fails due to
// slightly exceeding the limits for a standard format (e.g., 1080p at 30 fps).
double floorFrameRate = Math.floor(frameRate);
return capabilities.areSizeAndRateSupported(width, height, floorFrameRate);
}
}
@TargetApi(23)

View file

@ -328,14 +328,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private int inputIndex;
private int outputIndex;
private ByteBuffer outputBuffer;
private boolean shouldSkipOutputBuffer;
private boolean isDecodeOnlyOutputBuffer;
private boolean isLastOutputBuffer;
private boolean codecReconfigured;
@ReconfigurationState private int codecReconfigurationState;
@DrainState private int codecDrainState;
@DrainAction private int codecDrainAction;
private boolean codecReceivedBuffers;
private boolean codecReceivedEos;
private long lastBufferInStreamPresentationTimeUs;
private long largestQueuedPresentationTimeUs;
private boolean inputStreamEnded;
private boolean outputStreamEnded;
private boolean waitingForKeys;
@ -598,6 +600,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
waitingForKeys = false;
codecHotswapDeadlineMs = C.TIME_UNSET;
decodeOnlyPresentationTimestamps.clear();
largestQueuedPresentationTimeUs = C.TIME_UNSET;
lastBufferInStreamPresentationTimeUs = C.TIME_UNSET;
try {
if (codec != null) {
decoderCounters.decoderReleaseCount++;
@ -704,10 +708,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
waitingForFirstSyncSample = true;
codecNeedsAdaptationWorkaroundBuffer = false;
shouldSkipAdaptationWorkaroundOutputBuffer = false;
shouldSkipOutputBuffer = false;
isDecodeOnlyOutputBuffer = false;
isLastOutputBuffer = false;
waitingForKeys = false;
decodeOnlyPresentationTimestamps.clear();
largestQueuedPresentationTimeUs = C.TIME_UNSET;
lastBufferInStreamPresentationTimeUs = C.TIME_UNSET;
codecDrainState = DRAIN_STATE_NONE;
codecDrainAction = DRAIN_ACTION_NONE;
// Reconfiguration data sent shortly before the flush may not have been processed by the
@ -881,7 +888,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecDrainAction = DRAIN_ACTION_NONE;
codecNeedsAdaptationWorkaroundBuffer = false;
shouldSkipAdaptationWorkaroundOutputBuffer = false;
shouldSkipOutputBuffer = false;
isDecodeOnlyOutputBuffer = false;
isLastOutputBuffer = false;
waitingForFirstSyncSample = true;
decoderCounters.decoderInitCount++;
@ -1016,6 +1024,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
result = readSource(formatHolder, buffer, false);
}
if (hasReadStreamToEnd()) {
// Notify output queue of the last buffer's timestamp.
lastBufferInStreamPresentationTimeUs = largestQueuedPresentationTimeUs;
}
if (result == C.RESULT_NOTHING_READ) {
return false;
}
@ -1088,6 +1101,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
formatQueue.add(presentationTimeUs, inputFormat);
waitingForFirstSampleInFormat = false;
}
largestQueuedPresentationTimeUs =
Math.max(largestQueuedPresentationTimeUs, presentationTimeUs);
buffer.flip();
onQueueInputBuffer(buffer);
@ -1458,7 +1473,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
outputBuffer.position(outputBufferInfo.offset);
outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size);
}
shouldSkipOutputBuffer = shouldSkipOutputBuffer(outputBufferInfo.presentationTimeUs);
isDecodeOnlyOutputBuffer = isDecodeOnlyBuffer(outputBufferInfo.presentationTimeUs);
isLastOutputBuffer =
lastBufferInStreamPresentationTimeUs == outputBufferInfo.presentationTimeUs;
updateOutputFormatForTime(outputBufferInfo.presentationTimeUs);
}
@ -1474,7 +1491,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
outputIndex,
outputBufferInfo.flags,
outputBufferInfo.presentationTimeUs,
shouldSkipOutputBuffer,
isDecodeOnlyOutputBuffer,
isLastOutputBuffer,
outputFormat);
} catch (IllegalStateException e) {
processEndOfStream();
@ -1494,7 +1512,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
outputIndex,
outputBufferInfo.flags,
outputBufferInfo.presentationTimeUs,
shouldSkipOutputBuffer,
isDecodeOnlyOutputBuffer,
isLastOutputBuffer,
outputFormat);
}
@ -1561,7 +1580,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* @param bufferIndex The index of the output buffer.
* @param bufferFlags The flags attached to the output buffer.
* @param bufferPresentationTimeUs The presentation time of the output buffer in microseconds.
* @param shouldSkip Whether the buffer should be skipped (i.e. not rendered).
* @param isDecodeOnlyBuffer Whether the buffer was marked with {@link C#BUFFER_FLAG_DECODE_ONLY}
* by the source.
* @param isLastBuffer Whether the buffer is the last sample of the current stream.
* @param format The format associated with the buffer.
* @return Whether the output buffer was fully processed (e.g. rendered or skipped).
* @throws ExoPlaybackException If an error occurs processing the output buffer.
@ -1574,7 +1595,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
int bufferIndex,
int bufferFlags,
long bufferPresentationTimeUs,
boolean shouldSkip,
boolean isDecodeOnlyBuffer,
boolean isLastBuffer,
Format format)
throws ExoPlaybackException;
@ -1654,7 +1676,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecDrainAction = DRAIN_ACTION_NONE;
}
private boolean shouldSkipOutputBuffer(long presentationTimeUs) {
private boolean isDecodeOnlyBuffer(long presentationTimeUs) {
// We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would
// box presentationTimeUs, creating a Long object that would need to be garbage collected.
int size = decodeOnlyPresentationTimestamps.size();

View file

@ -817,10 +817,10 @@ public final class DownloadHelper {
private final MediaSource mediaSource;
private final DownloadHelper downloadHelper;
private final Allocator allocator;
private final ArrayList<MediaPeriod> pendingMediaPeriods;
private final Handler downloadHelperHandler;
private final HandlerThread mediaSourceThread;
private final Handler mediaSourceHandler;
private final Handler downloadHelperHandler;
private final ArrayList<MediaPeriod> pendingMediaPeriods;
@Nullable public Object manifest;
public @MonotonicNonNull Timeline timeline;
@ -832,6 +832,7 @@ public final class DownloadHelper {
this.mediaSource = mediaSource;
this.downloadHelper = downloadHelper;
allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
pendingMediaPeriods = new ArrayList<>();
@SuppressWarnings("methodref.receiver.bound.invalid")
Handler downloadThreadHandler = Util.createHandler(this::handleDownloadHelperCallbackMessage);
this.downloadHelperHandler = downloadThreadHandler;
@ -839,7 +840,6 @@ public final class DownloadHelper {
mediaSourceThread.start();
mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this);
mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE);
pendingMediaPeriods = new ArrayList<>();
}
public void release() {

View file

@ -25,6 +25,7 @@ import android.content.Context;
import android.content.Intent;
import android.os.PersistableBundle;
import androidx.annotation.RequiresPermission;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
@ -129,9 +130,8 @@ public final class PlatformScheduler implements Scheduler {
logd("Requirements are met");
String serviceAction = extras.getString(KEY_SERVICE_ACTION);
String servicePackage = extras.getString(KEY_SERVICE_PACKAGE);
// FIXME: incompatible types in argument.
@SuppressWarnings("nullness:argument.type.incompatible")
Intent intent = new Intent(serviceAction).setPackage(servicePackage);
Intent intent =
new Intent(Assertions.checkNotNull(serviceAction)).setPackage(servicePackage);
logd("Starting service action: " + serviceAction + " package: " + servicePackage);
Util.startForegroundService(this, intent);
} else {

View file

@ -733,7 +733,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
if (prepared) {
SeekMap seekMap = getPreparedState().seekMap;
Assertions.checkState(isPendingReset());
if (durationUs != C.TIME_UNSET && pendingResetPositionUs >= durationUs) {
if (durationUs != C.TIME_UNSET && pendingResetPositionUs > durationUs) {
loadingFinished = true;
pendingResetPositionUs = C.TIME_UNSET;
return;

View file

@ -242,7 +242,7 @@ public final class Cea608Decoder extends CeaDecoder {
private int captionMode;
private int captionRowCount;
private boolean captionValid;
private boolean isCaptionValid;
private boolean repeatableControlSet;
private byte repeatableControlCc1;
private byte repeatableControlCc2;
@ -300,7 +300,7 @@ public final class Cea608Decoder extends CeaDecoder {
setCaptionMode(CC_MODE_UNKNOWN);
setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT);
resetCueBuilders();
captionValid = false;
isCaptionValid = false;
repeatableControlSet = false;
repeatableControlCc1 = 0;
repeatableControlCc2 = 0;
@ -358,13 +358,19 @@ public final class Cea608Decoder extends CeaDecoder {
continue;
}
boolean repeatedControlPossible = repeatableControlSet;
repeatableControlSet = false;
boolean previousIsCaptionValid = isCaptionValid;
isCaptionValid =
(ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG
&& ODD_PARITY_BYTE_TABLE[ccByte1]
&& ODD_PARITY_BYTE_TABLE[ccByte2];
boolean previousCaptionValid = captionValid;
captionValid = (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG;
if (!captionValid) {
if (previousCaptionValid) {
if (isRepeatedCommand(isCaptionValid, ccData1, ccData2)) {
// Ignore repeated valid commands.
continue;
}
if (!isCaptionValid) {
if (previousIsCaptionValid) {
// The encoder has flipped the validity bit to indicate captions are being turned off.
resetCueBuilders();
captionDataProcessed = true;
@ -372,65 +378,41 @@ public final class Cea608Decoder extends CeaDecoder {
continue;
}
// If we've reached this point then there is data to process; flag that work has been done.
captionDataProcessed = true;
if (!ODD_PARITY_BYTE_TABLE[ccByte1] || !ODD_PARITY_BYTE_TABLE[ccByte2]) {
// The data is invalid.
resetCueBuilders();
continue;
}
maybeUpdateIsInCaptionService(ccData1, ccData2);
if (!isInCaptionService) {
// Only the Captioning service is supported. Drop all other bytes.
continue;
}
// Special North American character set.
// ccData1 - 0|0|0|1|C|0|0|1
// ccData2 - 0|0|1|1|X|X|X|X
if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) {
if (getChannel(ccData1) == selectedChannel) {
currentCueBuilder.append(getSpecialChar(ccData2));
}
if (!updateAndVerifyCurrentChannel(ccData1)) {
// Wrong channel.
continue;
}
// Extended Western European character set.
// ccData1 - 0|0|0|1|C|0|1|S
// ccData2 - 0|0|1|X|X|X|X|X
if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) {
if (getChannel(ccData1) == selectedChannel) {
// Remove standard equivalent of the special extended char before appending new one
if (isCtrlCode(ccData1)) {
if (isSpecialNorthAmericanChar(ccData1, ccData2)) {
currentCueBuilder.append(getSpecialNorthAmericanChar(ccData2));
} else if (isExtendedWestEuropeanChar(ccData1, ccData2)) {
// Remove standard equivalent of the special extended char before appending new one.
currentCueBuilder.backspace();
if ((ccData1 & 0x01) == 0x00) {
// Extended Spanish/Miscellaneous and French character set (S = 0).
currentCueBuilder.append(getExtendedEsFrChar(ccData2));
} else {
// Extended Portuguese and German/Danish character set (S = 1).
currentCueBuilder.append(getExtendedPtDeChar(ccData2));
}
currentCueBuilder.append(getExtendedWestEuropeanChar(ccData1, ccData2));
} else if (isMidrowCtrlCode(ccData1, ccData2)) {
handleMidrowCtrl(ccData2);
} else if (isPreambleAddressCode(ccData1, ccData2)) {
handlePreambleAddressCode(ccData1, ccData2);
} else if (isTabCtrlCode(ccData1, ccData2)) {
currentCueBuilder.tabOffset = ccData2 - 0x20;
} else if (isMiscCode(ccData1, ccData2)) {
handleMiscCode(ccData2);
}
} else {
// Basic North American character set.
currentCueBuilder.append(getBasicChar(ccData1));
if ((ccData2 & 0xE0) != 0x00) {
currentCueBuilder.append(getBasicChar(ccData2));
}
continue;
}
// Control character.
// ccData1 - 0|0|0|X|X|X|X|X
if ((ccData1 & 0xE0) == 0x00) {
handleCtrl(ccData1, ccData2, repeatedControlPossible);
continue;
}
if (currentChannel != selectedChannel) {
continue;
}
// Basic North American character set.
currentCueBuilder.append(getChar(ccData1));
if ((ccData2 & 0xE0) != 0x00) {
currentCueBuilder.append(getChar(ccData2));
}
captionDataProcessed = true;
}
if (captionDataProcessed) {
@ -440,15 +422,22 @@ public final class Cea608Decoder extends CeaDecoder {
}
}
private void handleCtrl(byte cc1, byte cc2, boolean repeatedControlPossible) {
currentChannel = getChannel(cc1);
private boolean updateAndVerifyCurrentChannel(byte cc1) {
if (isCtrlCode(cc1)) {
currentChannel = getChannel(cc1);
}
return currentChannel == selectedChannel;
}
private boolean isRepeatedCommand(boolean captionValid, byte cc1, byte cc2) {
// Most control commands are sent twice in succession to ensure they are received properly. We
// don't want to process duplicate commands, so if we see the same repeatable command twice in a
// row then we ignore the second one.
if (isRepeatable(cc1)) {
if (repeatedControlPossible && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) {
if (captionValid && isRepeatable(cc1)) {
if (repeatableControlSet && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) {
// This is a repeated command, so we ignore it.
return;
repeatableControlSet = false;
return true;
} else {
// This is the first occurrence of a repeatable command. Set the repeatable control
// variables so that we can recognize and ignore a duplicate (if there is one), and then
@ -457,21 +446,11 @@ public final class Cea608Decoder extends CeaDecoder {
repeatableControlCc1 = cc1;
repeatableControlCc2 = cc2;
}
} else {
// This command is not repeatable.
repeatableControlSet = false;
}
if (currentChannel != selectedChannel) {
return;
}
if (isMidrowCtrlCode(cc1, cc2)) {
handleMidrowCtrl(cc2);
} else if (isPreambleAddressCode(cc1, cc2)) {
handlePreambleAddressCode(cc1, cc2);
} else if (isTabCtrlCode(cc1, cc2)) {
currentCueBuilder.tabOffset = cc2 - 0x20;
} else if (isMiscCode(cc1, cc2)) {
handleMiscCode(cc2);
}
return false;
}
private void handleMidrowCtrl(byte cc2) {
@ -676,16 +655,38 @@ public final class Cea608Decoder extends CeaDecoder {
}
}
private static char getChar(byte ccData) {
private static char getBasicChar(byte ccData) {
int index = (ccData & 0x7F) - 0x20;
return (char) BASIC_CHARACTER_SET[index];
}
private static char getSpecialChar(byte ccData) {
private static boolean isSpecialNorthAmericanChar(byte cc1, byte cc2) {
// cc1 - 0|0|0|1|C|0|0|1
// cc2 - 0|0|1|1|X|X|X|X
return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x30);
}
private static char getSpecialNorthAmericanChar(byte ccData) {
int index = ccData & 0x0F;
return (char) SPECIAL_CHARACTER_SET[index];
}
private static boolean isExtendedWestEuropeanChar(byte cc1, byte cc2) {
// cc1 - 0|0|0|1|C|0|1|S
// cc2 - 0|0|1|X|X|X|X|X
return ((cc1 & 0xF6) == 0x12) && ((cc2 & 0xE0) == 0x20);
}
private static char getExtendedWestEuropeanChar(byte cc1, byte cc2) {
if ((cc1 & 0x01) == 0x00) {
// Extended Spanish/Miscellaneous and French character set (S = 0).
return getExtendedEsFrChar(cc2);
} else {
// Extended Portuguese and German/Danish character set (S = 1).
return getExtendedPtDeChar(cc2);
}
}
private static char getExtendedEsFrChar(byte ccData) {
int index = ccData & 0x1F;
return (char) SPECIAL_ES_FR_CHARACTER_SET[index];
@ -696,6 +697,11 @@ public final class Cea608Decoder extends CeaDecoder {
return (char) SPECIAL_PT_DE_CHARACTER_SET[index];
}
private static boolean isCtrlCode(byte cc1) {
// cc1 - 0|0|0|X|X|X|X|X
return (cc1 & 0xE0) == 0x00;
}
private static int getChannel(byte cc1) {
// cc1 - X|X|X|X|C|X|X|X
return (cc1 >> 3) & 0x1;

View file

@ -49,7 +49,6 @@ public final class CacheDataSink implements DataSink {
private final long fragmentSize;
private final int bufferSize;
private boolean syncFileDescriptor;
private DataSpec dataSpec;
private long dataSpecFragmentSize;
private File file;
@ -108,18 +107,6 @@ public final class CacheDataSink implements DataSink {
this.cache = Assertions.checkNotNull(cache);
this.fragmentSize = fragmentSize == C.LENGTH_UNSET ? Long.MAX_VALUE : fragmentSize;
this.bufferSize = bufferSize;
syncFileDescriptor = true;
}
/**
* Sets whether file descriptors are synced when closing output streams.
*
* <p>This method is experimental, and will be renamed or removed in a future release.
*
* @param syncFileDescriptor Whether file descriptors are synced when closing output streams.
*/
public void experimental_setSyncFileDescriptor(boolean syncFileDescriptor) {
this.syncFileDescriptor = syncFileDescriptor;
}
@Override
@ -208,9 +195,6 @@ public final class CacheDataSink implements DataSink {
boolean success = false;
try {
outputStream.flush();
if (syncFileDescriptor) {
underlyingFileOutputStream.getFD().sync();
}
success = true;
} finally {
Util.closeQuietly(outputStream);

View file

@ -26,8 +26,6 @@ public final class CacheDataSinkFactory implements DataSink.Factory {
private final long fragmentSize;
private final int bufferSize;
private boolean syncFileDescriptor;
/** @see CacheDataSink#CacheDataSink(Cache, long) */
public CacheDataSinkFactory(Cache cache, long fragmentSize) {
this(cache, fragmentSize, CacheDataSink.DEFAULT_BUFFER_SIZE);
@ -40,20 +38,8 @@ public final class CacheDataSinkFactory implements DataSink.Factory {
this.bufferSize = bufferSize;
}
/**
* See {@link CacheDataSink#experimental_setSyncFileDescriptor(boolean)}.
*
* <p>This method is experimental, and will be renamed or removed in a future release.
*/
public CacheDataSinkFactory experimental_setSyncFileDescriptor(boolean syncFileDescriptor) {
this.syncFileDescriptor = syncFileDescriptor;
return this;
}
@Override
public DataSink createDataSink() {
CacheDataSink dataSink = new CacheDataSink(cache, fragmentSize, bufferSize);
dataSink.experimental_setSyncFileDescriptor(syncFileDescriptor);
return dataSink;
return new CacheDataSink(cache, fragmentSize, bufferSize);
}
}

View file

@ -679,7 +679,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
int bufferIndex,
int bufferFlags,
long bufferPresentationTimeUs,
boolean shouldSkip,
boolean isDecodeOnlyBuffer,
boolean isLastBuffer,
Format format)
throws ExoPlaybackException {
if (initialPositionUs == C.TIME_UNSET) {
@ -688,7 +689,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs;
if (shouldSkip) {
if (isDecodeOnlyBuffer && !isLastBuffer) {
skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true;
}
@ -736,10 +737,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs);
earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;
if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs)
if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastBuffer)
&& maybeDropBuffersToKeyframe(codec, bufferIndex, presentationTimeUs, positionUs)) {
return false;
} else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {
} else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastBuffer)) {
dropOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true;
}
@ -807,8 +808,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
/**
* Returns the offset that should be subtracted from {@code bufferPresentationTimeUs} in {@link
* #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, Format)} to
* get the playback position with respect to the media.
* #processOutputBuffer(long, long, MediaCodec, ByteBuffer, int, int, long, boolean, boolean,
* Format)} to get the playback position with respect to the media.
*/
protected long getOutputStreamOffsetUs() {
return outputStreamOffsetUs;
@ -860,9 +861,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* indicates that the buffer is late.
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
* measured at the start of the current iteration of the rendering loop.
* @param isLastBuffer Whether the buffer is the last buffer in the current stream.
*/
protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) {
return isBufferLate(earlyUs);
protected boolean shouldDropOutputBuffer(
long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) {
return isBufferLate(earlyUs) && !isLastBuffer;
}
/**
@ -873,9 +876,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
* negative value indicates that the buffer is late.
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
* measured at the start of the current iteration of the rendering loop.
* @param isLastBuffer Whether the buffer is the last buffer in the current stream.
*/
protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) {
return isBufferVeryLate(earlyUs);
protected boolean shouldDropBuffersToKeyframe(
long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) {
return isBufferVeryLate(earlyUs) && !isLastBuffer;
}
/**

View file

@ -42,6 +42,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
@ -242,7 +243,7 @@ public class DashManifestParser extends DefaultHandler
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
segmentBase = parseSegmentList(xpp, null);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, null);
segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList());
} else {
maybeSkipTag(xpp);
}
@ -323,6 +324,7 @@ public class DashManifestParser extends DefaultHandler
language,
roleDescriptors,
accessibilityDescriptors,
supplementalProperties,
segmentBase);
contentType = checkContentTypeConsistency(contentType,
getContentType(representationInfo.format));
@ -332,7 +334,8 @@ public class DashManifestParser extends DefaultHandler
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase);
segmentBase =
parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, supplementalProperties);
} else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream"));
} else if (XmlPullParserUtil.isStartTag(xpp)) {
@ -492,6 +495,7 @@ public class DashManifestParser extends DefaultHandler
String adaptationSetLanguage,
List<Descriptor> adaptationSetRoleDescriptors,
List<Descriptor> adaptationSetAccessibilityDescriptors,
List<Descriptor> adaptationSetSupplementalProperties,
SegmentBase segmentBase)
throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id");
@ -524,7 +528,9 @@ public class DashManifestParser extends DefaultHandler
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase);
segmentBase =
parseSegmentTemplate(
xpp, (SegmentTemplate) segmentBase, adaptationSetSupplementalProperties);
} else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) {
Pair<String, SchemeData> contentProtection = parseContentProtection(xpp);
if (contentProtection.first != null) {
@ -763,13 +769,19 @@ public class DashManifestParser extends DefaultHandler
startNumber, duration, timeline, segments);
}
protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, SegmentTemplate parent)
protected SegmentTemplate parseSegmentTemplate(
XmlPullParser xpp,
SegmentTemplate parent,
List<Descriptor> adaptationSetSupplementalProperties)
throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
parent != null ? parent.presentationTimeOffset : 0);
long duration = parseLong(xpp, "duration", parent != null ? parent.duration : C.TIME_UNSET);
long startNumber = parseLong(xpp, "startNumber", parent != null ? parent.startNumber : 1);
long endNumber =
parseLastSegmentNumberSupplementalProperty(adaptationSetSupplementalProperties);
UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media",
parent != null ? parent.mediaTemplate : null);
UrlTemplate initializationTemplate = parseUrlTemplate(xpp, "initialization",
@ -794,8 +806,16 @@ public class DashManifestParser extends DefaultHandler
timeline = timeline != null ? timeline : parent.segmentTimeline;
}
return buildSegmentTemplate(initialization, timescale, presentationTimeOffset,
startNumber, duration, timeline, initializationTemplate, mediaTemplate);
return buildSegmentTemplate(
initialization,
timescale,
presentationTimeOffset,
startNumber,
endNumber,
duration,
timeline,
initializationTemplate,
mediaTemplate);
}
protected SegmentTemplate buildSegmentTemplate(
@ -803,12 +823,21 @@ public class DashManifestParser extends DefaultHandler
long timescale,
long presentationTimeOffset,
long startNumber,
long endNumber,
long duration,
List<SegmentTimelineElement> timeline,
UrlTemplate initializationTemplate,
UrlTemplate mediaTemplate) {
return new SegmentTemplate(initialization, timescale, presentationTimeOffset,
startNumber, duration, timeline, initializationTemplate, mediaTemplate);
return new SegmentTemplate(
initialization,
timescale,
presentationTimeOffset,
startNumber,
endNumber,
duration,
timeline,
initializationTemplate,
mediaTemplate);
}
/**
@ -1445,6 +1474,18 @@ public class DashManifestParser extends DefaultHandler
}
}
protected static long parseLastSegmentNumberSupplementalProperty(
List<Descriptor> supplementalProperties) {
for (int i = 0; i < supplementalProperties.size(); i++) {
Descriptor descriptor = supplementalProperties.get(i);
if ("http://dashif.org/guidelines/last-segment-number"
.equalsIgnoreCase(descriptor.schemeIdUri)) {
return Long.parseLong(descriptor.value);
}
}
return C.INDEX_UNSET;
}
/** A parsed Representation element. */
protected static final class RepresentationInfo {

View file

@ -277,6 +277,7 @@ public abstract class SegmentBase {
/* package */ final UrlTemplate initializationTemplate;
/* package */ final UrlTemplate mediaTemplate;
/* package */ final long endNumber;
/**
* @param initialization A {@link RangedUri} corresponding to initialization data, if such data
@ -286,6 +287,9 @@ public abstract class SegmentBase {
* @param presentationTimeOffset The presentation time offset. The value in seconds is the
* division of this value and {@code timescale}.
* @param startNumber The sequence number of the first segment.
* @param endNumber The sequence number of the last segment as specified by the
* SupplementalProperty with schemeIdUri="http://dashif.org/guidelines/last-segment-number",
* or {@link C#INDEX_UNSET}.
* @param duration The duration of each segment in the case of fixed duration segments. The
* value in seconds is the division of this value and {@code timescale}. If {@code
* segmentTimeline} is non-null then this parameter is ignored.
@ -302,14 +306,21 @@ public abstract class SegmentBase {
long timescale,
long presentationTimeOffset,
long startNumber,
long endNumber,
long duration,
List<SegmentTimelineElement> segmentTimeline,
UrlTemplate initializationTemplate,
UrlTemplate mediaTemplate) {
super(initialization, timescale, presentationTimeOffset, startNumber,
duration, segmentTimeline);
super(
initialization,
timescale,
presentationTimeOffset,
startNumber,
duration,
segmentTimeline);
this.initializationTemplate = initializationTemplate;
this.mediaTemplate = mediaTemplate;
this.endNumber = endNumber;
}
@Override
@ -340,6 +351,8 @@ public abstract class SegmentBase {
public int getSegmentCount(long periodDurationUs) {
if (segmentTimeline != null) {
return segmentTimeline.size();
} else if (endNumber != C.INDEX_UNSET) {
return (int) (endNumber - startNumber + 1);
} else if (periodDurationUs != C.TIME_UNSET) {
long durationUs = (duration * C.MICROS_PER_SECOND) / timescale;
return (int) Util.ceilDivide(periodDurationUs, durationUs);
@ -347,7 +360,6 @@ public abstract class SegmentBase {
return DashSegmentIndex.INDEX_UNBOUNDED;
}
}
}
/**

View file

@ -586,6 +586,7 @@ public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> {
} else {
subType = parser.getAttributeValue(null, KEY_SUB_TYPE);
}
putNormalizedAttribute(KEY_SUB_TYPE, subType);
name = parser.getAttributeValue(null, KEY_NAME);
url = parseRequiredString(parser, KEY_URL);
maxWidth = parseInt(parser, KEY_MAX_WIDTH, Format.NO_VALUE);
@ -645,6 +646,7 @@ public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> {
private static final String KEY_CHANNELS = "Channels";
private static final String KEY_FOUR_CC = "FourCC";
private static final String KEY_TYPE = "Type";
private static final String KEY_SUB_TYPE = "Subtype";
private static final String KEY_LANGUAGE = "Language";
private static final String KEY_NAME = "Name";
private static final String KEY_MAX_WIDTH = "MaxWidth";
@ -709,6 +711,18 @@ public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> {
/* roleFlags= */ 0,
language);
} else if (type == C.TRACK_TYPE_TEXT) {
String subType = (String) getNormalizedAttribute(KEY_SUB_TYPE);
@C.RoleFlags int roleFlags = 0;
switch (subType) {
case "CAPT":
roleFlags = C.ROLE_FLAG_CAPTION;
break;
case "DESC":
roleFlags = C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND;
break;
default:
break;
}
String language = (String) getNormalizedAttribute(KEY_LANGUAGE);
format =
Format.createTextContainerFormat(
@ -719,7 +733,7 @@ public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> {
/* codecs= */ null,
bitrate,
/* selectionFlags= */ 0,
/* roleFlags= */ 0,
roleFlags,
language);
} else {
format =

View file

@ -40,7 +40,7 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.media:media:1.0.0'
implementation 'androidx.media:media:1.0.1'
implementation 'androidx.annotation:annotation:1.0.2'
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils-robolectric')

View file

@ -1050,6 +1050,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!useController || player == null) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isTouching = true;
@ -1150,9 +1153,6 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
// Internal methods.
private boolean toggleControllerVisibility() {
if (!useController || player == null) {
return false;
}
if (!controller.isVisible()) {
maybeShowController(true);
} else if (controllerHideOnTouch) {
@ -1472,6 +1472,9 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
@Override
public boolean onSingleTapUp(MotionEvent e) {
if (!useController || player == null) {
return false;
}
return toggleControllerVisibility();
}
}

View file

@ -23,6 +23,21 @@ if (project.ext.has("exoplayerPublishEnabled")
groupId = 'com.google.android.exoplayer'
website = 'https://github.com/google/ExoPlayer'
}
gradle.taskGraph.whenReady { taskGraph ->
project.tasks
.findAll { task -> task.name.contains("generatePomFileFor") }
.forEach { task ->
task.doLast {
task.outputs.files
.filter { File file ->
file.path.contains("publications") \
&& file.name.matches("^pom-.+\\.xml\$")
}
.forEach { File file -> addLicense(file) }
}
}
}
}
def getBintrayRepo() {
@ -30,3 +45,24 @@ def getBintrayRepo() {
property('publicRepo').toBoolean()
return publicRepo ? 'exoplayer' : 'exoplayer-test'
}
static void addLicense(File pom) {
def licenseNode = new Node(null, "license")
licenseNode.append(
new Node(null, "name", "The Apache Software License, Version 2.0"))
licenseNode.append(
new Node(null, "url", "http://www.apache.org/licenses/LICENSE-2.0.txt"))
licenseNode.append(new Node(null, "distribution", "repo"))
def licensesNode = new Node(null, "licenses")
licensesNode.append(licenseNode)
def xml = new XmlParser().parse(pom)
xml.append(licensesNode)
def writer = new PrintWriter(new FileWriter(pom))
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
def printer = new XmlNodePrinter(writer)
printer.preserveWhitespace = true
printer.print(xml)
writer.close()
}

View file

@ -163,14 +163,15 @@ public class DebugRenderersFactory extends DefaultRenderersFactory {
int bufferIndex,
int bufferFlags,
long bufferPresentationTimeUs,
boolean shouldSkip,
boolean isDecodeOnlyBuffer,
boolean isLastBuffer,
Format format)
throws ExoPlaybackException {
if (skipToPositionBeforeRenderingFirstFrame && bufferPresentationTimeUs < positionUs) {
// After the codec has been initialized, don't render the first frame until we've caught up
// to the playback position. Else test runs on devices that do not support dummy surface
// will drop frames between rendering the first one and catching up [Internal: b/66494991].
shouldSkip = true;
isDecodeOnlyBuffer = true;
}
return super.processOutputBuffer(
positionUs,
@ -180,7 +181,8 @@ public class DebugRenderersFactory extends DefaultRenderersFactory {
bufferIndex,
bufferFlags,
bufferPresentationTimeUs,
shouldSkip,
isDecodeOnlyBuffer,
isLastBuffer,
format);
}