diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 246a968256..04bf514c12 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -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
diff --git a/build.gradle b/build.gradle
index a0e8fcf20a..bc538ead68 100644
--- a/build.gradle
+++ b/build.gradle
@@ -44,6 +44,7 @@ allprojects {
}
buildDir = "${externalBuildDir}/${project.name}"
}
+ group = 'com.google.android.exoplayer'
}
apply from: 'javadoc_combined.gradle'
diff --git a/constants.gradle b/constants.gradle
index bf464ad2c1..70e77b22c6 100644
--- a/constants.gradle
+++ b/constants.gradle
@@ -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
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
index a7dd1a0df8..bc409410c3 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java
@@ -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 {
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
index c0b5fd67f6..7e72904078 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
@@ -377,6 +377,13 @@ public final class MediaSessionConnector {
/**
* Gets the {@link MediaMetadataCompat} to be published to the session.
*
+ *
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);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
index db3f3943e1..190f4de5a6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
- public static final String VERSION = "2.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}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
index 697f35e417..f1b6b5bc90 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -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;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
index a3c0990366..be1b7d3d53 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
@@ -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 {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
index d43bd6cbf8..ace7ebbcc6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
@@ -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();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
index 3820836e49..fb684f627f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
@@ -544,7 +544,7 @@ public class DefaultDrmSessionManager implements DrmSe
@Override
public void onEvent(
ExoMediaDrm extends T> md,
- byte[] sessionId,
+ @Nullable byte[] sessionId,
int event,
int extra,
@Nullable byte[] data) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
index 49915f3af5..6bd8d9688f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java
@@ -80,7 +80,7 @@ public interface ExoMediaDrm {
*/
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 {
throws NotProvisionedException;
/** @see MediaDrm#provideKeyResponse(byte[], byte[]) */
+ @Nullable
byte[] provideKeyResponse(byte[] scope, byte[] response)
throws NotProvisionedException, DeniedByServerException;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
index 615aa0e7b1..2cb7e66a2c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
@@ -84,8 +84,6 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) {
@@ -160,8 +158,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm 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) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java
index e8652d653f..48914b7c2c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java
@@ -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;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java
index 92db91e20b..5ddaafb4a8 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java
@@ -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;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
index 08ba94f257..2158f182b1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
@@ -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)
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
index 5f7f5d60b7..c3072a1590 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -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();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java
index 7e98f30301..821696aae7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java
@@ -817,10 +817,10 @@ public final class DownloadHelper {
private final MediaSource mediaSource;
private final DownloadHelper downloadHelper;
private final Allocator allocator;
+ private final ArrayList pendingMediaPeriods;
+ private final Handler downloadHelperHandler;
private final HandlerThread mediaSourceThread;
private final Handler mediaSourceHandler;
- private final Handler downloadHelperHandler;
- private final ArrayList 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() {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
index e6679e1a5a..752239c991 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java
@@ -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 {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
index d9f0008a7f..4dafa0ba76 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
@@ -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;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
index 774b94a43c..5a14063aa1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
@@ -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;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
index 2caf4c92f8..566c928b20 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
@@ -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.
- *
- * 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);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java
index 856e9db168..ce9735badd 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java
@@ -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)}.
- *
- *
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);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
index e75a3866b6..8d5b890c7f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
@@ -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;
}
/**
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
index c4f61a73cd..f03a443431 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
@@ -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 adaptationSetRoleDescriptors,
List adaptationSetAccessibilityDescriptors,
+ List 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 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 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 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 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 {
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java
index f033232590..ba4faafd95 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java
@@ -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 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;
}
}
-
}
/**
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java
index 66731660f5..39e22f2982 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java
@@ -586,6 +586,7 @@ public class SsManifestParser implements ParsingLoadable.Parser {
} 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 {
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 {
/* 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 {
/* codecs= */ null,
bitrate,
/* selectionFlags= */ 0,
- /* roleFlags= */ 0,
+ roleFlags,
language);
} else {
format =
diff --git a/library/ui/build.gradle b/library/ui/build.gradle
index 49446b25de..6384bf920f 100644
--- a/library/ui/build.gradle
+++ b/library/ui/build.gradle
@@ -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')
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java
index c7ffda8ae5..e6bc1a6a71 100644
--- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java
@@ -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();
}
}
diff --git a/publish.gradle b/publish.gradle
index 85cf87aa85..8cfc2b2ea1 100644
--- a/publish.gradle
+++ b/publish.gradle
@@ -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("\n")
+ def printer = new XmlNodePrinter(writer)
+ printer.preserveWhitespace = true
+ printer.print(xml)
+ writer.close()
+}
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java
index 9feaf6863a..8b11a89d8d 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java
@@ -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);
}