diff --git a/README.md b/README.md index 37967dd527a..03f16bd6556 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ repository and depend on the modules locally. ### From JCenter ### +#### 1. Add repositories #### + The easiest way to get started using ExoPlayer is to add it as a gradle dependency. You need to make sure you have the Google and JCenter repositories included in the `build.gradle` file in the root of your project: @@ -38,6 +40,8 @@ repositories { } ``` +#### 2. Add ExoPlayer module dependencies #### + Next add a dependency in the `build.gradle` file of your app module. The following will add a dependency to the full library: @@ -45,15 +49,7 @@ following will add a dependency to the full library: implementation 'com.google.android.exoplayer:exoplayer:2.X.X' ``` -where `2.X.X` is your preferred version. If not enabled already, you also need -to turn on Java 8 support in all `build.gradle` files depending on ExoPlayer, by -adding the following to the `android` section: - -```gradle -compileOptions { - targetCompatibility JavaVersion.VERSION_1_8 -} -``` +where `2.X.X` is your preferred version. As an alternative to the full library, you can depend on only the library modules that you actually need. For example the following will add dependencies @@ -87,6 +83,32 @@ JCenter can be found on [Bintray][]. [extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [Bintray]: https://bintray.com/google/exoplayer +#### 3. Turn on Java 8 support #### + +If not enabled already, you also need to turn on Java 8 support in all +`build.gradle` files depending on ExoPlayer, by adding the following to the +`android` section: + +```gradle +compileOptions { + targetCompatibility JavaVersion.VERSION_1_8 +} +``` + +Note that if you want to use Java 8 features in your own code, the following +additional options need to be set: + +```gradle +// For Java compilers: +compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 +} +// For Kotlin compilers: +kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 +} +``` + ### Locally ### Cloning the repository and depending on the modules locally is required when diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1699f2c09b0..68794310bd5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,34 @@ # Release notes # +### 2.9.4 ### + +* IMA extension: Clear ads loader listeners on release + ([#4114](https://github.com/google/ExoPlayer/issues/4114)). +* SmoothStreaming: Fix support for subtitles in DRM protected streams + ([#5378](https://github.com/google/ExoPlayer/issues/5378)). +* FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior + of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)). +* GVR extension: upgrade GVR SDK dependency to 1.190.0. +* Associate fatal player errors of type SOURCE with the loading source in + `AnalyticsListener.EventTime` + ([#5407](https://github.com/google/ExoPlayer/issues/5407)). +* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where + using lazy preparation in `ConcatenatingMediaSource` with an + `ExtractorMediaSource` overrides initial seek positions + ([#5350](https://github.com/google/ExoPlayer/issues/5350)). +* Add subtext to the `MediaDescriptionAdapter` of the + `PlayerNotificationManager`. +* Add workaround for video quality problems with Amlogic decoders + ([#5003](https://github.com/google/ExoPlayer/issues/5003)). +* Fix issue where sending callbacks for playlist changes may cause problems + because of parallel player access + ([#5240](https://github.com/google/ExoPlayer/issues/5240)). +* Fix issue with reusing a `ClippingMediaSource` with an inner + `ExtractorMediaSource` and a non-zero start position + ([#5351](https://github.com/google/ExoPlayer/issues/5351)). +* Fix issue where uneven track durations in MP4 streams can cause OOM problems + ([#3670](https://github.com/google/ExoPlayer/issues/3670)). + ### 2.9.3 ### * Captions: Support PNG subtitles in SMPTE-TT diff --git a/constants.gradle b/constants.gradle index ac801d2d3b5..716ddbadbaf 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.9.3' - releaseVersionCode = 2009003 + releaseVersion = '2.9.4' + releaseVersionCode = 2009004 // Important: ExoPlayer specifies a minSdkVersion of 14 because various // components provided by the library may be of use on older devices. // However, please note that the core media playback functionality provided diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 30968b8f851..65896851240 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; +import com.google.android.gms.dynamite.DynamiteModule; /** * An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}. @@ -61,7 +62,20 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Getting the cast context later than onStart can cause device discovery not to take place. - castContext = CastContext.getSharedInstance(this); + try { + castContext = CastContext.getSharedInstance(this); + } catch (RuntimeException e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof DynamiteModule.LoadingException) { + setContentView(R.layout.cast_context_error_message_layout); + return; + } + cause = cause.getCause(); + } + // Unknown error. We propagate it. + throw e; + } setContentView(R.layout.main_activity); @@ -91,6 +105,10 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public void onResume() { super.onResume(); + if (castContext == null) { + // There is no Cast context to work with. Do nothing. + return; + } playerManager = PlayerManager.createPlayerManager( /* queuePositionListener= */ this, @@ -104,6 +122,10 @@ public void onResume() { @Override public void onPause() { super.onPause(); + if (castContext == null) { + // Nothing to release. + return; + } mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount()); mediaQueueList.setAdapter(null); playerManager.release(); diff --git a/demos/cast/src/main/res/layout/cast_context_error_message_layout.xml b/demos/cast/src/main/res/layout/cast_context_error_message_layout.xml new file mode 100644 index 00000000000..6d3260de385 --- /dev/null +++ b/demos/cast/src/main/res/layout/cast_context_error_message_layout.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index 3505c404006..58f5233412d 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -22,4 +22,6 @@ Add samples + Failed to get Cast context. Try updating Google Play Services and restart the app. + diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 30fe10085f4..0baa074d4a7 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -31,7 +31,9 @@ android { } dependencies { - api 'com.google.android.gms:play-services-cast-framework:16.0.3' + api 'com.google.android.gms:play-services-cast-framework:16.1.2' + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') testImplementation project(modulePrefix + 'testutils') diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 6f3c623f3fa..c5b76002fa9 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -37,6 +37,10 @@ private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536; private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2; + // Error codes matching ffmpeg_jni.cc. + private static final int DECODER_ERROR_INVALID_DATA = -1; + private static final int DECODER_ERROR_OTHER = -2; + private final String codecName; private final @Nullable byte[] extraData; private final @C.Encoding int encoding; @@ -106,8 +110,14 @@ protected FfmpegDecoderException createUnexpectedDecodeException(Throwable error int inputSize = inputData.limit(); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize); int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize); - if (result < 0) { - return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result); + if (result == DECODER_ERROR_INVALID_DATA) { + // Treat invalid data errors as non-fatal to match the behavior of MediaCodec. No output will + // be produced for this buffer, so mark it as decode-only to ensure that the audio sink's + // position is reset when more audio is produced. + outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY); + return null; + } else if (result == DECODER_ERROR_OTHER) { + return new FfmpegDecoderException("Error decoding (see logcat)."); } if (!hasOutputFormat) { channelCount = ffmpegGetChannelCount(nativeContext); diff --git a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc index 87579ebb9a4..dcd4560e4ab 100644 --- a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc +++ b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc @@ -63,6 +63,10 @@ static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16; // Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT. static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT; +// Error codes matching FfmpegDecoder.java. +static const int DECODER_ERROR_INVALID_DATA = -1; +static const int DECODER_ERROR_OTHER = -2; + /** * Returns the AVCodec with the specified name, or NULL if it is not available. */ @@ -79,7 +83,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData, /** * Decodes the packet into the output buffer, returning the number of bytes - * written, or a negative value in the case of an error. + * written, or a negative DECODER_ERROR constant value in the case of an error. */ int decodePacket(AVCodecContext *context, AVPacket *packet, uint8_t *outputBuffer, int outputSize); @@ -238,6 +242,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData, context->channels = rawChannelCount; context->channel_layout = av_get_default_channel_layout(rawChannelCount); } + context->err_recognition = AV_EF_IGNORE_ERR; int result = avcodec_open2(context, codec, NULL); if (result < 0) { logError("avcodec_open2", result); @@ -254,7 +259,8 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, result = avcodec_send_packet(context, packet); if (result) { logError("avcodec_send_packet", result); - return result; + return result == AVERROR_INVALIDDATA ? DECODER_ERROR_INVALID_DATA + : DECODER_ERROR_OTHER; } // Dequeue output data until it runs out. diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index af973e13455..234f5518967 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -32,7 +32,8 @@ android { dependencies { implementation project(modulePrefix + 'library-core') implementation 'com.android.support:support-annotations:' + supportLibraryVersion - implementation 'com.google.vr:sdk-audio:1.80.0' + api 'com.google.vr:sdk-base:1.190.0' + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } ext { diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 6ca3bfd8817..9b4b66125cd 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -597,6 +597,8 @@ public void release() { adsManager.destroy(); adsManager = null; } + adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this); + adsLoader.removeAdErrorListener(/* adErrorListener= */ this); imaPausedContent = false; imaAdState = IMA_AD_STATE_NONE; pendingAdLoadError = null; diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 85042c43549..0978ee401ca 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -97,8 +97,8 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - return adsMediaSource.createPeriod(id, allocator); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + return adsMediaSource.createPeriod(id, allocator, startPositionUs); } @Override diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java index b626a087802..59dfc6473cb 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakeAd.java @@ -64,14 +64,17 @@ public double getTimeOffset() { }; } + @Override public int getVastMediaWidth() { throw new UnsupportedOperationException(); } + @Override public int getVastMediaHeight() { throw new UnsupportedOperationException(); } + @Override public int getVastMediaBitrate() { throw new UnsupportedOperationException(); } diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md index b222bdabd95..3863dff9659 100644 --- a/extensions/rtmp/README.md +++ b/extensions/rtmp/README.md @@ -39,7 +39,7 @@ either instantiated and injected from application code, or obtained from instances of `DataSource.Factory` that are instantiated and injected from application code. -`DefaultDataSource` will automatically use uses the RTMP extension whenever it's +`DefaultDataSource` will automatically use the RTMP extension whenever it's available. Hence if your application is using `DefaultDataSource` or `DefaultDataSourceFactory`, adding support for RTMP streams is as simple as adding a dependency to the RTMP extension as described above. No changes to your diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index fac9818d9ec..8810b510006 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -460,8 +460,8 @@ private C() {} /** * Flags which can apply to a buffer containing a media sample. Possible flag values are {@link - * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_ENCRYPTED} and - * {@link #BUFFER_FLAG_DECODE_ONLY}. + * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE}, + * {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -470,6 +470,7 @@ private C() {} value = { BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_END_OF_STREAM, + BUFFER_FLAG_LAST_SAMPLE, BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_DECODE_ONLY }) @@ -482,6 +483,8 @@ private C() {} * Flag for empty buffers that signal that the end of the stream was reached. */ public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM; + /** Indicates that a buffer is known to contain the last media sample of the stream. */ + public static final int BUFFER_FLAG_LAST_SAMPLE = 1 << 29; // 0x20000000 /** Indicates that a buffer is (at least partially) encrypted. */ public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000 /** Indicates that a buffer should be decoded but not rendered. */ @@ -896,6 +899,26 @@ private C() {} */ public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL; + /** Video projection types. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + Format.NO_VALUE, + PROJECTION_RECTANGULAR, + PROJECTION_EQUIRECTANGULAR, + PROJECTION_CUBEMAP, + PROJECTION_MESH + }) + public @interface Projection {} + /** Conventional rectangular projection. */ + public static final int PROJECTION_RECTANGULAR = 0; + /** Equirectangular spherical projection. */ + public static final int PROJECTION_EQUIRECTANGULAR = 1; + /** Cube map projection. */ + public static final int PROJECTION_CUBEMAP = 2; + /** 3-D mesh projection. */ + public static final int PROJECTION_MESH = 3; + /** * Priority for media playback. * 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 792f6cf6510..36723c5d73b 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.9.3"; + public static final String VERSION = "2.9.4"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.3"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.4"; /** * 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 = 2009003; + public static final int VERSION_INT = 2009004; /** * 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/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 3456fc39a2f..d40ae6eccda 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -1181,6 +1181,37 @@ public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { metadata); } + public Format copyWithFrameRate(float frameRate) { + return new Format( + id, + label, + containerMimeType, + sampleMimeType, + codecs, + bitrate, + maxInputSize, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + selectionFlags, + language, + accessibilityChannel, + subsampleOffsetUs, + initializationData, + drmInitData, + metadata); + } + public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { return new Format( id, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index 5925c8f3830..8e51740c56c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -79,7 +79,7 @@ public MediaPeriodHolder( this.info = info; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; - MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, allocator); + MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, allocator, info.startPositionUs); if (info.id.endPositionUs != C.TIME_END_OF_SOURCE) { mediaPeriod = new ClippingMediaPeriod( 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 fe52cc7e8c2..eba02b0c0a8 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 @@ -94,25 +94,25 @@ public interface VideoListener extends com.google.android.exoplayer2.video.Video private final AudioFocusManager audioFocusManager; - private Format videoFormat; - private Format audioFormat; + @Nullable private Format videoFormat; + @Nullable private Format audioFormat; - private Surface surface; + @Nullable private Surface surface; private boolean ownsSurface; private @C.VideoScalingMode int videoScalingMode; - private SurfaceHolder surfaceHolder; - private TextureView textureView; + @Nullable private SurfaceHolder surfaceHolder; + @Nullable private TextureView textureView; private int surfaceWidth; private int surfaceHeight; - private DecoderCounters videoDecoderCounters; - private DecoderCounters audioDecoderCounters; + @Nullable private DecoderCounters videoDecoderCounters; + @Nullable private DecoderCounters audioDecoderCounters; private int audioSessionId; private AudioAttributes audioAttributes; private float audioVolume; - private MediaSource mediaSource; + @Nullable private MediaSource mediaSource; private List currentCues; - private VideoFrameMetadataListener videoFrameMetadataListener; - private CameraMotionListener cameraMotionListener; + @Nullable private VideoFrameMetadataListener videoFrameMetadataListener; + @Nullable private CameraMotionListener cameraMotionListener; private boolean hasNotifiedFullWrongThreadWarning; /** @@ -558,30 +558,26 @@ public void setPlaybackParams(@Nullable PlaybackParams params) { setPlaybackParameters(playbackParameters); } - /** - * Returns the video format currently being played, or null if no video is being played. - */ + /** Returns the video format currently being played, or null if no video is being played. */ + @Nullable public Format getVideoFormat() { return videoFormat; } - /** - * Returns the audio format currently being played, or null if no audio is being played. - */ + /** Returns the audio format currently being played, or null if no audio is being played. */ + @Nullable public Format getAudioFormat() { return audioFormat; } - /** - * Returns {@link DecoderCounters} for video, or null if no video is being played. - */ + /** Returns {@link DecoderCounters} for video, or null if no video is being played. */ + @Nullable public DecoderCounters getVideoDecoderCounters() { return videoDecoderCounters; } - /** - * Returns {@link DecoderCounters} for audio, or null if no audio is being played. - */ + /** Returns {@link DecoderCounters} for audio, or null if no audio is being played. */ + @Nullable public DecoderCounters getAudioDecoderCounters() { return audioDecoderCounters; } @@ -1048,7 +1044,8 @@ public Timeline getCurrentTimeline() { } @Override - public @Nullable Object getCurrentManifest() { + @Nullable + public Object getCurrentManifest() { verifyApplicationThread(); return player.getCurrentManifest(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 113add612a3..55031e2d12f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -488,7 +488,10 @@ public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { @Override public final void onPlayerError(ExoPlaybackException error) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = + error.type == ExoPlaybackException.TYPE_SOURCE + ? generateLoadingMediaPeriodEventTime() + : generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPlayerError(eventTime, error); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 9b6be57e4ce..bd63765953f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -366,7 +366,10 @@ private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderExc if (outputBuffer == null) { return false; } - decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; + if (outputBuffer.skippedOutputBufferCount > 0) { + decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; + audioSink.handleDiscontinuity(); + } } if (outputBuffer.isEndOfStream()) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 86b750e821f..187b9ae443e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -191,7 +191,11 @@ public final class MatroskaExtractor implements Extractor { private static final int ID_CUE_CLUSTER_POSITION = 0xF1; private static final int ID_LANGUAGE = 0x22B59C; private static final int ID_PROJECTION = 0x7670; + private static final int ID_PROJECTION_TYPE = 0x7671; private static final int ID_PROJECTION_PRIVATE = 0x7672; + private static final int ID_PROJECTION_POSE_YAW = 0x7673; + private static final int ID_PROJECTION_POSE_PITCH = 0x7674; + private static final int ID_PROJECTION_POSE_ROLL = 0x7675; private static final int ID_STEREO_MODE = 0x53B8; private static final int ID_COLOUR = 0x55B0; private static final int ID_COLOUR_RANGE = 0x55B9; @@ -760,6 +764,24 @@ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOExce case ID_MAX_FALL: currentTrack.maxFrameAverageLuminance = (int) value; break; + case ID_PROJECTION_TYPE: + switch ((int) value) { + case 0: + currentTrack.projectionType = C.PROJECTION_RECTANGULAR; + break; + case 1: + currentTrack.projectionType = C.PROJECTION_EQUIRECTANGULAR; + break; + case 2: + currentTrack.projectionType = C.PROJECTION_CUBEMAP; + break; + case 3: + currentTrack.projectionType = C.PROJECTION_MESH; + break; + default: + break; + } + break; default: break; } @@ -803,6 +825,15 @@ public int read(ExtractorInput input, PositionHolder seekPosition) throws IOExce case ID_LUMNINANCE_MIN: currentTrack.minMasteringLuminance = (float) value; break; + case ID_PROJECTION_POSE_YAW: + currentTrack.projectionPoseYaw = (float) value; + break; + case ID_PROJECTION_POSE_PITCH: + currentTrack.projectionPosePitch = (float) value; + break; + case ID_PROJECTION_POSE_ROLL: + currentTrack.projectionPoseRoll = (float) value; + break; default: break; } @@ -1465,6 +1496,7 @@ public int getElementType(int id) { case ID_COLOUR_PRIMARIES: case ID_MAX_CLL: case ID_MAX_FALL: + case ID_PROJECTION_TYPE: return TYPE_UNSIGNED_INT; case ID_DOC_TYPE: case ID_NAME: @@ -1491,6 +1523,9 @@ public int getElementType(int id) { case ID_WHITE_POINT_CHROMATICITY_Y: case ID_LUMNINANCE_MAX: case ID_LUMNINANCE_MIN: + case ID_PROJECTION_POSE_YAW: + case ID_PROJECTION_POSE_PITCH: + case ID_PROJECTION_POSE_ROLL: return TYPE_FLOAT; default: return TYPE_UNKNOWN; @@ -1631,6 +1666,10 @@ private static final class Track { public int displayWidth = Format.NO_VALUE; public int displayHeight = Format.NO_VALUE; public int displayUnit = DISPLAY_UNIT_PIXELS; + @C.Projection public int projectionType = Format.NO_VALUE; + public float projectionPoseYaw = 0f; + public float projectionPosePitch = 0f; + public float projectionPoseRoll = 0f; public byte[] projectionData = null; @C.StereoMode public int stereoMode = Format.NO_VALUE; @@ -1850,6 +1889,21 @@ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserE } else if ("htc_video_rotA-270".equals(name)) { rotationDegrees = 270; } + if (projectionType == C.PROJECTION_RECTANGULAR + && Float.compare(projectionPoseYaw, 0f) == 0 + && Float.compare(projectionPosePitch, 0f) == 0) { + // The range of projectionPoseRoll is [-180, 180]. + if (Float.compare(projectionPoseRoll, 0f) == 0) { + rotationDegrees = 0; + } else if (Float.compare(projectionPosePitch, 90f) == 0) { + rotationDegrees = 90; + } else if (Float.compare(projectionPosePitch, -180f) == 0 + || Float.compare(projectionPosePitch, 180f) == 0) { + rotationDegrees = 180; + } else if (Float.compare(projectionPosePitch, -90f) == 0) { + rotationDegrees = 270; + } + } format = Format.createVideoSampleFormat( Integer.toString(trackId), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index f51c97389bf..8d78337617c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -22,8 +22,8 @@ import java.util.Arrays; import java.util.List; -@SuppressWarnings("ConstantField") -/* package*/ abstract class Atom { +@SuppressWarnings({"ConstantField", "ConstantCaseForConstants"}) +/* package */ abstract class Atom { /** * Size of an atom header, in bytes. @@ -130,6 +130,7 @@ public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb"); public static final int TYPE_udta = Util.getIntegerCodeForString("udta"); public static final int TYPE_meta = Util.getIntegerCodeForString("meta"); + public static final int TYPE_keys = Util.getIntegerCodeForString("keys"); public static final int TYPE_ilst = Util.getIntegerCodeForString("ilst"); public static final int TYPE_mean = Util.getIntegerCodeForString("mean"); public static final int TYPE_name = Util.getIntegerCodeForString("name"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index d085156f2bf..008a155d1fe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -17,6 +17,7 @@ import static com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType; +import android.support.annotation.Nullable; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -39,7 +40,7 @@ import java.util.List; /** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */ -@SuppressWarnings("ConstantField") +@SuppressWarnings({"ConstantField", "ConstantCaseForConstants"}) /* package */ final class AtomParsers { private static final String TAG = "AtomParsers"; @@ -51,6 +52,7 @@ private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); private static final int TYPE_meta = Util.getIntegerCodeForString("meta"); + private static final int TYPE_mdta = Util.getIntegerCodeForString("mdta"); /** * The threshold number of samples to trim from the start/end of an audio track when applying an @@ -77,7 +79,7 @@ public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime) throws ParserException { Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); - int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data); + int trackType = getTrackTypeForHdlr(parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data)); if (trackType == C.TRACK_TYPE_UNKNOWN) { return null; } @@ -485,6 +487,7 @@ public static TrackSampleTable parseStbl( * @param isQuickTime True for QuickTime media. False otherwise. * @return Parsed metadata, or null. */ + @Nullable public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { if (isQuickTime) { // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and @@ -499,14 +502,69 @@ public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { int atomType = udtaData.readInt(); if (atomType == Atom.TYPE_meta) { udtaData.setPosition(atomPosition); - return parseMetaAtom(udtaData, atomPosition + atomSize); + return parseUdtaMeta(udtaData, atomPosition + atomSize); } - udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); + udtaData.setPosition(atomPosition + atomSize); } return null; } - private static Metadata parseMetaAtom(ParsableByteArray meta, int limit) { + /** + * Parses a metadata meta atom if it contains metadata with handler 'mdta'. + * + * @param meta The metadata atom to decode. + * @return Parsed metadata, or null. + */ + @Nullable + public static Metadata parseMdtaFromMeta(Atom.ContainerAtom meta) { + Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr); + Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys); + Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst); + if (hdlrAtom == null + || keysAtom == null + || ilstAtom == null + || AtomParsers.parseHdlr(hdlrAtom.data) != TYPE_mdta) { + // There isn't enough information to parse the metadata, or the handler type is unexpected. + return null; + } + + // Parse metadata keys. + ParsableByteArray keys = keysAtom.data; + keys.setPosition(Atom.FULL_HEADER_SIZE); + int entryCount = keys.readInt(); + String[] keyNames = new String[entryCount]; + for (int i = 0; i < entryCount; i++) { + int entrySize = keys.readInt(); + keys.skipBytes(4); // keyNamespace + int keySize = entrySize - 8; + keyNames[i] = keys.readString(keySize); + } + + // Parse metadata items. + ParsableByteArray ilst = ilstAtom.data; + ilst.setPosition(Atom.HEADER_SIZE); + ArrayList entries = new ArrayList<>(); + while (ilst.bytesLeft() > Atom.HEADER_SIZE) { + int atomPosition = ilst.getPosition(); + int atomSize = ilst.readInt(); + int keyIndex = ilst.readInt() - 1; + if (keyIndex >= 0 && keyIndex < keyNames.length) { + String key = keyNames[keyIndex]; + Metadata.Entry entry = + MetadataUtil.parseMdtaMetadataEntryFromIlst(ilst, atomPosition + atomSize, key); + if (entry != null) { + entries.add(entry); + } + } else { + Log.w(TAG, "Skipped metadata with unknown key index: " + keyIndex); + } + ilst.setPosition(atomPosition + atomSize); + } + return entries.isEmpty() ? null : new Metadata(entries); + } + + @Nullable + private static Metadata parseUdtaMeta(ParsableByteArray meta, int limit) { meta.skipBytes(Atom.FULL_HEADER_SIZE); while (meta.getPosition() < limit) { int atomPosition = meta.getPosition(); @@ -516,11 +574,12 @@ private static Metadata parseMetaAtom(ParsableByteArray meta, int limit) { meta.setPosition(atomPosition); return parseIlst(meta, atomPosition + atomSize); } - meta.skipBytes(atomSize - Atom.HEADER_SIZE); + meta.setPosition(atomPosition + atomSize); } return null; } + @Nullable private static Metadata parseIlst(ParsableByteArray ilst, int limit) { ilst.skipBytes(Atom.HEADER_SIZE); ArrayList entries = new ArrayList<>(); @@ -610,19 +669,22 @@ private static TkhdData parseTkhd(ParsableByteArray tkhd) { * Parses an hdlr atom. * * @param hdlr The hdlr atom to decode. - * @return The track type. + * @return The handler value. */ private static int parseHdlr(ParsableByteArray hdlr) { hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4); - int trackType = hdlr.readInt(); - if (trackType == TYPE_soun) { + return hdlr.readInt(); + } + + /** Returns the track type for a given handler value. */ + private static int getTrackTypeForHdlr(int hdlr) { + if (hdlr == TYPE_soun) { return C.TRACK_TYPE_AUDIO; - } else if (trackType == TYPE_vide) { + } else if (hdlr == TYPE_vide) { return C.TRACK_TYPE_VIDEO; - } else if (trackType == TYPE_text || trackType == TYPE_sbtl || trackType == TYPE_subt - || trackType == TYPE_clcp) { + } else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) { return C.TRACK_TYPE_TEXT; - } else if (trackType == TYPE_meta) { + } else if (hdlr == TYPE_meta) { return C.TRACK_TYPE_METADATA; } else { return C.TRACK_TYPE_UNKNOWN; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java new file mode 100644 index 00000000000..b458a8f0f41 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntry.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.mp4; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; + +/** + * Stores extensible metadata with handler type 'mdta'. See also the QuickTime File Format + * Specification. + */ +public final class MdtaMetadataEntry implements Metadata.Entry { + + /** The metadata key name. */ + public final String key; + /** The payload. The interpretation of the value depends on {@link #typeIndicator}. */ + public final byte[] value; + /** The four byte locale indicator. */ + public final int localeIndicator; + /** The four byte type indicator. */ + public final int typeIndicator; + + /** Creates a new metadata entry for the specified metadata key/value. */ + public MdtaMetadataEntry(String key, byte[] value, int localeIndicator, int typeIndicator) { + this.key = key; + this.value = value; + this.localeIndicator = localeIndicator; + this.typeIndicator = typeIndicator; + } + + private MdtaMetadataEntry(Parcel in) { + key = Util.castNonNull(in.readString()); + value = new byte[in.readInt()]; + in.readByteArray(value); + localeIndicator = in.readInt(); + typeIndicator = in.readInt(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + MdtaMetadataEntry other = (MdtaMetadataEntry) obj; + return key.equals(other.key) + && Arrays.equals(value, other.value) + && localeIndicator == other.localeIndicator + && typeIndicator == other.typeIndicator; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + key.hashCode(); + result = 31 * result + Arrays.hashCode(value); + result = 31 * result + localeIndicator; + result = 31 * result + typeIndicator; + return result; + } + + @Override + public String toString() { + return "mdta: key=" + key; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(key); + dest.writeInt(value.length); + dest.writeByteArray(value); + dest.writeInt(localeIndicator); + dest.writeInt(typeIndicator); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public MdtaMetadataEntry createFromParcel(Parcel in) { + return new MdtaMetadataEntry(in); + } + + @Override + public MdtaMetadataEntry[] newArray(int size) { + return new MdtaMetadataEntry[size]; + } + }; +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index 670fe116a64..02522897ce2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -16,6 +16,9 @@ package com.google.android.exoplayer2.extractor.mp4; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame; @@ -25,10 +28,9 @@ import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; +import java.nio.ByteBuffer; -/** - * Parses metadata items stored in ilst atoms. - */ +/** Utilities for handling metadata in MP4. */ /* package */ final class MetadataUtil { private static final String TAG = "MetadataUtil"; @@ -103,24 +105,73 @@ private static final String LANGUAGE_UNDEFINED = "und"; + private static final int TYPE_TOP_BYTE_COPYRIGHT = 0xA9; + private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \uFFFD. + + private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps"; + private static final int MDTA_TYPE_INDICATOR_FLOAT = 23; + private MetadataUtil() {} /** - * Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting - * from the current position of the {@link ParsableByteArray}, and the position is advanced by the - * size of the element. The position is advanced even if the element's type is unrecognized. + * Returns a {@link Format} that is the same as the input format but includes information from the + * specified sources of metadata. + */ + public static Format getFormatWithMetadata( + int trackType, + Format format, + @Nullable Metadata udtaMetadata, + @Nullable Metadata mdtaMetadata, + GaplessInfoHolder gaplessInfoHolder) { + if (trackType == C.TRACK_TYPE_AUDIO) { + if (gaplessInfoHolder.hasGaplessInfo()) { + format = + format.copyWithGaplessInfo( + gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding); + } + // We assume all udta metadata is associated with the audio track. + if (udtaMetadata != null) { + format = format.copyWithMetadata(udtaMetadata); + } + } else if (trackType == C.TRACK_TYPE_VIDEO && mdtaMetadata != null) { + // Populate only metadata keys that are known to be specific to video. + for (int i = 0; i < mdtaMetadata.length(); i++) { + Metadata.Entry entry = mdtaMetadata.get(i); + if (entry instanceof MdtaMetadataEntry) { + MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry; + if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key) + && mdtaMetadataEntry.typeIndicator == MDTA_TYPE_INDICATOR_FLOAT) { + try { + float fps = ByteBuffer.wrap(mdtaMetadataEntry.value).asFloatBuffer().get(); + format = format.copyWithFrameRate(fps); + format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry)); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring invalid framerate"); + } + } + } + } + } + return format; + } + + /** + * Parses a single userdata ilst element from a {@link ParsableByteArray}. The element is read + * starting from the current position of the {@link ParsableByteArray}, and the position is + * advanced by the size of the element. The position is advanced even if the element's type is + * unrecognized. * * @param ilst Holds the data to be parsed. * @return The parsed element, or null if the element's type was not recognized. */ - public static @Nullable Metadata.Entry parseIlstElement(ParsableByteArray ilst) { + @Nullable + public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) { int position = ilst.getPosition(); int endPosition = position + ilst.readInt(); int type = ilst.readInt(); int typeTopByte = (type >> 24) & 0xFF; try { - if (typeTopByte == '\u00A9' /* Copyright char */ - || typeTopByte == '\uFFFD' /* Replacement char */) { + if (typeTopByte == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) { int shortType = type & 0x00FFFFFF; if (shortType == SHORT_TYPE_COMMENT) { return parseCommentAttribute(type, ilst); @@ -185,7 +236,36 @@ private MetadataUtil() {} } } - private static @Nullable TextInformationFrame parseTextAttribute( + /** + * Parses an 'mdta' metadata entry starting at the current position in an ilst box. + * + * @param ilst The ilst box. + * @param endPosition The end position of the entry in the ilst box. + * @param key The mdta metadata entry key for the entry. + * @return The parsed element, or null if the entry wasn't recognized. + */ + @Nullable + public static MdtaMetadataEntry parseMdtaMetadataEntryFromIlst( + ParsableByteArray ilst, int endPosition, String key) { + int atomPosition; + while ((atomPosition = ilst.getPosition()) < endPosition) { + int atomSize = ilst.readInt(); + int atomType = ilst.readInt(); + if (atomType == Atom.TYPE_data) { + int typeIndicator = ilst.readInt(); + int localeIndicator = ilst.readInt(); + int dataSize = atomSize - 16; + byte[] value = new byte[dataSize]; + ilst.readBytes(value, 0, dataSize); + return new MdtaMetadataEntry(key, value, localeIndicator, typeIndicator); + } + ilst.setPosition(atomPosition + atomSize); + } + return null; + } + + @Nullable + private static TextInformationFrame parseTextAttribute( int type, String id, ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); @@ -198,7 +278,8 @@ private MetadataUtil() {} return null; } - private static @Nullable CommentFrame parseCommentAttribute(int type, ParsableByteArray data) { + @Nullable + private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); if (atomType == Atom.TYPE_data) { @@ -210,7 +291,8 @@ private MetadataUtil() {} return null; } - private static @Nullable Id3Frame parseUint8Attribute( + @Nullable + private static Id3Frame parseUint8Attribute( int type, String id, ParsableByteArray data, @@ -229,7 +311,8 @@ private MetadataUtil() {} return null; } - private static @Nullable TextInformationFrame parseIndexAndCountAttribute( + @Nullable + private static TextInformationFrame parseIndexAndCountAttribute( int type, String attributeName, ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); @@ -249,8 +332,8 @@ private MetadataUtil() {} return null; } - private static @Nullable TextInformationFrame parseStandardGenreAttribute( - ParsableByteArray data) { + @Nullable + private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) { int genreCode = parseUint8AttributeValue(data); String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length) ? STANDARD_GENRES[genreCode - 1] : null; @@ -261,7 +344,8 @@ private MetadataUtil() {} return null; } - private static @Nullable ApicFrame parseCoverArt(ParsableByteArray data) { + @Nullable + private static ApicFrame parseCoverArt(ParsableByteArray data) { int atomSize = data.readInt(); int atomType = data.readInt(); if (atomType == Atom.TYPE_data) { @@ -285,8 +369,8 @@ private MetadataUtil() {} return null; } - private static @Nullable Id3Frame parseInternalAttribute( - ParsableByteArray data, int endPosition) { + @Nullable + private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) { String domain = null; String name = null; int dataAtomPosition = -1; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 17c82c2c5b1..eec48c23cb4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -75,7 +75,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private static final int STATE_READING_ATOM_PAYLOAD = 1; private static final int STATE_READING_SAMPLE = 2; - // Brand stored in the ftyp atom for QuickTime media. + /** Brand stored in the ftyp atom for QuickTime media. */ private static final int BRAND_QUICKTIME = Util.getIntegerCodeForString("qt "); /** @@ -377,15 +377,21 @@ private void processMoovAtom(ContainerAtom moov) throws ParserException { long durationUs = C.TIME_UNSET; List tracks = new ArrayList<>(); - Metadata metadata = null; + // Process metadata. + Metadata udtaMetadata = null; GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); if (udta != null) { - metadata = AtomParsers.parseUdta(udta, isQuickTime); - if (metadata != null) { - gaplessInfoHolder.setFromMetadata(metadata); + udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime); + if (udtaMetadata != null) { + gaplessInfoHolder.setFromMetadata(udtaMetadata); } } + Metadata mdtaMetadata = null; + Atom.ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta); + if (meta != null) { + mdtaMetadata = AtomParsers.parseMdtaFromMeta(meta); + } boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0; ArrayList trackSampleTables = @@ -401,15 +407,9 @@ private void processMoovAtom(ContainerAtom moov) throws ParserException { // Allow ten source samples per output sample, like the platform extractor. int maxInputSize = trackSampleTable.maximumSize + 3 * 10; Format format = track.format.copyWithMaxInputSize(maxInputSize); - if (track.type == C.TRACK_TYPE_AUDIO) { - if (gaplessInfoHolder.hasGaplessInfo()) { - format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, - gaplessInfoHolder.encoderPadding); - } - if (metadata != null) { - format = format.copyWithMetadata(metadata); - } - } + format = + MetadataUtil.getFormatWithMetadata( + track.type, format, udtaMetadata, mdtaMetadata, gaplessInfoHolder); mp4Track.trackOutput.format(format); durationUs = @@ -716,24 +716,37 @@ private static boolean processFtypAtom(ParsableByteArray atomData) { return false; } - /** - * Returns whether the extractor should decode a leaf atom with type {@code atom}. - */ + /** Returns whether the extractor should decode a leaf atom with type {@code atom}. */ private static boolean shouldParseLeafAtom(int atom) { - return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr - || atom == Atom.TYPE_stsd || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss - || atom == Atom.TYPE_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc - || atom == Atom.TYPE_stsz || atom == Atom.TYPE_stz2 || atom == Atom.TYPE_stco - || atom == Atom.TYPE_co64 || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp - || atom == Atom.TYPE_udta; + return atom == Atom.TYPE_mdhd + || atom == Atom.TYPE_mvhd + || atom == Atom.TYPE_hdlr + || atom == Atom.TYPE_stsd + || atom == Atom.TYPE_stts + || atom == Atom.TYPE_stss + || atom == Atom.TYPE_ctts + || atom == Atom.TYPE_elst + || atom == Atom.TYPE_stsc + || atom == Atom.TYPE_stsz + || atom == Atom.TYPE_stz2 + || atom == Atom.TYPE_stco + || atom == Atom.TYPE_co64 + || atom == Atom.TYPE_tkhd + || atom == Atom.TYPE_ftyp + || atom == Atom.TYPE_udta + || atom == Atom.TYPE_keys + || atom == Atom.TYPE_ilst; } - /** - * Returns whether the extractor should decode a container atom with type {@code atom}. - */ + /** Returns whether the extractor should decode a container atom with type {@code atom}. */ private static boolean shouldParseContainerAtom(int atom) { - return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia - || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_edts; + return atom == Atom.TYPE_moov + || atom == Atom.TYPE_trak + || atom == Atom.TYPE_mdia + || atom == Atom.TYPE_minf + || atom == Atom.TYPE_stbl + || atom == Atom.TYPE_edts + || atom == Atom.TYPE_meta; } private static final class Mp4Track { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java index 021c9de654f..a1c90bf1f25 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java @@ -27,9 +27,7 @@ */ /* package */ final class Sniffer { - /** - * The maximum number of bytes to peek when sniffing. - */ + /** The maximum number of bytes to peek when sniffing. */ private static final int SEARCH_LENGTH = 4 * 1024; private static final int[] COMPATIBLE_BRANDS = new int[] { @@ -109,15 +107,19 @@ private static boolean sniffInternal(ExtractorInput input, boolean fragmented) headerSize = Atom.LONG_HEADER_SIZE; input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE); buffer.setLimit(Atom.LONG_HEADER_SIZE); - atomSize = buffer.readUnsignedLongToLong(); + atomSize = buffer.readLong(); } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { // The atom extends to the end of the file. - long endPosition = input.getLength(); - if (endPosition != C.LENGTH_UNSET) { - atomSize = endPosition - input.getPosition() + headerSize; + long fileEndPosition = input.getLength(); + if (fileEndPosition != C.LENGTH_UNSET) { + atomSize = fileEndPosition - input.getPeekPosition() + headerSize; } } + if (inputLength != C.LENGTH_UNSET && bytesSearched + atomSize > inputLength) { + // The file is invalid because the atom extends past the end of the file. + return false; + } if (atomSize < headerSize) { // The file is invalid because the atom size is too small for its header. return false; @@ -125,6 +127,13 @@ private static boolean sniffInternal(ExtractorInput input, boolean fragmented) bytesSearched += headerSize; if (atomType == Atom.TYPE_moov) { + // We have seen the moov atom. We increase the search size to make sure we don't miss an + // mvex atom because the moov's size exceeds the search length. + bytesToSearch += (int) atomSize; + if (inputLength != C.LENGTH_UNSET && bytesToSearch > inputLength) { + // Make sure we don't exceed the file size. + bytesToSearch = (int) inputLength; + } // Check for an mvex atom inside the moov atom to identify whether the file is fragmented. continue; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java index 56851fc1e09..59ea3863354 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java @@ -64,6 +64,9 @@ public TrackSampleTable( this.flags = flags; this.durationUs = durationUs; sampleCount = offsets.length; + if (flags.length > 0) { + flags[flags.length - 1] |= C.BUFFER_FLAG_LAST_SAMPLE; + } } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 4d971d461e1..9ae50179c34 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -326,7 +326,9 @@ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, S || Util.MODEL.startsWith("SM-G350") || Util.MODEL.startsWith("SM-G386") || Util.MODEL.startsWith("SM-T231") - || Util.MODEL.startsWith("SM-T530"))) { + || Util.MODEL.startsWith("SM-T530") + || Util.MODEL.startsWith("SCH-I535") + || Util.MODEL.startsWith("SPH-L710"))) { return false; } if ("OMX.brcm.audio.mp3.decoder".equals(name) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 3ed18049bf1..fce1c4b8770 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -240,10 +240,10 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( - mediaSource.createPeriod(id, allocator), + mediaSource.createPeriod(id, allocator, startPositionUs), enableInitialDiscontinuity, periodStartUs, periodEndUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 03ccd566455..c93afdb2495 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -16,13 +16,12 @@ package com.google.android.exoplayer2.source; import android.os.Handler; +import android.os.Message; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Pair; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; @@ -45,8 +44,7 @@ * during playback. It is valid for the same {@link MediaSource} instance to be present more than * once in the concatenation. Access to this class is thread-safe. */ -public class ConcatenatingMediaSource extends CompositeMediaSource - implements PlayerMessage.Target { +public class ConcatenatingMediaSource extends CompositeMediaSource { private static final int MSG_ADD = 0; private static final int MSG_REMOVE = 1; @@ -68,8 +66,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(index, mediaSourceHolders, actionOnCompletion)) - .send(); + if (playbackThreadHandler != null && !mediaSources.isEmpty()) { + playbackThreadHandler + .obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, actionOnCompletion)) + .sendToTarget(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -328,12 +324,10 @@ public final synchronized void removeMediaSourceRange( } return; } - if (player != null) { - player - .createMessage(this) - .setType(MSG_REMOVE) - .setPayload(new MessageData<>(fromIndex, toIndex, actionOnCompletion)) - .send(); + if (playbackThreadHandler != null) { + playbackThreadHandler + .obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, actionOnCompletion)) + .sendToTarget(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -371,12 +365,10 @@ public final synchronized void moveMediaSource( return; } mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); - if (player != null) { - player - .createMessage(this) - .setType(MSG_MOVE) - .setPayload(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) - .send(); + if (playbackThreadHandler != null) { + playbackThreadHandler + .obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, actionOnCompletion)) + .sendToTarget(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -430,8 +422,8 @@ public final synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) { */ public final synchronized void setShuffleOrder( ShuffleOrder shuffleOrder, @Nullable Runnable actionOnCompletion) { - ExoPlayer player = this.player; - if (player != null) { + Handler playbackThreadHandler = this.playbackThreadHandler; + if (playbackThreadHandler != null) { int size = getSize(); if (shuffleOrder.getLength() != size) { shuffleOrder = @@ -439,11 +431,11 @@ public final synchronized void setShuffleOrder( .cloneAndClear() .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size); } - player - .createMessage(this) - .setType(MSG_SET_SHUFFLE_ORDER) - .setPayload(new MessageData<>(/* index= */ 0, shuffleOrder, actionOnCompletion)) - .send(); + playbackThreadHandler + .obtainMessage( + MSG_SET_SHUFFLE_ORDER, + new MessageData<>(/* index= */ 0, shuffleOrder, actionOnCompletion)) + .sendToTarget(); } else { this.shuffleOrder = shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; @@ -465,8 +457,8 @@ public final synchronized void prepareSourceInternal( boolean isTopLevelSource, @Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(player, isTopLevelSource, mediaTransferListener); - this.player = player; - playerApplicationHandler = new Handler(player.getApplicationLooper()); + playbackThreadHandler = new Handler(/* callback= */ this::handleMessage); + applicationThreadHandler = new Handler(player.getApplicationLooper()); if (mediaSourcesPublic.isEmpty()) { notifyListener(); } else { @@ -484,7 +476,8 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { } @Override - public final MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public final MediaPeriod createPeriod( + MediaPeriodId id, Allocator allocator, long startPositionUs) { Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid); if (holder == null) { @@ -492,7 +485,8 @@ public final MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { holder = new MediaSourceHolder(new DummyMediaSource()); holder.hasStartedPreparing = true; } - DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, id, allocator); + DeferredMediaPeriod mediaPeriod = + new DeferredMediaPeriod(holder.mediaSource, id, allocator, startPositionUs); mediaSourceByMediaPeriod.put(mediaPeriod, holder); holder.activeMediaPeriods.add(mediaPeriod); if (!holder.hasStartedPreparing) { @@ -519,8 +513,8 @@ public final void releaseSourceInternal() { super.releaseSourceInternal(); mediaSourceHolders.clear(); mediaSourceByUid.clear(); - player = null; - playerApplicationHandler = null; + playbackThreadHandler = null; + applicationThreadHandler = null; shuffleOrder = shuffleOrder.cloneAndClear(); windowCount = 0; periodCount = 0; @@ -556,24 +550,22 @@ protected int getWindowIndexForChildWindowIndex( return windowIndex + mediaSourceHolder.firstWindowIndexInChild; } - @Override @SuppressWarnings("unchecked") - public final void handleMessage(int messageType, @Nullable Object message) - throws ExoPlaybackException { - if (player == null) { + private boolean handleMessage(Message msg) { + if (playbackThreadHandler == null) { // Stale event. - return; + return false; } - switch (messageType) { + switch (msg.what) { case MSG_ADD: MessageData> addMessage = - (MessageData>) Util.castNonNull(message); + (MessageData>) Util.castNonNull(msg.obj); shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size()); addMediaSourcesInternal(addMessage.index, addMessage.customData); scheduleListenerNotification(addMessage.actionOnCompletion); break; case MSG_REMOVE: - MessageData removeMessage = (MessageData) Util.castNonNull(message); + MessageData removeMessage = (MessageData) Util.castNonNull(msg.obj); int fromIndex = removeMessage.index; int toIndex = removeMessage.customData; if (fromIndex == 0 && toIndex == shuffleOrder.getLength()) { @@ -587,7 +579,7 @@ public final void handleMessage(int messageType, @Nullable Object message) scheduleListenerNotification(removeMessage.actionOnCompletion); break; case MSG_MOVE: - MessageData moveMessage = (MessageData) Util.castNonNull(message); + MessageData moveMessage = (MessageData) Util.castNonNull(msg.obj); shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1); shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); moveMediaSourceInternal(moveMessage.index, moveMessage.customData); @@ -595,7 +587,7 @@ public final void handleMessage(int messageType, @Nullable Object message) break; case MSG_SET_SHUFFLE_ORDER: MessageData shuffleOrderMessage = - (MessageData) Util.castNonNull(message); + (MessageData) Util.castNonNull(msg.obj); shuffleOrder = shuffleOrderMessage.customData; scheduleListenerNotification(shuffleOrderMessage.actionOnCompletion); break; @@ -603,8 +595,8 @@ public final void handleMessage(int messageType, @Nullable Object message) notifyListener(); break; case MSG_ON_COMPLETION: - List actionsOnCompletion = (List) Util.castNonNull(message); - Handler handler = Assertions.checkNotNull(playerApplicationHandler); + List actionsOnCompletion = (List) Util.castNonNull(msg.obj); + Handler handler = Assertions.checkNotNull(applicationThreadHandler); for (int i = 0; i < actionsOnCompletion.size(); i++) { handler.post(actionsOnCompletion.get(i)); } @@ -612,11 +604,14 @@ public final void handleMessage(int messageType, @Nullable Object message) default: throw new IllegalStateException(); } + return true; } private void scheduleListenerNotification(@Nullable Runnable actionOnCompletion) { if (!listenerNotificationScheduled) { - Assertions.checkNotNull(player).createMessage(this).setType(MSG_NOTIFY_LISTENER).send(); + Assertions.checkNotNull(playbackThreadHandler) + .obtainMessage(MSG_NOTIFY_LISTENER) + .sendToTarget(); listenerNotificationScheduled = true; } if (actionOnCompletion != null) { @@ -636,11 +631,9 @@ private void notifyListener() { mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), /* manifest= */ null); if (!actionsOnCompletion.isEmpty()) { - Assertions.checkNotNull(player) - .createMessage(this) - .setType(MSG_ON_COMPLETION) - .setPayload(actionsOnCompletion) - .send(); + Assertions.checkNotNull(playbackThreadHandler) + .obtainMessage(MSG_ON_COMPLETION, actionsOnCompletion) + .sendToTarget(); } } @@ -718,6 +711,7 @@ private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Time // unlikely to be a problem as a non-zero default position usually only occurs for live // playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions // anyway. + timeline.getWindow(/* windowIndex= */ 0, window); long windowStartPositionUs = window.getDefaultPositionUs(); if (deferredMediaPeriod != null) { long periodPreparePositionUs = deferredMediaPeriod.getPreparePositionUs(); @@ -1101,7 +1095,7 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { throw new UnsupportedOperationException(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java index 26c25a749eb..858769180d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -25,7 +25,7 @@ /** * Media period that wraps a media source and defers calling its {@link - * MediaSource#createPeriod(MediaPeriodId, Allocator)} method until {@link + * MediaSource#createPeriod(MediaPeriodId, Allocator, long)} method until {@link * #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media * period immediately but the media source that should create it is not yet prepared. */ @@ -60,11 +60,14 @@ public interface PrepareErrorListener { * @param mediaSource The media source to wrap. * @param id The identifier used to create the deferred media period. * @param allocator The allocator used to create the media period. + * @param preparePositionUs The expected start position, in microseconds. */ - public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { + public DeferredMediaPeriod( + MediaSource mediaSource, MediaPeriodId id, Allocator allocator, long preparePositionUs) { this.id = id; this.allocator = allocator; this.mediaSource = mediaSource; + this.preparePositionUs = preparePositionUs; preparePositionOverrideUs = C.TIME_UNSET; } @@ -86,28 +89,25 @@ public long getPreparePositionUs() { /** * Overrides the default prepare position at which to prepare the media period. This value is only - * used if the call to {@link MediaPeriod#prepare(Callback, long)} is being deferred. + * used if called before {@link #createPeriod(MediaPeriodId)}. * - * @param defaultPreparePositionUs The default prepare position to use, in microseconds. + * @param preparePositionUs The default prepare position to use, in microseconds. */ - public void overridePreparePositionUs(long defaultPreparePositionUs) { - preparePositionOverrideUs = defaultPreparePositionUs; + public void overridePreparePositionUs(long preparePositionUs) { + preparePositionOverrideUs = preparePositionUs; } /** - * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then - * prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()} - * to release the period. + * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source + * then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link + * #releasePeriod()} to release the period. * * @param id The identifier that should be used to create the media period from the media source. */ public void createPeriod(MediaPeriodId id) { - mediaPeriod = mediaSource.createPeriod(id, allocator); + long preparePositionUs = getPreparePositionWithOverride(this.preparePositionUs); + mediaPeriod = mediaSource.createPeriod(id, allocator, preparePositionUs); if (callback != null) { - long preparePositionUs = - preparePositionOverrideUs != C.TIME_UNSET - ? preparePositionOverrideUs - : this.preparePositionUs; mediaPeriod.prepare(this, preparePositionUs); } } @@ -124,9 +124,8 @@ public void releasePeriod() { @Override public void prepare(Callback callback, long preparePositionUs) { this.callback = callback; - this.preparePositionUs = preparePositionUs; if (mediaPeriod != null) { - mediaPeriod.prepare(this, preparePositionUs); + mediaPeriod.prepare(this, getPreparePositionWithOverride(this.preparePositionUs)); } } @@ -217,4 +216,9 @@ public void onPrepared(MediaPeriod mediaPeriod) { callback.onPrepared(this); } + private long getPreparePositionWithOverride(long preparePositionUs) { + return preparePositionOverrideUs != C.TIME_UNSET + ? preparePositionOverrideUs + : preparePositionUs; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 31daf65d386..823901af2a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -346,18 +346,19 @@ public long getBufferedPositionUs() { } else if (isPendingReset()) { return pendingResetPositionUs; } - long largestQueuedTimestampUs; + long largestQueuedTimestampUs = C.TIME_UNSET; if (haveAudioVideoTracks) { // Ignore non-AV tracks, which may be sparse or poorly interleaved. largestQueuedTimestampUs = Long.MAX_VALUE; int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { - if (trackIsAudioVideoFlags[i]) { + if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) { largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs, sampleQueues[i].getLargestQueuedTimestampUs()); } } - } else { + } + if (largestQueuedTimestampUs == C.TIME_UNSET) { largestQueuedTimestampUs = getLargestQueuedTimestampUs(); } return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 085b5dba717..9465a8f1a29 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -370,7 +370,7 @@ public void prepareSourceInternal( boolean isTopLevelSource, @Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; - notifySourceInfoRefreshed(timelineDurationUs, /* isSeekable= */ false); + notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable); } @Override @@ -379,7 +379,7 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { DataSource dataSource = dataSourceFactory.createDataSource(); if (transferListener != null) { dataSource.addTransferListener(transferListener); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index cac15d5ed93..281465eec35 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -80,14 +80,15 @@ public void prepareSourceInternal( } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { if (loopCount == Integer.MAX_VALUE) { - return childSource.createPeriod(id, allocator); + return childSource.createPeriod(id, allocator, startPositionUs); } Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid); MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid); childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id); - MediaPeriod mediaPeriod = childSource.createPeriod(childMediaPeriodId, allocator); + MediaPeriod mediaPeriod = + childSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId); return mediaPeriod; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 74449ba16b5..801737faef6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -35,8 +35,8 @@ * on the {@link SourceInfoRefreshListener}s passed to {@link #prepareSource(ExoPlayer, * boolean, SourceInfoRefreshListener, TransferListener)}. *
  • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are - * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for - * the player to load and read the media. + * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a + * way for the player to load and read the media. * * * All methods are called on the player's internal playback thread, as described in the {@link @@ -274,9 +274,10 @@ void prepareSource( * * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. + * @param startPositionUs The expected start position, in microseconds. * @return A new {@link MediaPeriod}. */ - MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator); + MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs); /** * Releases the period. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 573e97cb13c..cc7202f9b2d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -124,13 +124,13 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { MediaPeriod[] periods = new MediaPeriod[mediaSources.length]; int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid); for (int i = 0; i < periods.length; i++) { MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex)); - periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator); + periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator, startPositionUs); } return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index e5b950cf2e6..ab5c5e57d9a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -57,6 +57,7 @@ public static final class SampleExtrasHolder { private long largestDiscardedTimestampUs; private long largestQueuedTimestampUs; + private boolean isLastSampleQueued; private boolean upstreamKeyframeRequired; private boolean upstreamFormatRequired; private Format upstreamFormat; @@ -93,6 +94,7 @@ public void reset(boolean resetUpstreamFormat) { upstreamKeyframeRequired = true; largestDiscardedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; + isLastSampleQueued = false; if (resetUpstreamFormat) { upstreamFormat = null; upstreamFormatRequired = true; @@ -118,6 +120,7 @@ public long discardUpstreamSamples(int discardFromIndex) { Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); length -= discardCount; largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); + isLastSampleQueued = discardCount == 0 && isLastSampleQueued; if (length == 0) { return 0; } else { @@ -186,6 +189,19 @@ public synchronized long getLargestQueuedTimestampUs() { return largestQueuedTimestampUs; } + /** + * Returns whether the last sample of the stream has knowingly been queued. A return value of + * {@code false} means that the last sample had not been queued or that it's unknown whether the + * last sample has been queued. + * + *

    Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * considered as having been queued. Samples that were dequeued from the front of the queue are + * considered as having been queued. + */ + public synchronized boolean isLastSampleQueued() { + return isLastSampleQueued; + } + /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ public synchronized long getFirstTimestampUs() { return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; @@ -224,7 +240,7 @@ public synchronized int read(FormatHolder formatHolder, DecoderInputBuffer buffe boolean formatRequired, boolean loadingFinished, Format downstreamFormat, SampleExtrasHolder extrasHolder) { if (!hasNextSample()) { - if (loadingFinished) { + if (loadingFinished || isLastSampleQueued) { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; } else if (upstreamFormat != null @@ -388,7 +404,9 @@ public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlag upstreamKeyframeRequired = false; } Assertions.checkState(!upstreamFormatRequired); - commitSampleTimestamp(timeUs); + + isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0; + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); int relativeEndIndex = getRelativeIndex(length); timesUs[relativeEndIndex] = timeUs; @@ -439,10 +457,6 @@ public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlag } } - public synchronized void commitSampleTimestamp(long timeUs) { - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); - } - /** * Attempts to discard samples from the end of the queue to allow samples starting from the * specified timestamp to be spliced in. Samples will not be discarded prior to the read position. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index ecc720c656d..0886e79d212 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -224,6 +224,15 @@ public long getLargestQueuedTimestampUs() { return metadataQueue.getLargestQueuedTimestampUs(); } + /** + * Returns whether the last sample of the stream has knowingly been queued. A return value of + * {@code false} means that the last sample had not been queued or that it's unknown whether the + * last sample has been queued. + */ + public boolean isLastSampleQueued() { + return metadataQueue.isLastSampleQueued(); + } + /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ public long getFirstTimestampUs() { return metadataQueue.getFirstTimestampUs(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 66097970c79..046672bb77a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -318,7 +318,7 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { return new SingleSampleMediaPeriod( dataSpec, dataSourceFactory, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 19ddbd2c542..4bf661ddc05 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -341,7 +341,7 @@ public void prepareSourceInternal( } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { if (adPlaybackState.adGroupCount > 0 && id.isAd()) { int adGroupIndex = id.adGroupIndex; int adIndexInAdGroup = id.adIndexInAdGroup; @@ -360,7 +360,8 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { prepareChildSource(id, adMediaSource); } MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - DeferredMediaPeriod deferredMediaPeriod = new DeferredMediaPeriod(mediaSource, id, allocator); + DeferredMediaPeriod deferredMediaPeriod = + new DeferredMediaPeriod(mediaSource, id, allocator, startPositionUs); deferredMediaPeriod.setPrepareErrorListener( new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup)); List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); @@ -376,7 +377,8 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { } return deferredMediaPeriod; } else { - DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(contentMediaSource, id, allocator); + DeferredMediaPeriod mediaPeriod = + new DeferredMediaPeriod(contentMediaSource, id, allocator, startPositionUs); mediaPeriod.createPeriod(id); return mediaPeriod; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java index c7594995776..ab22e18358f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSource.java @@ -64,11 +64,11 @@ interface Factory { long open(DataSpec dataSpec) throws IOException; /** - * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at + * Reads up to {@code readLength} bytes of data and stores them into {@code buffer}, starting at * index {@code offset}. - *

    - * If {@code length} is zero then 0 is returned. Otherwise, if no data is available because the - * end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned. + * + *

    If {@code readLength} is zero then 0 is returned. Otherwise, if no data is available because + * the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned. * Otherwise, the call will block until at least one byte of data has been read and the number of * bytes read is returned. * 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 8d310015f80..63bc47504b0 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 @@ -43,8 +43,8 @@ public final class CacheDataSink implements DataSink { private final Cache cache; private final long maxCacheFileSize; private final int bufferSize; - private final boolean syncFileDescriptor; + private boolean syncFileDescriptor; private DataSpec dataSpec; private File file; private OutputStream outputStream; @@ -64,18 +64,6 @@ public CacheDataSinkException(IOException cause) { } - /** - * Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}. - * - * @param cache The cache into which data should be written. - * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for - * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into - * multiple cache files. - */ - public CacheDataSink(Cache cache, long maxCacheFileSize) { - this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE, true); - } - /** * Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}. * @@ -83,10 +71,9 @@ public CacheDataSink(Cache cache, long maxCacheFileSize) { * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for a * {@link DataSpec} whose size exceeds this value, then the data will be fragmented into * multiple cache files. - * @param syncFileDescriptor Whether file descriptors are sync'd when closing output streams. */ - public CacheDataSink(Cache cache, long maxCacheFileSize, boolean syncFileDescriptor) { - this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE, syncFileDescriptor); + public CacheDataSink(Cache cache, long maxCacheFileSize) { + this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE); } /** @@ -98,23 +85,21 @@ public CacheDataSink(Cache cache, long maxCacheFileSize, boolean syncFileDescrip * value disables buffering. */ public CacheDataSink(Cache cache, long maxCacheFileSize, int bufferSize) { - this(cache, maxCacheFileSize, bufferSize, true); + this.cache = Assertions.checkNotNull(cache); + this.maxCacheFileSize = maxCacheFileSize; + this.bufferSize = bufferSize; + syncFileDescriptor = true; } /** - * @param cache The cache into which data should be written. - * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for a - * {@link DataSpec} whose size exceeds this value, then the data will be fragmented into - * multiple cache files. - * @param bufferSize The buffer size in bytes for writing to a cache file. A zero or negative - * value disables buffering. - * @param syncFileDescriptor Whether file descriptors are sync'd when closing output streams. + * Sets whether file descriptors are synced when closing output streams. + * + *

    This method is experimental, and will be renamed or removed in a future release. It should + * only be called before the renderer is used. + * + * @param syncFileDescriptor Whether file descriptors are synced when closing output streams. */ - public CacheDataSink( - Cache cache, long maxCacheFileSize, int bufferSize, boolean syncFileDescriptor) { - this.cache = Assertions.checkNotNull(cache); - this.maxCacheFileSize = maxCacheFileSize; - this.bufferSize = bufferSize; + public void experimental_setSyncFileDescriptor(boolean syncFileDescriptor) { this.syncFileDescriptor = syncFileDescriptor; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java index 4bdee5ceeab..2466d5a0494 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -29,7 +29,7 @@ * has successfully completed. * *

    Atomic file guarantees file integrity by ensuring that a file has been completely written and - * sync'd to disk before removing its backup. As long as the backup file exists, the original file + * synced to disk before removing its backup. As long as the backup file exists, the original file * is considered to be invalid (left over from a previous attempt to write the file). * *

    Atomic file does not confer any file locking semantics. Do not use this class when the file 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 40b25c2b2e4..388aa29ce94 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 @@ -1087,6 +1087,10 @@ protected CodecMaxValues getCodecMaxValues( throws DecoderQueryException { int maxWidth = format.width; int maxHeight = format.height; + if (codecNeedsMaxVideoSizeResetWorkaround(codecInfo.name)) { + maxWidth = Math.max(maxWidth, 1920); + maxHeight = Math.max(maxHeight, 1089); + } int maxInputSize = getMaxInputSize(codecInfo, format); if (streamFormats.length == 1) { // The single entry in streamFormats must correspond to the format for which the codec is @@ -1274,6 +1278,18 @@ private static boolean deviceNeedsNoPostProcessWorkaround() { return "NVIDIA".equals(Util.MANUFACTURER); } + /** + * Returns whether the codec is known to have problems with the configuration for interlaced + * content and needs minimum values for the maximum video size to force reset the configuration. + * + *

    See https://github.com/google/ExoPlayer/issues/5003. + * + * @param name The name of the codec. + */ + private static boolean codecNeedsMaxVideoSizeResetWorkaround(String name) { + return "OMX.amlogic.avc.decoder.awesome".equals(name) && Util.SDK_INT <= 25; + } + /* * TODO: * @@ -1322,7 +1338,8 @@ protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) { // https://github.com/google/ExoPlayer/issues/4315, // https://github.com/google/ExoPlayer/issues/4419, // https://github.com/google/ExoPlayer/issues/4460, - // https://github.com/google/ExoPlayer/issues/4468. + // https://github.com/google/ExoPlayer/issues/4468, + // https://github.com/google/ExoPlayer/issues/5312. switch (Util.DEVICE) { case "1601": case "1713": @@ -1378,6 +1395,7 @@ protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) { case "HWBLN-H": case "HWCAM-H": case "HWVNS-H": + case "HWWAS-H": case "i9031": case "iball8735_9806": case "Infinix-X572": diff --git a/library/core/src/test/assets/mp4/sample.mp4.0.dump b/library/core/src/test/assets/mp4/sample.mp4.0.dump index efc804d48b1..b05d8250ab1 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.0.dump +++ b/library/core/src/test/assets/mp4/sample.mp4.0.dump @@ -147,7 +147,7 @@ track 0: data = length 530, hash C98BC6A8 sample 29: time = 934266 - flags = 0 + flags = 536870912 data = length 568, hash 4FE5C8EA track 1: format: @@ -352,6 +352,6 @@ track 1: data = length 229, hash FFF98DF0 sample 44: time = 1065678 - flags = 1 + flags = 536870913 data = length 6, hash 31B22286 tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample.mp4.1.dump b/library/core/src/test/assets/mp4/sample.mp4.1.dump index 10104b5e81c..84d86f8ccfc 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.1.dump +++ b/library/core/src/test/assets/mp4/sample.mp4.1.dump @@ -147,7 +147,7 @@ track 0: data = length 530, hash C98BC6A8 sample 29: time = 934266 - flags = 0 + flags = 536870912 data = length 568, hash 4FE5C8EA track 1: format: @@ -304,6 +304,6 @@ track 1: data = length 229, hash FFF98DF0 sample 32: time = 1065678 - flags = 1 + flags = 536870913 data = length 6, hash 31B22286 tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample.mp4.2.dump b/library/core/src/test/assets/mp4/sample.mp4.2.dump index 8af96be673e..9bbe8caa01d 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.2.dump +++ b/library/core/src/test/assets/mp4/sample.mp4.2.dump @@ -147,7 +147,7 @@ track 0: data = length 530, hash C98BC6A8 sample 29: time = 934266 - flags = 0 + flags = 536870912 data = length 568, hash 4FE5C8EA track 1: format: @@ -244,6 +244,6 @@ track 1: data = length 229, hash FFF98DF0 sample 17: time = 1065678 - flags = 1 + flags = 536870913 data = length 6, hash 31B22286 tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample.mp4.3.dump b/library/core/src/test/assets/mp4/sample.mp4.3.dump index f1259661edd..f210f277b3c 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.3.dump +++ b/library/core/src/test/assets/mp4/sample.mp4.3.dump @@ -147,7 +147,7 @@ track 0: data = length 530, hash C98BC6A8 sample 29: time = 934266 - flags = 0 + flags = 536870912 data = length 568, hash 4FE5C8EA track 1: format: @@ -184,6 +184,6 @@ track 1: data = length 229, hash FFF98DF0 sample 2: time = 1065678 - flags = 1 + flags = 536870913 data = length 6, hash 31B22286 tracksEnded = true diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntryTest.java new file mode 100644 index 00000000000..2f81836540c --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/MdtaMetadataEntryTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.mp4; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Test for {@link MdtaMetadataEntry}. */ +@RunWith(RobolectricTestRunner.class) +public final class MdtaMetadataEntryTest { + + @Test + public void testParcelable() { + MdtaMetadataEntry mdtaMetadataEntryToParcel = + new MdtaMetadataEntry("test", new byte[] {1, 2}, 3, 4); + + Parcel parcel = Parcel.obtain(); + mdtaMetadataEntryToParcel.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + MdtaMetadataEntry mdtaMetadataEntryFromParcel = + MdtaMetadataEntry.CREATOR.createFromParcel(parcel); + assertThat(mdtaMetadataEntryFromParcel).isEqualTo(mdtaMetadataEntryToParcel); + + parcel.recycle(); + } +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 5c9a933508e..ea5193eae16 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -46,6 +46,7 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -452,13 +453,22 @@ private static int[][] getGroupedAdaptationSetIndices(List adapta if (adaptationSetSwitchingProperty == null) { groupedAdaptationSetIndices[groupCount++] = new int[] {i}; } else { - String[] extraAdaptationSetIds = adaptationSetSwitchingProperty.value.split(","); + String[] extraAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ","); int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length]; adaptationSetIndices[0] = i; + int outputIndex = 1; for (int j = 0; j < extraAdaptationSetIds.length; j++) { - int extraIndex = idToIndexMap.get(Integer.parseInt(extraAdaptationSetIds[j])); - adaptationSetUsedFlags[extraIndex] = true; - adaptationSetIndices[1 + j] = extraIndex; + int extraIndex = + idToIndexMap.get( + Integer.parseInt(extraAdaptationSetIds[j]), /* valueIfKeyNotFound= */ -1); + if (extraIndex != -1) { + adaptationSetUsedFlags[extraIndex] = true; + adaptationSetIndices[outputIndex] = extraIndex; + outputIndex++; + } + } + if (outputIndex < adaptationSetIndices.length) { + adaptationSetIndices = Arrays.copyOf(adaptationSetIndices, outputIndex); } groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices; } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index c8de8f02b10..c65bfceb39a 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -635,7 +635,8 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { } @Override - public MediaPeriod createPeriod(MediaPeriodId periodId, Allocator allocator) { + public MediaPeriod createPeriod( + MediaPeriodId periodId, Allocator allocator, long startPositionUs) { int periodIndex = (Integer) periodId.periodUid - firstPeriodId; EventDispatcher periodEventDispatcher = createEventDispatcher(periodId, manifest.getPeriod(periodIndex).startMs); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 5e20fb769ce..3e51009f20e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -457,10 +457,10 @@ private long getSegmentNum( } private ArrayList getRepresentations() { - List manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets; + List manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets; ArrayList representations = new ArrayList<>(); for (int adaptationSetIndex : adaptationSetIndices) { - representations.addAll(manifestAdapationSets.get(adaptationSetIndex).representations); + representations.addAll(manifestAdaptationSets.get(adaptationSetIndex).representations); } return representations; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index a9b0c579ac6..2afd0416312 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -412,7 +412,7 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { EventDispatcher eventDispatcher = createEventDispatcher(id); return new HlsMediaPeriod( extractorFactory, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 65f47961878..242711431c3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -360,7 +360,7 @@ private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Stri /* initializationData= */ null, selectionFlags, language); - if (uri == null) { + if (isMediaTagMuxed(variants, uri)) { muxedAudioFormat = format; } else { audios.add(new HlsMasterPlaylist.HlsUrl(uri, format)); @@ -766,6 +766,20 @@ private static Pattern compileBooleanAttrPattern(String attribute) { return Pattern.compile(attribute + "=(" + BOOLEAN_FALSE + "|" + BOOLEAN_TRUE + ")"); } + private static boolean isMediaTagMuxed( + List variants, String mediaTagUri) { + if (mediaTagUri == null) { + return true; + } + // The URI attribute is defined, but it may match the uri of a variant. + for (int i = 0; i < variants.size(); i++) { + if (mediaTagUri.equals(variants.get(i).url)) { + return true; + } + } + return false; + } + private static class LineIterator { private final BufferedReader reader; diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index d03049efb33..9701171ce9e 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -134,6 +134,17 @@ public class HlsMasterPlaylistParserTest { + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"{$codecs}\"\n" + "http://example.com/{$tricky}\n"; + private static final String PLAYLIST_WITH_MULTIPLE_MUXED_MEDIA_TAGS = + "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"a\",NAME=\"audio_0\",DEFAULT=YES,URI=\"0/0.m3u8\"\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"b\",NAME=\"audio_0\",DEFAULT=YES,URI=\"1/1.m3u8\"\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=140800,CODECS=\"mp4a.40.2\",AUDIO=\"a\"\n" + + "0/0.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=281600,CODECS=\"mp4a.40.2\",AUDIO=\"b\"\n" + + "1/1.m3u8\n"; + @Test public void testParseMasterPlaylist() throws IOException { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); @@ -271,6 +282,14 @@ public void testVariableSubstitution() throws IOException { assertThat(variant.url).isEqualTo("http://example.com/This/{$nested}/reference/shouldnt/work"); } + @Test + public void testMultipleMuxedMediaTags() throws IOException { + HlsMasterPlaylist playlistWithMultipleMuxedMediaTags = + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_MULTIPLE_MUXED_MEDIA_TAGS); + assertThat(playlistWithMultipleMuxedMediaTags.variants).hasSize(2); + assertThat(playlistWithMultipleMuxedMediaTags.audios).isEmpty(); + } + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index 9ac376efad4..7c76dba7491 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -61,14 +61,13 @@ public SsChunkSource createChunkSource( SsManifest manifest, int elementIndex, TrackSelection trackSelection, - TrackEncryptionBox[] trackEncryptionBoxes, @Nullable TransferListener transferListener) { DataSource dataSource = dataSourceFactory.createDataSource(); if (transferListener != null) { dataSource.addTransferListener(transferListener); } - return new DefaultSsChunkSource(manifestLoaderErrorThrower, manifest, elementIndex, - trackSelection, dataSource, trackEncryptionBoxes); + return new DefaultSsChunkSource( + manifestLoaderErrorThrower, manifest, elementIndex, trackSelection, dataSource); } } @@ -90,15 +89,13 @@ public SsChunkSource createChunkSource( * @param streamElementIndex The index of the stream element in the manifest. * @param trackSelection The track selection. * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param trackEncryptionBoxes Track encryption boxes for the stream. */ public DefaultSsChunkSource( LoaderErrorThrower manifestLoaderErrorThrower, SsManifest manifest, int streamElementIndex, TrackSelection trackSelection, - DataSource dataSource, - TrackEncryptionBox[] trackEncryptionBoxes) { + DataSource dataSource) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; this.streamElementIndex = streamElementIndex; @@ -110,6 +107,8 @@ public DefaultSsChunkSource( for (int i = 0; i < extractorWrappers.length; i++) { int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i); Format format = streamElement.formats[manifestTrackIndex]; + TrackEncryptionBox[] trackEncryptionBoxes = + format.drmInitData != null ? manifest.protectionElement.trackEncryptionBoxes : null; int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : 0; Track track = new Track(manifestTrackIndex, streamElement.type, streamElement.timescale, C.TIME_UNSET, manifest.durationUs, format, Track.TRANSFORMATION_NONE, diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java index f333a6f92c3..4940f1592fe 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.source.chunk.ChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -38,7 +37,6 @@ interface Factory { * @param manifest The initial manifest. * @param streamElementIndex The index of the corresponding stream element in the manifest. * @param trackSelection The track selection. - * @param trackEncryptionBoxes Track encryption boxes for the stream. * @param transferListener The transfer listener which should be informed of any data transfers. * May be null if no listener is available. * @return The created {@link SsChunkSource}. @@ -48,7 +46,6 @@ SsChunkSource createChunkSource( SsManifest manifest, int streamElementIndex, TrackSelection trackSelection, - TrackEncryptionBox[] trackEncryptionBoxes, @Nullable TransferListener transferListener); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 14b54bc471e..d3518c0a35e 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -29,7 +29,6 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; @@ -44,8 +43,6 @@ /* package */ final class SsMediaPeriod implements MediaPeriod, SequenceableLoader.Callback> { - private static final int INITIALIZATION_VECTOR_SIZE = 8; - private final SsChunkSource.Factory chunkSourceFactory; private final @Nullable TransferListener transferListener; private final LoaderErrorThrower manifestLoaderErrorThrower; @@ -53,7 +50,6 @@ private final EventDispatcher eventDispatcher; private final Allocator allocator; private final TrackGroupArray trackGroups; - private final TrackEncryptionBox[] trackEncryptionBoxes; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private @Nullable Callback callback; @@ -71,6 +67,7 @@ public SsMediaPeriod( EventDispatcher eventDispatcher, LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) { + this.manifest = manifest; this.chunkSourceFactory = chunkSourceFactory; this.transferListener = transferListener; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; @@ -78,18 +75,7 @@ public SsMediaPeriod( this.eventDispatcher = eventDispatcher; this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; - trackGroups = buildTrackGroups(manifest); - ProtectionElement protectionElement = manifest.protectionElement; - if (protectionElement != null) { - byte[] keyId = getProtectionElementKeyId(protectionElement.data); - // We assume pattern encryption does not apply. - trackEncryptionBoxes = new TrackEncryptionBox[] { - new TrackEncryptionBox(true, null, INITIALIZATION_VECTOR_SIZE, keyId, 0, 0, null)}; - } else { - trackEncryptionBoxes = null; - } - this.manifest = manifest; sampleStreams = newSampleStreamArray(0); compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); @@ -229,7 +215,6 @@ private ChunkSampleStream buildSampleStream(TrackSelection select manifest, streamElementIndex, selection, - trackEncryptionBoxes, transferListener); return new ChunkSampleStream<>( manifest.streamElements[streamElementIndex].type, @@ -277,5 +262,4 @@ private static void swap(byte[] data, int firstPosition, int secondPosition) { data[firstPosition] = data[secondPosition]; data[secondPosition] = temp; } - } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 103a52a55a0..d025f8fa3a8 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -533,7 +533,7 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { EventDispatcher eventDispatcher = createEventDispatcher(id); SsMediaPeriod period = new SsMediaPeriod( diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index 2c508f0fde4..cfb772a86bb 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -18,6 +18,7 @@ import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.offline.FilterableManifest; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.util.Assertions; @@ -41,10 +42,12 @@ public static class ProtectionElement { public final UUID uuid; public final byte[] data; + public final TrackEncryptionBox[] trackEncryptionBoxes; - public ProtectionElement(UUID uuid, byte[] data) { + public ProtectionElement(UUID uuid, byte[] data, TrackEncryptionBox[] trackEncryptionBoxes) { this.uuid = uuid; this.data = data; + this.trackEncryptionBoxes = trackEncryptionBoxes; } } 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 3d5ade403ab..4c1c6ee0cc9 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 @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; +import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.upstream.ParsingLoadable; @@ -397,9 +398,10 @@ private static class ProtectionParser extends ElementParser { public static final String TAG = "Protection"; public static final String TAG_PROTECTION_HEADER = "ProtectionHeader"; - public static final String KEY_SYSTEM_ID = "SystemID"; + private static final int INITIALIZATION_VECTOR_SIZE = 8; + private boolean inProtectionHeader; private UUID uuid; private byte[] initData; @@ -439,7 +441,44 @@ public void parseEndTag(XmlPullParser parser) { @Override public Object build() { - return new ProtectionElement(uuid, PsshAtomUtil.buildPsshAtom(uuid, initData)); + return new ProtectionElement( + uuid, PsshAtomUtil.buildPsshAtom(uuid, initData), buildTrackEncryptionBoxes(initData)); + } + + private static TrackEncryptionBox[] buildTrackEncryptionBoxes(byte[] initData) { + return new TrackEncryptionBox[] { + new TrackEncryptionBox( + /* isEncrypted= */ true, + /* schemeType= */ null, + INITIALIZATION_VECTOR_SIZE, + getProtectionElementKeyId(initData), + /* defaultEncryptedBlocks= */ 0, + /* defaultClearBlocks= */ 0, + /* defaultInitializationVector= */ null) + }; + } + + private static byte[] getProtectionElementKeyId(byte[] initData) { + StringBuilder initDataStringBuilder = new StringBuilder(); + for (int i = 0; i < initData.length; i += 2) { + initDataStringBuilder.append((char) initData[i]); + } + String initDataString = initDataStringBuilder.toString(); + String keyIdString = + initDataString.substring( + initDataString.indexOf("") + 5, initDataString.indexOf("")); + byte[] keyId = Base64.decode(keyIdString, Base64.DEFAULT); + swap(keyId, 0, 3); + swap(keyId, 1, 2); + swap(keyId, 4, 5); + swap(keyId, 6, 7); + return keyId; + } + + private static void swap(byte[] data, int firstPosition, int secondPosition) { + byte temp = data[firstPosition]; + data[firstPosition] = data[secondPosition]; + data[secondPosition] = temp; } private static String stripCurlyBraces(String uuidString) { diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java index 5125beff1c1..88830dde6af 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; @@ -42,7 +43,7 @@ public final class SsDownloadHelper extends DownloadHelper { private @MonotonicNonNull SsManifest manifest; public SsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { - this.uri = uri; + this.uri = SsUtil.fixManifestUri(uri);; this.manifestDataSourceFactory = manifestDataSourceFactory; } diff --git a/library/smoothstreaming/src/test/assets/sample_ismc_1 b/library/smoothstreaming/src/test/assets/sample_ismc_1 index 25a37d65b4f..1d279d0a67c 100644 --- a/library/smoothstreaming/src/test/assets/sample_ismc_1 +++ b/library/smoothstreaming/src/test/assets/sample_ismc_1 @@ -3,7 +3,7 @@ Duration="2300000000" TimeScale="10000000"> - + fgMAAAEAAQB0AzwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQgBhAFUATQBPAEcAYwBzAGgAVQBDAEQAZAB3ADMANABZAGMAawBmAFoAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBnADcATgBhAFIARABJAEkATwA5ADAAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAcwA6AC8ALwBUAC0ATwBOAEwASQBOAEUALgBEAFUATQBNAFkALQBTAEUAUgBWAEUAUgAvAEEAcgB0AGUAbQBpAHMATABpAGMAZQBuAHMAZQBTAGUAcgB2AGUAcgAvAFAAbABhAHkAUgBlAGEAZAB5AE0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwAQwBJAEQAPgAxADcANQA4ADIANgA8AC8AQwBJAEQAPgA8AEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4ANwAuADEALgAxADUANgA1AC4ANAA8AC8ASQBJAFMAXwBEAFIATQBfAFYARQBSAFMASQBPAE4APgA8AC8AQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A diff --git a/library/smoothstreaming/src/test/assets/sample_ismc_2 b/library/smoothstreaming/src/test/assets/sample_ismc_2 index 5875a181835..7f2a53036f0 100644 --- a/library/smoothstreaming/src/test/assets/sample_ismc_2 +++ b/library/smoothstreaming/src/test/assets/sample_ismc_2 @@ -3,7 +3,7 @@ Duration="2300000000" TimeScale="10000000"> - + fgMAAAEAAQB0AzwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQgBhAFUATQBPAEcAYwBzAGgAVQBDAEQAZAB3ADMANABZAGMAawBmAFoAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBnADcATgBhAFIARABJAEkATwA5ADAAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAcwA6AC8ALwBUAC0ATwBOAEwASQBOAEUALgBEAFUATQBNAFkALQBTAEUAUgBWAEUAUgAvAEEAcgB0AGUAbQBpAHMATABpAGMAZQBuAHMAZQBTAGUAcgB2AGUAcgAvAFAAbABhAHkAUgBlAGEAZAB5AE0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwAQwBJAEQAPgAxADcANQA4ADIANgA8AC8AQwBJAEQAPgA8AEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4ANwAuADEALgAxADUANgA1AC4ANAA8AC8ASQBJAFMAXwBEAFIATQBfAFYARQBSAFMASQBPAE4APgA8AC8AQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java index dc8d6754f59..b692d94c186 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; @@ -36,7 +37,7 @@ public class SsManifestTest { private static final ProtectionElement DUMMY_PROTECTION_ELEMENT = - new ProtectionElement(C.WIDEVINE_UUID, new byte[] {0, 1, 2}); + new ProtectionElement(C.WIDEVINE_UUID, new byte[0], new TrackEncryptionBox[0]); @Test public void testCopy() throws Exception { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 8c7c507f924..da2081db31d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -137,23 +137,40 @@ protected String getPlayerStateString() { /** Returns a string containing video debugging information. */ protected String getVideoString() { Format format = player.getVideoFormat(); - if (format == null) { + DecoderCounters decoderCounters = player.getVideoDecoderCounters(); + if (format == null || decoderCounters == null) { return ""; } - return "\n" + format.sampleMimeType + "(id:" + format.id + " r:" + format.width + "x" - + format.height + getPixelAspectRatioString(format.pixelWidthHeightRatio) - + getDecoderCountersBufferCountString(player.getVideoDecoderCounters()) + ")"; + return "\n" + + format.sampleMimeType + + "(id:" + + format.id + + " r:" + + format.width + + "x" + + format.height + + getPixelAspectRatioString(format.pixelWidthHeightRatio) + + getDecoderCountersBufferCountString(decoderCounters) + + ")"; } /** Returns a string containing audio debugging information. */ protected String getAudioString() { Format format = player.getAudioFormat(); - if (format == null) { + DecoderCounters decoderCounters = player.getAudioDecoderCounters(); + if (format == null || decoderCounters == null) { return ""; } - return "\n" + format.sampleMimeType + "(id:" + format.id + " hz:" + format.sampleRate + " ch:" + return "\n" + + format.sampleMimeType + + "(id:" + + format.id + + " hz:" + + format.sampleRate + + " ch:" + format.channelCount - + getDecoderCountersBufferCountString(player.getAudioDecoderCounters()) + ")"; + + getDecoderCountersBufferCountString(decoderCounters) + + ")"; } private static String getDecoderCountersBufferCountString(DecoderCounters counters) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 47025d9bba1..7cbe52d4046 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -125,6 +125,18 @@ public interface MediaDescriptionAdapter { @Nullable String getCurrentContentText(Player player); + /** + * Gets the content sub text for the current media item. + * + *

    See {@link NotificationCompat.Builder#setSubText(CharSequence)}. + * + * @param player The {@link Player} for which a notification is being built. + */ + @Nullable + default String getCurrentSubText(Player player) { + return null; + } + /** * Gets the large icon for the current media item. * @@ -832,6 +844,7 @@ protected Notification createNotification(Player player, @Nullable Bitmap largeI // Set media specific notification properties from MediaDescriptionAdapter. builder.setContentTitle(mediaDescriptionAdapter.getCurrentContentTitle(player)); builder.setContentText(mediaDescriptionAdapter.getCurrentContentText(player)); + builder.setSubText(mediaDescriptionAdapter.getCurrentSubText(player)); if (largeIcon == null) { largeIcon = mediaDescriptionAdapter.getCurrentLargeIcon( 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 88eabfed074..83f5b70cbb3 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 @@ -679,8 +679,9 @@ public void setShutterBackgroundColor(int color) { /** * Sets whether the currently displayed video frame or media artwork is kept visible when the * player is reset. A player reset is defined to mean the player being re-prepared with different - * media, {@link Player#stop(boolean)} being called with {@code reset=true}, or the player being - * replaced or cleared by calling {@link #setPlayer(Player)}. + * media, the player transitioning to unprepared media, {@link Player#stop(boolean)} being called + * with {@code reset=true}, or the player being replaced or cleared by calling {@link + * #setPlayer(Player)}. * *

    If enabled, the currently displayed video frame or media artwork will be kept visible until * the player set on the view has been successfully prepared with new media and loaded enough of diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 1f0c0c1a406..999372b90ac 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -116,7 +116,7 @@ public void maybeThrowSourceInfoRefreshError() throws IOException { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { assertThat(preparedSource).isTrue(); assertThat(releasedSource).isFalse(); int periodIndex = timeline.getIndexOfPeriod(id.periodUid); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 70e7669dfb2..e6fb5bc5f38 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -142,15 +142,28 @@ public Timeline prepareSource() throws IOException { } /** - * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator)} on the playback - * thread, asserting that a non-null {@link MediaPeriod} is returned. + * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator, long)} with a zero + * start position on the playback thread, asserting that a non-null {@link MediaPeriod} is + * returned. * * @param periodId The id of the period to create. * @return The created {@link MediaPeriod}. */ public MediaPeriod createPeriod(final MediaPeriodId periodId) { + return createPeriod(periodId, /* startPositionUs= */ 0); + } + + /** + * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator, long)} on the + * playback thread, asserting that a non-null {@link MediaPeriod} is returned. + * + * @param periodId The id of the period to create. + * @return The created {@link MediaPeriod}. + */ + public MediaPeriod createPeriod(final MediaPeriodId periodId, long startPositionUs) { final MediaPeriod[] holder = new MediaPeriod[1]; - runOnPlaybackThread(() -> holder[0] = mediaSource.createPeriod(periodId, allocator)); + runOnPlaybackThread( + () -> holder[0] = mediaSource.createPeriod(periodId, allocator, startPositionUs)); assertThat(holder[0]).isNotNull(); return holder[0]; }