Commit 1c4ea26f by Oliver Woodman Committed by GitHub

Merge pull request #5388 from google/dev-v2-r2.9.4

r2.9.4
parents 71f72c59 200c877d
Showing with 1029 additions and 316 deletions
...@@ -27,6 +27,8 @@ repository and depend on the modules locally. ...@@ -27,6 +27,8 @@ repository and depend on the modules locally.
### From JCenter ### ### From JCenter ###
#### 1. Add repositories ####
The easiest way to get started using ExoPlayer is to add it as a gradle 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 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: included in the `build.gradle` file in the root of your project:
...@@ -38,6 +40,8 @@ repositories { ...@@ -38,6 +40,8 @@ repositories {
} }
``` ```
#### 2. Add ExoPlayer module dependencies ####
Next add a dependency in the `build.gradle` file of your app module. The Next add a dependency in the `build.gradle` file of your app module. The
following will add a dependency to the full library: following will add a dependency to the full library:
...@@ -45,15 +49,7 @@ 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' implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
``` ```
where `2.X.X` is your preferred version. If not enabled already, you also need where `2.X.X` is your preferred version.
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
}
```
As an alternative to the full library, you can depend on only the library 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 modules that you actually need. For example the following will add dependencies
...@@ -87,6 +83,32 @@ JCenter can be found on [Bintray][]. ...@@ -87,6 +83,32 @@ JCenter can be found on [Bintray][].
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
[Bintray]: https://bintray.com/google/exoplayer [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 ### ### Locally ###
Cloning the repository and depending on the modules locally is required when Cloning the repository and depending on the modules locally is required when
......
# Release notes # # 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 ### ### 2.9.3 ###
* Captions: Support PNG subtitles in SMPTE-TT * Captions: Support PNG subtitles in SMPTE-TT
......
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.9.3' releaseVersion = '2.9.4'
releaseVersionCode = 2009003 releaseVersionCode = 2009004
// Important: ExoPlayer specifies a minSdkVersion of 14 because various // Important: ExoPlayer specifies a minSdkVersion of 14 because various
// components provided by the library may be of use on older devices. // components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality provided // However, please note that the core media playback functionality provided
......
...@@ -41,6 +41,7 @@ import com.google.android.exoplayer2.ui.PlayerControlView; ...@@ -41,6 +41,7 @@ import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.gms.cast.framework.CastContext; 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}. * An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}.
...@@ -61,7 +62,20 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, ...@@ -61,7 +62,20 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Getting the cast context later than onStart can cause device discovery not to take place. // Getting the cast context later than onStart can cause device discovery not to take place.
try {
castContext = CastContext.getSharedInstance(this); 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); setContentView(R.layout.main_activity);
...@@ -91,6 +105,10 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, ...@@ -91,6 +105,10 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
if (castContext == null) {
// There is no Cast context to work with. Do nothing.
return;
}
playerManager = playerManager =
PlayerManager.createPlayerManager( PlayerManager.createPlayerManager(
/* queuePositionListener= */ this, /* queuePositionListener= */ this,
...@@ -104,6 +122,10 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, ...@@ -104,6 +122,10 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
if (castContext == null) {
// Nothing to release.
return;
}
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount()); mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
mediaQueueList.setAdapter(null); mediaQueueList.setAdapter(null);
playerManager.release(); playerManager.release();
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:textSize="20sp"
android:gravity="center"
android:text="@string/cast_context_error"/>
</LinearLayout>
...@@ -22,4 +22,6 @@ ...@@ -22,4 +22,6 @@
<string name="sample_list_dialog_title">Add samples</string> <string name="sample_list_dialog_title">Add samples</string>
<string name="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
</resources> </resources>
...@@ -31,7 +31,9 @@ android { ...@@ -31,7 +31,9 @@ android {
} }
dependencies { 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-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
testImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils')
......
...@@ -37,6 +37,10 @@ import java.util.List; ...@@ -37,6 +37,10 @@ import java.util.List;
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536; private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2; 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 String codecName;
private final @Nullable byte[] extraData; private final @Nullable byte[] extraData;
private final @C.Encoding int encoding; private final @C.Encoding int encoding;
...@@ -106,8 +110,14 @@ import java.util.List; ...@@ -106,8 +110,14 @@ import java.util.List;
int inputSize = inputData.limit(); int inputSize = inputData.limit();
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize); int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
if (result < 0) { if (result == DECODER_ERROR_INVALID_DATA) {
return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result); // 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) { if (!hasOutputFormat) {
channelCount = ffmpegGetChannelCount(nativeContext); channelCount = ffmpegGetChannelCount(nativeContext);
......
...@@ -63,6 +63,10 @@ static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16; ...@@ -63,6 +63,10 @@ static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16;
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT. // Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT; 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. * 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, ...@@ -79,7 +83,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
/** /**
* Decodes the packet into the output buffer, returning the number of bytes * 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, int decodePacket(AVCodecContext *context, AVPacket *packet,
uint8_t *outputBuffer, int outputSize); uint8_t *outputBuffer, int outputSize);
...@@ -238,6 +242,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData, ...@@ -238,6 +242,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
context->channels = rawChannelCount; context->channels = rawChannelCount;
context->channel_layout = av_get_default_channel_layout(rawChannelCount); context->channel_layout = av_get_default_channel_layout(rawChannelCount);
} }
context->err_recognition = AV_EF_IGNORE_ERR;
int result = avcodec_open2(context, codec, NULL); int result = avcodec_open2(context, codec, NULL);
if (result < 0) { if (result < 0) {
logError("avcodec_open2", result); logError("avcodec_open2", result);
...@@ -254,7 +259,8 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, ...@@ -254,7 +259,8 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
result = avcodec_send_packet(context, packet); result = avcodec_send_packet(context, packet);
if (result) { if (result) {
logError("avcodec_send_packet", 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. // Dequeue output data until it runs out.
......
...@@ -32,7 +32,8 @@ android { ...@@ -32,7 +32,8 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'com.android.support:support-annotations:' + supportLibraryVersion 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 { ext {
......
...@@ -597,6 +597,8 @@ public final class ImaAdsLoader ...@@ -597,6 +597,8 @@ public final class ImaAdsLoader
adsManager.destroy(); adsManager.destroy();
adsManager = null; adsManager = null;
} }
adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this);
adsLoader.removeAdErrorListener(/* adErrorListener= */ this);
imaPausedContent = false; imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE; imaAdState = IMA_AD_STATE_NONE;
pendingAdLoadError = null; pendingAdLoadError = null;
......
...@@ -97,8 +97,8 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn ...@@ -97,8 +97,8 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
return adsMediaSource.createPeriod(id, allocator); return adsMediaSource.createPeriod(id, allocator, startPositionUs);
} }
@Override @Override
......
...@@ -64,14 +64,17 @@ import java.util.Set; ...@@ -64,14 +64,17 @@ import java.util.Set;
}; };
} }
@Override
public int getVastMediaWidth() { public int getVastMediaWidth() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public int getVastMediaHeight() { public int getVastMediaHeight() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public int getVastMediaBitrate() { public int getVastMediaBitrate() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
......
...@@ -39,7 +39,7 @@ either instantiated and injected from application code, or obtained from ...@@ -39,7 +39,7 @@ either instantiated and injected from application code, or obtained from
instances of `DataSource.Factory` that are instantiated and injected from instances of `DataSource.Factory` that are instantiated and injected from
application code. 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 available. Hence if your application is using `DefaultDataSource` or
`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as `DefaultDataSourceFactory`, adding support for RTMP streams is as simple as
adding a dependency to the RTMP extension as described above. No changes to your adding a dependency to the RTMP extension as described above. No changes to your
......
...@@ -460,8 +460,8 @@ public final class C { ...@@ -460,8 +460,8 @@ public final class C {
/** /**
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link * 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 * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE},
* {@link #BUFFER_FLAG_DECODE_ONLY}. * {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
...@@ -470,6 +470,7 @@ public final class C { ...@@ -470,6 +470,7 @@ public final class C {
value = { value = {
BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_KEY_FRAME,
BUFFER_FLAG_END_OF_STREAM, BUFFER_FLAG_END_OF_STREAM,
BUFFER_FLAG_LAST_SAMPLE,
BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_ENCRYPTED,
BUFFER_FLAG_DECODE_ONLY BUFFER_FLAG_DECODE_ONLY
}) })
...@@ -482,6 +483,8 @@ public final class C { ...@@ -482,6 +483,8 @@ public final class C {
* Flag for empty buffers that signal that the end of the stream was reached. * 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; 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. */ /** Indicates that a buffer is (at least partially) encrypted. */
public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000 public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
/** Indicates that a buffer should be decoded but not rendered. */ /** Indicates that a buffer should be decoded but not rendered. */
...@@ -896,6 +899,26 @@ public final class C { ...@@ -896,6 +899,26 @@ public final class C {
*/ */
public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL; 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. * Priority for media playback.
* *
......
...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** 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. // 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}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // 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. * The version of the library expressed as an integer, for example 1002003.
...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // 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} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -1181,6 +1181,37 @@ public final class Format implements Parcelable { ...@@ -1181,6 +1181,37 @@ public final class Format implements Parcelable {
metadata); 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) { public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {
return new Format( return new Format(
id, id,
......
...@@ -79,7 +79,7 @@ import com.google.android.exoplayer2.util.Log; ...@@ -79,7 +79,7 @@ import com.google.android.exoplayer2.util.Log;
this.info = info; this.info = info;
sampleStreams = new SampleStream[rendererCapabilities.length]; sampleStreams = new SampleStream[rendererCapabilities.length];
mayRetainStreamFlags = new boolean[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) { if (info.id.endPositionUs != C.TIME_END_OF_SOURCE) {
mediaPeriod = mediaPeriod =
new ClippingMediaPeriod( new ClippingMediaPeriod(
......
...@@ -94,25 +94,25 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -94,25 +94,25 @@ public class SimpleExoPlayer extends BasePlayer
private final AudioFocusManager audioFocusManager; private final AudioFocusManager audioFocusManager;
private Format videoFormat; @Nullable private Format videoFormat;
private Format audioFormat; @Nullable private Format audioFormat;
private Surface surface; @Nullable private Surface surface;
private boolean ownsSurface; private boolean ownsSurface;
private @C.VideoScalingMode int videoScalingMode; private @C.VideoScalingMode int videoScalingMode;
private SurfaceHolder surfaceHolder; @Nullable private SurfaceHolder surfaceHolder;
private TextureView textureView; @Nullable private TextureView textureView;
private int surfaceWidth; private int surfaceWidth;
private int surfaceHeight; private int surfaceHeight;
private DecoderCounters videoDecoderCounters; @Nullable private DecoderCounters videoDecoderCounters;
private DecoderCounters audioDecoderCounters; @Nullable private DecoderCounters audioDecoderCounters;
private int audioSessionId; private int audioSessionId;
private AudioAttributes audioAttributes; private AudioAttributes audioAttributes;
private float audioVolume; private float audioVolume;
private MediaSource mediaSource; @Nullable private MediaSource mediaSource;
private List<Cue> currentCues; private List<Cue> currentCues;
private VideoFrameMetadataListener videoFrameMetadataListener; @Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
private CameraMotionListener cameraMotionListener; @Nullable private CameraMotionListener cameraMotionListener;
private boolean hasNotifiedFullWrongThreadWarning; private boolean hasNotifiedFullWrongThreadWarning;
/** /**
...@@ -558,30 +558,26 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -558,30 +558,26 @@ public class SimpleExoPlayer extends BasePlayer
setPlaybackParameters(playbackParameters); 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() { public Format getVideoFormat() {
return videoFormat; 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() { public Format getAudioFormat() {
return audioFormat; 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() { public DecoderCounters getVideoDecoderCounters() {
return videoDecoderCounters; 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() { public DecoderCounters getAudioDecoderCounters() {
return audioDecoderCounters; return audioDecoderCounters;
} }
...@@ -1048,7 +1044,8 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1048,7 +1044,8 @@ public class SimpleExoPlayer extends BasePlayer
} }
@Override @Override
public @Nullable Object getCurrentManifest() { @Nullable
public Object getCurrentManifest() {
verifyApplicationThread(); verifyApplicationThread();
return player.getCurrentManifest(); return player.getCurrentManifest();
} }
......
...@@ -488,7 +488,10 @@ public class AnalyticsCollector ...@@ -488,7 +488,10 @@ public class AnalyticsCollector
@Override @Override
public final void onPlayerError(ExoPlaybackException error) { public final void onPlayerError(ExoPlaybackException error) {
EventTime eventTime = generatePlayingMediaPeriodEventTime(); EventTime eventTime =
error.type == ExoPlaybackException.TYPE_SOURCE
? generateLoadingMediaPeriodEventTime()
: generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) { for (AnalyticsListener listener : listeners) {
listener.onPlayerError(eventTime, error); listener.onPlayerError(eventTime, error);
} }
......
...@@ -366,7 +366,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -366,7 +366,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
if (outputBuffer == null) { if (outputBuffer == null) {
return false; return false;
} }
if (outputBuffer.skippedOutputBufferCount > 0) {
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
audioSink.handleDiscontinuity();
}
} }
if (outputBuffer.isEndOfStream()) { if (outputBuffer.isEndOfStream()) {
......
...@@ -191,7 +191,11 @@ public final class MatroskaExtractor implements Extractor { ...@@ -191,7 +191,11 @@ public final class MatroskaExtractor implements Extractor {
private static final int ID_CUE_CLUSTER_POSITION = 0xF1; private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
private static final int ID_LANGUAGE = 0x22B59C; private static final int ID_LANGUAGE = 0x22B59C;
private static final int ID_PROJECTION = 0x7670; 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_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_STEREO_MODE = 0x53B8;
private static final int ID_COLOUR = 0x55B0; private static final int ID_COLOUR = 0x55B0;
private static final int ID_COLOUR_RANGE = 0x55B9; private static final int ID_COLOUR_RANGE = 0x55B9;
...@@ -760,6 +764,24 @@ public final class MatroskaExtractor implements Extractor { ...@@ -760,6 +764,24 @@ public final class MatroskaExtractor implements Extractor {
case ID_MAX_FALL: case ID_MAX_FALL:
currentTrack.maxFrameAverageLuminance = (int) value; currentTrack.maxFrameAverageLuminance = (int) value;
break; 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: default:
break; break;
} }
...@@ -803,6 +825,15 @@ public final class MatroskaExtractor implements Extractor { ...@@ -803,6 +825,15 @@ public final class MatroskaExtractor implements Extractor {
case ID_LUMNINANCE_MIN: case ID_LUMNINANCE_MIN:
currentTrack.minMasteringLuminance = (float) value; currentTrack.minMasteringLuminance = (float) value;
break; 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: default:
break; break;
} }
...@@ -1465,6 +1496,7 @@ public final class MatroskaExtractor implements Extractor { ...@@ -1465,6 +1496,7 @@ public final class MatroskaExtractor implements Extractor {
case ID_COLOUR_PRIMARIES: case ID_COLOUR_PRIMARIES:
case ID_MAX_CLL: case ID_MAX_CLL:
case ID_MAX_FALL: case ID_MAX_FALL:
case ID_PROJECTION_TYPE:
return TYPE_UNSIGNED_INT; return TYPE_UNSIGNED_INT;
case ID_DOC_TYPE: case ID_DOC_TYPE:
case ID_NAME: case ID_NAME:
...@@ -1491,6 +1523,9 @@ public final class MatroskaExtractor implements Extractor { ...@@ -1491,6 +1523,9 @@ public final class MatroskaExtractor implements Extractor {
case ID_WHITE_POINT_CHROMATICITY_Y: case ID_WHITE_POINT_CHROMATICITY_Y:
case ID_LUMNINANCE_MAX: case ID_LUMNINANCE_MAX:
case ID_LUMNINANCE_MIN: case ID_LUMNINANCE_MIN:
case ID_PROJECTION_POSE_YAW:
case ID_PROJECTION_POSE_PITCH:
case ID_PROJECTION_POSE_ROLL:
return TYPE_FLOAT; return TYPE_FLOAT;
default: default:
return TYPE_UNKNOWN; return TYPE_UNKNOWN;
...@@ -1631,6 +1666,10 @@ public final class MatroskaExtractor implements Extractor { ...@@ -1631,6 +1666,10 @@ public final class MatroskaExtractor implements Extractor {
public int displayWidth = Format.NO_VALUE; public int displayWidth = Format.NO_VALUE;
public int displayHeight = Format.NO_VALUE; public int displayHeight = Format.NO_VALUE;
public int displayUnit = DISPLAY_UNIT_PIXELS; 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; public byte[] projectionData = null;
@C.StereoMode @C.StereoMode
public int stereoMode = Format.NO_VALUE; public int stereoMode = Format.NO_VALUE;
...@@ -1850,6 +1889,21 @@ public final class MatroskaExtractor implements Extractor { ...@@ -1850,6 +1889,21 @@ public final class MatroskaExtractor implements Extractor {
} else if ("htc_video_rotA-270".equals(name)) { } else if ("htc_video_rotA-270".equals(name)) {
rotationDegrees = 270; 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 =
Format.createVideoSampleFormat( Format.createVideoSampleFormat(
Integer.toString(trackId), Integer.toString(trackId),
......
...@@ -22,8 +22,8 @@ import java.util.ArrayList; ...@@ -22,8 +22,8 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@SuppressWarnings("ConstantField") @SuppressWarnings({"ConstantField", "ConstantCaseForConstants"})
/* package*/ abstract class Atom { /* package */ abstract class Atom {
/** /**
* Size of an atom header, in bytes. * Size of an atom header, in bytes.
...@@ -130,6 +130,7 @@ import java.util.List; ...@@ -130,6 +130,7 @@ import java.util.List;
public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb"); public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb");
public static final int TYPE_udta = Util.getIntegerCodeForString("udta"); public static final int TYPE_udta = Util.getIntegerCodeForString("udta");
public static final int TYPE_meta = Util.getIntegerCodeForString("meta"); 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_ilst = Util.getIntegerCodeForString("ilst");
public static final int TYPE_mean = Util.getIntegerCodeForString("mean"); public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
public static final int TYPE_name = Util.getIntegerCodeForString("name"); public static final int TYPE_name = Util.getIntegerCodeForString("name");
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.mp4; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.mp4;
import static com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType; import static com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType;
import android.support.annotation.Nullable;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
...@@ -39,7 +40,7 @@ import java.util.Collections; ...@@ -39,7 +40,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */ /** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */
@SuppressWarnings("ConstantField") @SuppressWarnings({"ConstantField", "ConstantCaseForConstants"})
/* package */ final class AtomParsers { /* package */ final class AtomParsers {
private static final String TAG = "AtomParsers"; private static final String TAG = "AtomParsers";
...@@ -51,6 +52,7 @@ import java.util.List; ...@@ -51,6 +52,7 @@ import java.util.List;
private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); private static final int TYPE_subt = Util.getIntegerCodeForString("subt");
private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
private static final int TYPE_meta = Util.getIntegerCodeForString("meta"); 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 * The threshold number of samples to trim from the start/end of an audio track when applying an
...@@ -77,7 +79,7 @@ import java.util.List; ...@@ -77,7 +79,7 @@ import java.util.List;
DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime) DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime)
throws ParserException { throws ParserException {
Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); 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) { if (trackType == C.TRACK_TYPE_UNKNOWN) {
return null; return null;
} }
...@@ -485,6 +487,7 @@ import java.util.List; ...@@ -485,6 +487,7 @@ import java.util.List;
* @param isQuickTime True for QuickTime media. False otherwise. * @param isQuickTime True for QuickTime media. False otherwise.
* @return Parsed metadata, or null. * @return Parsed metadata, or null.
*/ */
@Nullable
public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {
if (isQuickTime) { if (isQuickTime) {
// Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
...@@ -499,14 +502,69 @@ import java.util.List; ...@@ -499,14 +502,69 @@ import java.util.List;
int atomType = udtaData.readInt(); int atomType = udtaData.readInt();
if (atomType == Atom.TYPE_meta) { if (atomType == Atom.TYPE_meta) {
udtaData.setPosition(atomPosition); 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; 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<Metadata.Entry> 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); meta.skipBytes(Atom.FULL_HEADER_SIZE);
while (meta.getPosition() < limit) { while (meta.getPosition() < limit) {
int atomPosition = meta.getPosition(); int atomPosition = meta.getPosition();
...@@ -516,11 +574,12 @@ import java.util.List; ...@@ -516,11 +574,12 @@ import java.util.List;
meta.setPosition(atomPosition); meta.setPosition(atomPosition);
return parseIlst(meta, atomPosition + atomSize); return parseIlst(meta, atomPosition + atomSize);
} }
meta.skipBytes(atomSize - Atom.HEADER_SIZE); meta.setPosition(atomPosition + atomSize);
} }
return null; return null;
} }
@Nullable
private static Metadata parseIlst(ParsableByteArray ilst, int limit) { private static Metadata parseIlst(ParsableByteArray ilst, int limit) {
ilst.skipBytes(Atom.HEADER_SIZE); ilst.skipBytes(Atom.HEADER_SIZE);
ArrayList<Metadata.Entry> entries = new ArrayList<>(); ArrayList<Metadata.Entry> entries = new ArrayList<>();
...@@ -610,19 +669,22 @@ import java.util.List; ...@@ -610,19 +669,22 @@ import java.util.List;
* Parses an hdlr atom. * Parses an hdlr atom.
* *
* @param hdlr The hdlr atom to decode. * @param hdlr The hdlr atom to decode.
* @return The track type. * @return The handler value.
*/ */
private static int parseHdlr(ParsableByteArray hdlr) { private static int parseHdlr(ParsableByteArray hdlr) {
hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4); hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
int trackType = hdlr.readInt(); return hdlr.readInt();
if (trackType == TYPE_soun) { }
/** Returns the track type for a given handler value. */
private static int getTrackTypeForHdlr(int hdlr) {
if (hdlr == TYPE_soun) {
return C.TRACK_TYPE_AUDIO; return C.TRACK_TYPE_AUDIO;
} else if (trackType == TYPE_vide) { } else if (hdlr == TYPE_vide) {
return C.TRACK_TYPE_VIDEO; return C.TRACK_TYPE_VIDEO;
} else if (trackType == TYPE_text || trackType == TYPE_sbtl || trackType == TYPE_subt } else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) {
|| trackType == TYPE_clcp) {
return C.TRACK_TYPE_TEXT; return C.TRACK_TYPE_TEXT;
} else if (trackType == TYPE_meta) { } else if (hdlr == TYPE_meta) {
return C.TRACK_TYPE_METADATA; return C.TRACK_TYPE_METADATA;
} else { } else {
return C.TRACK_TYPE_UNKNOWN; return C.TRACK_TYPE_UNKNOWN;
......
/*
* 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<MdtaMetadataEntry> CREATOR =
new Parcelable.Creator<MdtaMetadataEntry>() {
@Override
public MdtaMetadataEntry createFromParcel(Parcel in) {
return new MdtaMetadataEntry(in);
}
@Override
public MdtaMetadataEntry[] newArray(int size) {
return new MdtaMetadataEntry[size];
}
};
}
...@@ -16,6 +16,9 @@ ...@@ -16,6 +16,9 @@
package com.google.android.exoplayer2.extractor.mp4; package com.google.android.exoplayer2.extractor.mp4;
import android.support.annotation.Nullable; 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.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame;
...@@ -25,10 +28,9 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; ...@@ -25,10 +28,9 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
/** /** Utilities for handling metadata in MP4. */
* Parses metadata items stored in ilst atoms.
*/
/* package */ final class MetadataUtil { /* package */ final class MetadataUtil {
private static final String TAG = "MetadataUtil"; private static final String TAG = "MetadataUtil";
...@@ -103,24 +105,73 @@ import com.google.android.exoplayer2.util.Util; ...@@ -103,24 +105,73 @@ import com.google.android.exoplayer2.util.Util;
private static final String LANGUAGE_UNDEFINED = "und"; 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() {} private MetadataUtil() {}
/** /**
* Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting * Returns a {@link Format} that is the same as the input format but includes information from the
* from the current position of the {@link ParsableByteArray}, and the position is advanced by the * specified sources of metadata.
* size of the element. The position is advanced even if the element's type is unrecognized. */
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. * @param ilst Holds the data to be parsed.
* @return The parsed element, or null if the element's type was not recognized. * @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 position = ilst.getPosition();
int endPosition = position + ilst.readInt(); int endPosition = position + ilst.readInt();
int type = ilst.readInt(); int type = ilst.readInt();
int typeTopByte = (type >> 24) & 0xFF; int typeTopByte = (type >> 24) & 0xFF;
try { try {
if (typeTopByte == '\u00A9' /* Copyright char */ if (typeTopByte == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) {
|| typeTopByte == '\uFFFD' /* Replacement char */) {
int shortType = type & 0x00FFFFFF; int shortType = type & 0x00FFFFFF;
if (shortType == SHORT_TYPE_COMMENT) { if (shortType == SHORT_TYPE_COMMENT) {
return parseCommentAttribute(type, ilst); return parseCommentAttribute(type, ilst);
...@@ -185,7 +236,36 @@ import com.google.android.exoplayer2.util.Util; ...@@ -185,7 +236,36 @@ import com.google.android.exoplayer2.util.Util;
} }
} }
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 type, String id, ParsableByteArray data) {
int atomSize = data.readInt(); int atomSize = data.readInt();
int atomType = data.readInt(); int atomType = data.readInt();
...@@ -198,7 +278,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -198,7 +278,8 @@ import com.google.android.exoplayer2.util.Util;
return null; 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 atomSize = data.readInt();
int atomType = data.readInt(); int atomType = data.readInt();
if (atomType == Atom.TYPE_data) { if (atomType == Atom.TYPE_data) {
...@@ -210,7 +291,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -210,7 +291,8 @@ import com.google.android.exoplayer2.util.Util;
return null; return null;
} }
private static @Nullable Id3Frame parseUint8Attribute( @Nullable
private static Id3Frame parseUint8Attribute(
int type, int type,
String id, String id,
ParsableByteArray data, ParsableByteArray data,
...@@ -229,7 +311,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -229,7 +311,8 @@ import com.google.android.exoplayer2.util.Util;
return null; return null;
} }
private static @Nullable TextInformationFrame parseIndexAndCountAttribute( @Nullable
private static TextInformationFrame parseIndexAndCountAttribute(
int type, String attributeName, ParsableByteArray data) { int type, String attributeName, ParsableByteArray data) {
int atomSize = data.readInt(); int atomSize = data.readInt();
int atomType = data.readInt(); int atomType = data.readInt();
...@@ -249,8 +332,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -249,8 +332,8 @@ import com.google.android.exoplayer2.util.Util;
return null; return null;
} }
private static @Nullable TextInformationFrame parseStandardGenreAttribute( @Nullable
ParsableByteArray data) { private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) {
int genreCode = parseUint8AttributeValue(data); int genreCode = parseUint8AttributeValue(data);
String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length) String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
? STANDARD_GENRES[genreCode - 1] : null; ? STANDARD_GENRES[genreCode - 1] : null;
...@@ -261,7 +344,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -261,7 +344,8 @@ import com.google.android.exoplayer2.util.Util;
return null; return null;
} }
private static @Nullable ApicFrame parseCoverArt(ParsableByteArray data) { @Nullable
private static ApicFrame parseCoverArt(ParsableByteArray data) {
int atomSize = data.readInt(); int atomSize = data.readInt();
int atomType = data.readInt(); int atomType = data.readInt();
if (atomType == Atom.TYPE_data) { if (atomType == Atom.TYPE_data) {
...@@ -285,8 +369,8 @@ import com.google.android.exoplayer2.util.Util; ...@@ -285,8 +369,8 @@ import com.google.android.exoplayer2.util.Util;
return null; return null;
} }
private static @Nullable Id3Frame parseInternalAttribute( @Nullable
ParsableByteArray data, int endPosition) { private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) {
String domain = null; String domain = null;
String name = null; String name = null;
int dataAtomPosition = -1; int dataAtomPosition = -1;
......
...@@ -75,7 +75,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -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_ATOM_PAYLOAD = 1;
private static final int STATE_READING_SAMPLE = 2; 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 "); private static final int BRAND_QUICKTIME = Util.getIntegerCodeForString("qt ");
/** /**
...@@ -377,15 +377,21 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -377,15 +377,21 @@ public final class Mp4Extractor implements Extractor, SeekMap {
long durationUs = C.TIME_UNSET; long durationUs = C.TIME_UNSET;
List<Mp4Track> tracks = new ArrayList<>(); List<Mp4Track> tracks = new ArrayList<>();
Metadata metadata = null; // Process metadata.
Metadata udtaMetadata = null;
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
if (udta != null) { if (udta != null) {
metadata = AtomParsers.parseUdta(udta, isQuickTime); udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime);
if (metadata != null) { if (udtaMetadata != null) {
gaplessInfoHolder.setFromMetadata(metadata); 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; boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
ArrayList<TrackSampleTable> trackSampleTables = ArrayList<TrackSampleTable> trackSampleTables =
...@@ -401,15 +407,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -401,15 +407,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
// Allow ten source samples per output sample, like the platform extractor. // Allow ten source samples per output sample, like the platform extractor.
int maxInputSize = trackSampleTable.maximumSize + 3 * 10; int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
Format format = track.format.copyWithMaxInputSize(maxInputSize); Format format = track.format.copyWithMaxInputSize(maxInputSize);
if (track.type == C.TRACK_TYPE_AUDIO) { format =
if (gaplessInfoHolder.hasGaplessInfo()) { MetadataUtil.getFormatWithMetadata(
format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay, track.type, format, udtaMetadata, mdtaMetadata, gaplessInfoHolder);
gaplessInfoHolder.encoderPadding);
}
if (metadata != null) {
format = format.copyWithMetadata(metadata);
}
}
mp4Track.trackOutput.format(format); mp4Track.trackOutput.format(format);
durationUs = durationUs =
...@@ -716,24 +716,37 @@ public final class Mp4Extractor implements Extractor, SeekMap { ...@@ -716,24 +716,37 @@ public final class Mp4Extractor implements Extractor, SeekMap {
return false; 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) { private static boolean shouldParseLeafAtom(int atom) {
return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr return atom == Atom.TYPE_mdhd
|| atom == Atom.TYPE_stsd || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss || atom == Atom.TYPE_mvhd
|| atom == Atom.TYPE_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc || atom == Atom.TYPE_hdlr
|| atom == Atom.TYPE_stsz || atom == Atom.TYPE_stz2 || atom == Atom.TYPE_stco || atom == Atom.TYPE_stsd
|| atom == Atom.TYPE_co64 || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp || atom == Atom.TYPE_stts
|| atom == Atom.TYPE_udta; || atom == Atom.TYPE_stss
} || atom == Atom.TYPE_ctts
|| atom == Atom.TYPE_elst
/** || atom == Atom.TYPE_stsc
* Returns whether the extractor should decode a container atom with type {@code atom}. || 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}. */
private static boolean shouldParseContainerAtom(int atom) { private static boolean shouldParseContainerAtom(int atom) {
return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia return atom == Atom.TYPE_moov
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_edts; || 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 { private static final class Mp4Track {
......
...@@ -27,9 +27,7 @@ import java.io.IOException; ...@@ -27,9 +27,7 @@ import java.io.IOException;
*/ */
/* package */ final class Sniffer { /* 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 SEARCH_LENGTH = 4 * 1024;
private static final int[] COMPATIBLE_BRANDS = new int[] { private static final int[] COMPATIBLE_BRANDS = new int[] {
...@@ -109,15 +107,19 @@ import java.io.IOException; ...@@ -109,15 +107,19 @@ import java.io.IOException;
headerSize = Atom.LONG_HEADER_SIZE; headerSize = Atom.LONG_HEADER_SIZE;
input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE); input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
buffer.setLimit(Atom.LONG_HEADER_SIZE); buffer.setLimit(Atom.LONG_HEADER_SIZE);
atomSize = buffer.readUnsignedLongToLong(); atomSize = buffer.readLong();
} else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {
// The atom extends to the end of the file. // The atom extends to the end of the file.
long endPosition = input.getLength(); long fileEndPosition = input.getLength();
if (endPosition != C.LENGTH_UNSET) { if (fileEndPosition != C.LENGTH_UNSET) {
atomSize = endPosition - input.getPosition() + headerSize; 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) { if (atomSize < headerSize) {
// The file is invalid because the atom size is too small for its header. // The file is invalid because the atom size is too small for its header.
return false; return false;
...@@ -125,6 +127,13 @@ import java.io.IOException; ...@@ -125,6 +127,13 @@ import java.io.IOException;
bytesSearched += headerSize; bytesSearched += headerSize;
if (atomType == Atom.TYPE_moov) { 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. // Check for an mvex atom inside the moov atom to identify whether the file is fragmented.
continue; continue;
} }
......
...@@ -64,6 +64,9 @@ import com.google.android.exoplayer2.util.Util; ...@@ -64,6 +64,9 @@ import com.google.android.exoplayer2.util.Util;
this.flags = flags; this.flags = flags;
this.durationUs = durationUs; this.durationUs = durationUs;
sampleCount = offsets.length; sampleCount = offsets.length;
if (flags.length > 0) {
flags[flags.length - 1] |= C.BUFFER_FLAG_LAST_SAMPLE;
}
} }
/** /**
......
...@@ -326,7 +326,9 @@ public final class MediaCodecUtil { ...@@ -326,7 +326,9 @@ public final class MediaCodecUtil {
|| Util.MODEL.startsWith("SM-G350") || Util.MODEL.startsWith("SM-G350")
|| Util.MODEL.startsWith("SM-G386") || Util.MODEL.startsWith("SM-G386")
|| Util.MODEL.startsWith("SM-T231") || 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; return false;
} }
if ("OMX.brcm.audio.mp3.decoder".equals(name) if ("OMX.brcm.audio.mp3.decoder".equals(name)
......
...@@ -240,10 +240,10 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> { ...@@ -240,10 +240,10 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> {
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
ClippingMediaPeriod mediaPeriod = ClippingMediaPeriod mediaPeriod =
new ClippingMediaPeriod( new ClippingMediaPeriod(
mediaSource.createPeriod(id, allocator), mediaSource.createPeriod(id, allocator, startPositionUs),
enableInitialDiscontinuity, enableInitialDiscontinuity,
periodStartUs, periodStartUs,
periodEndUs); periodEndUs);
......
...@@ -16,13 +16,12 @@ ...@@ -16,13 +16,12 @@
package com.google.android.exoplayer2.source; package com.google.android.exoplayer2.source;
import android.os.Handler; import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.PlayerMessage;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder;
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
...@@ -45,8 +44,7 @@ import java.util.Map; ...@@ -45,8 +44,7 @@ import java.util.Map;
* during playback. It is valid for the same {@link MediaSource} instance to be present more than * 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. * once in the concatenation. Access to this class is thread-safe.
*/ */
public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHolder> public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHolder> {
implements PlayerMessage.Target {
private static final int MSG_ADD = 0; private static final int MSG_ADD = 0;
private static final int MSG_REMOVE = 1; private static final int MSG_REMOVE = 1;
...@@ -68,8 +66,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -68,8 +66,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
private final Timeline.Window window; private final Timeline.Window window;
private final Timeline.Period period; private final Timeline.Period period;
private @Nullable ExoPlayer player; @Nullable private Handler playbackThreadHandler;
private @Nullable Handler playerApplicationHandler; @Nullable private Handler applicationThreadHandler;
private boolean listenerNotificationScheduled; private boolean listenerNotificationScheduled;
private ShuffleOrder shuffleOrder; private ShuffleOrder shuffleOrder;
private int windowCount; private int windowCount;
...@@ -239,12 +237,10 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -239,12 +237,10 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
mediaSourceHolders.add(new MediaSourceHolder(mediaSource)); mediaSourceHolders.add(new MediaSourceHolder(mediaSource));
} }
mediaSourcesPublic.addAll(index, mediaSourceHolders); mediaSourcesPublic.addAll(index, mediaSourceHolders);
if (player != null && !mediaSources.isEmpty()) { if (playbackThreadHandler != null && !mediaSources.isEmpty()) {
player playbackThreadHandler
.createMessage(this) .obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, actionOnCompletion))
.setType(MSG_ADD) .sendToTarget();
.setPayload(new MessageData<>(index, mediaSourceHolders, actionOnCompletion))
.send();
} else if (actionOnCompletion != null) { } else if (actionOnCompletion != null) {
actionOnCompletion.run(); actionOnCompletion.run();
} }
...@@ -328,12 +324,10 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -328,12 +324,10 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
} }
return; return;
} }
if (player != null) { if (playbackThreadHandler != null) {
player playbackThreadHandler
.createMessage(this) .obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, actionOnCompletion))
.setType(MSG_REMOVE) .sendToTarget();
.setPayload(new MessageData<>(fromIndex, toIndex, actionOnCompletion))
.send();
} else if (actionOnCompletion != null) { } else if (actionOnCompletion != null) {
actionOnCompletion.run(); actionOnCompletion.run();
} }
...@@ -371,12 +365,10 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -371,12 +365,10 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
return; return;
} }
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
if (player != null) { if (playbackThreadHandler != null) {
player playbackThreadHandler
.createMessage(this) .obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, actionOnCompletion))
.setType(MSG_MOVE) .sendToTarget();
.setPayload(new MessageData<>(currentIndex, newIndex, actionOnCompletion))
.send();
} else if (actionOnCompletion != null) { } else if (actionOnCompletion != null) {
actionOnCompletion.run(); actionOnCompletion.run();
} }
...@@ -430,8 +422,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -430,8 +422,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
*/ */
public final synchronized void setShuffleOrder( public final synchronized void setShuffleOrder(
ShuffleOrder shuffleOrder, @Nullable Runnable actionOnCompletion) { ShuffleOrder shuffleOrder, @Nullable Runnable actionOnCompletion) {
ExoPlayer player = this.player; Handler playbackThreadHandler = this.playbackThreadHandler;
if (player != null) { if (playbackThreadHandler != null) {
int size = getSize(); int size = getSize();
if (shuffleOrder.getLength() != size) { if (shuffleOrder.getLength() != size) {
shuffleOrder = shuffleOrder =
...@@ -439,11 +431,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -439,11 +431,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
.cloneAndClear() .cloneAndClear()
.cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size); .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
} }
player playbackThreadHandler
.createMessage(this) .obtainMessage(
.setType(MSG_SET_SHUFFLE_ORDER) MSG_SET_SHUFFLE_ORDER,
.setPayload(new MessageData<>(/* index= */ 0, shuffleOrder, actionOnCompletion)) new MessageData<>(/* index= */ 0, shuffleOrder, actionOnCompletion))
.send(); .sendToTarget();
} else { } else {
this.shuffleOrder = this.shuffleOrder =
shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
...@@ -465,8 +457,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -465,8 +457,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
boolean isTopLevelSource, boolean isTopLevelSource,
@Nullable TransferListener mediaTransferListener) { @Nullable TransferListener mediaTransferListener) {
super.prepareSourceInternal(player, isTopLevelSource, mediaTransferListener); super.prepareSourceInternal(player, isTopLevelSource, mediaTransferListener);
this.player = player; playbackThreadHandler = new Handler(/* callback= */ this::handleMessage);
playerApplicationHandler = new Handler(player.getApplicationLooper()); applicationThreadHandler = new Handler(player.getApplicationLooper());
if (mediaSourcesPublic.isEmpty()) { if (mediaSourcesPublic.isEmpty()) {
notifyListener(); notifyListener();
} else { } else {
...@@ -484,7 +476,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -484,7 +476,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
} }
@Override @Override
public final MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public final MediaPeriod createPeriod(
MediaPeriodId id, Allocator allocator, long startPositionUs) {
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid); MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid);
if (holder == null) { if (holder == null) {
...@@ -492,7 +485,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -492,7 +485,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
holder = new MediaSourceHolder(new DummyMediaSource()); holder = new MediaSourceHolder(new DummyMediaSource());
holder.hasStartedPreparing = true; holder.hasStartedPreparing = true;
} }
DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, id, allocator); DeferredMediaPeriod mediaPeriod =
new DeferredMediaPeriod(holder.mediaSource, id, allocator, startPositionUs);
mediaSourceByMediaPeriod.put(mediaPeriod, holder); mediaSourceByMediaPeriod.put(mediaPeriod, holder);
holder.activeMediaPeriods.add(mediaPeriod); holder.activeMediaPeriods.add(mediaPeriod);
if (!holder.hasStartedPreparing) { if (!holder.hasStartedPreparing) {
...@@ -519,8 +513,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -519,8 +513,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
super.releaseSourceInternal(); super.releaseSourceInternal();
mediaSourceHolders.clear(); mediaSourceHolders.clear();
mediaSourceByUid.clear(); mediaSourceByUid.clear();
player = null; playbackThreadHandler = null;
playerApplicationHandler = null; applicationThreadHandler = null;
shuffleOrder = shuffleOrder.cloneAndClear(); shuffleOrder = shuffleOrder.cloneAndClear();
windowCount = 0; windowCount = 0;
periodCount = 0; periodCount = 0;
...@@ -556,24 +550,22 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -556,24 +550,22 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
return windowIndex + mediaSourceHolder.firstWindowIndexInChild; return windowIndex + mediaSourceHolder.firstWindowIndexInChild;
} }
@Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public final void handleMessage(int messageType, @Nullable Object message) private boolean handleMessage(Message msg) {
throws ExoPlaybackException { if (playbackThreadHandler == null) {
if (player == null) {
// Stale event. // Stale event.
return; return false;
} }
switch (messageType) { switch (msg.what) {
case MSG_ADD: case MSG_ADD:
MessageData<Collection<MediaSourceHolder>> addMessage = MessageData<Collection<MediaSourceHolder>> addMessage =
(MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(message); (MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj);
shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size()); shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size());
addMediaSourcesInternal(addMessage.index, addMessage.customData); addMediaSourcesInternal(addMessage.index, addMessage.customData);
scheduleListenerNotification(addMessage.actionOnCompletion); scheduleListenerNotification(addMessage.actionOnCompletion);
break; break;
case MSG_REMOVE: case MSG_REMOVE:
MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(message); MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
int fromIndex = removeMessage.index; int fromIndex = removeMessage.index;
int toIndex = removeMessage.customData; int toIndex = removeMessage.customData;
if (fromIndex == 0 && toIndex == shuffleOrder.getLength()) { if (fromIndex == 0 && toIndex == shuffleOrder.getLength()) {
...@@ -587,7 +579,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -587,7 +579,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
scheduleListenerNotification(removeMessage.actionOnCompletion); scheduleListenerNotification(removeMessage.actionOnCompletion);
break; break;
case MSG_MOVE: case MSG_MOVE:
MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(message); MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1); shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1);
shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1);
moveMediaSourceInternal(moveMessage.index, moveMessage.customData); moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
...@@ -595,7 +587,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -595,7 +587,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
break; break;
case MSG_SET_SHUFFLE_ORDER: case MSG_SET_SHUFFLE_ORDER:
MessageData<ShuffleOrder> shuffleOrderMessage = MessageData<ShuffleOrder> shuffleOrderMessage =
(MessageData<ShuffleOrder>) Util.castNonNull(message); (MessageData<ShuffleOrder>) Util.castNonNull(msg.obj);
shuffleOrder = shuffleOrderMessage.customData; shuffleOrder = shuffleOrderMessage.customData;
scheduleListenerNotification(shuffleOrderMessage.actionOnCompletion); scheduleListenerNotification(shuffleOrderMessage.actionOnCompletion);
break; break;
...@@ -603,8 +595,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -603,8 +595,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
notifyListener(); notifyListener();
break; break;
case MSG_ON_COMPLETION: case MSG_ON_COMPLETION:
List<Runnable> actionsOnCompletion = (List<Runnable>) Util.castNonNull(message); List<Runnable> actionsOnCompletion = (List<Runnable>) Util.castNonNull(msg.obj);
Handler handler = Assertions.checkNotNull(playerApplicationHandler); Handler handler = Assertions.checkNotNull(applicationThreadHandler);
for (int i = 0; i < actionsOnCompletion.size(); i++) { for (int i = 0; i < actionsOnCompletion.size(); i++) {
handler.post(actionsOnCompletion.get(i)); handler.post(actionsOnCompletion.get(i));
} }
...@@ -612,11 +604,14 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -612,11 +604,14 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
default: default:
throw new IllegalStateException(); throw new IllegalStateException();
} }
return true;
} }
private void scheduleListenerNotification(@Nullable Runnable actionOnCompletion) { private void scheduleListenerNotification(@Nullable Runnable actionOnCompletion) {
if (!listenerNotificationScheduled) { if (!listenerNotificationScheduled) {
Assertions.checkNotNull(player).createMessage(this).setType(MSG_NOTIFY_LISTENER).send(); Assertions.checkNotNull(playbackThreadHandler)
.obtainMessage(MSG_NOTIFY_LISTENER)
.sendToTarget();
listenerNotificationScheduled = true; listenerNotificationScheduled = true;
} }
if (actionOnCompletion != null) { if (actionOnCompletion != null) {
...@@ -636,11 +631,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -636,11 +631,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),
/* manifest= */ null); /* manifest= */ null);
if (!actionsOnCompletion.isEmpty()) { if (!actionsOnCompletion.isEmpty()) {
Assertions.checkNotNull(player) Assertions.checkNotNull(playbackThreadHandler)
.createMessage(this) .obtainMessage(MSG_ON_COMPLETION, actionsOnCompletion)
.setType(MSG_ON_COMPLETION) .sendToTarget();
.setPayload(actionsOnCompletion)
.send();
} }
} }
...@@ -718,6 +711,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -718,6 +711,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
// unlikely to be a problem as a non-zero default position usually only occurs for live // 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 // playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions
// anyway. // anyway.
timeline.getWindow(/* windowIndex= */ 0, window);
long windowStartPositionUs = window.getDefaultPositionUs(); long windowStartPositionUs = window.getDefaultPositionUs();
if (deferredMediaPeriod != null) { if (deferredMediaPeriod != null) {
long periodPreparePositionUs = deferredMediaPeriod.getPreparePositionUs(); long periodPreparePositionUs = deferredMediaPeriod.getPreparePositionUs();
...@@ -1101,7 +1095,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -1101,7 +1095,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
......
...@@ -25,7 +25,7 @@ import java.io.IOException; ...@@ -25,7 +25,7 @@ import java.io.IOException;
/** /**
* Media period that wraps a media source and defers calling its {@link * 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 * #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. * period immediately but the media source that should create it is not yet prepared.
*/ */
...@@ -60,11 +60,14 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb ...@@ -60,11 +60,14 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
* @param mediaSource The media source to wrap. * @param mediaSource The media source to wrap.
* @param id The identifier used to create the deferred media period. * @param id The identifier used to create the deferred media period.
* @param allocator The allocator used to create the 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.id = id;
this.allocator = allocator; this.allocator = allocator;
this.mediaSource = mediaSource; this.mediaSource = mediaSource;
this.preparePositionUs = preparePositionUs;
preparePositionOverrideUs = C.TIME_UNSET; preparePositionOverrideUs = C.TIME_UNSET;
} }
...@@ -86,28 +89,25 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb ...@@ -86,28 +89,25 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
/** /**
* Overrides the default prepare position at which to prepare the media period. This value is only * 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) { public void overridePreparePositionUs(long preparePositionUs) {
preparePositionOverrideUs = defaultPreparePositionUs; preparePositionOverrideUs = preparePositionUs;
} }
/** /**
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source
* prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()} * then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link
* to release the period. * #releasePeriod()} to release the period.
* *
* @param id The identifier that should be used to create the media period from the media source. * @param id The identifier that should be used to create the media period from the media source.
*/ */
public void createPeriod(MediaPeriodId id) { public void createPeriod(MediaPeriodId id) {
mediaPeriod = mediaSource.createPeriod(id, allocator); long preparePositionUs = getPreparePositionWithOverride(this.preparePositionUs);
mediaPeriod = mediaSource.createPeriod(id, allocator, preparePositionUs);
if (callback != null) { if (callback != null) {
long preparePositionUs =
preparePositionOverrideUs != C.TIME_UNSET
? preparePositionOverrideUs
: this.preparePositionUs;
mediaPeriod.prepare(this, preparePositionUs); mediaPeriod.prepare(this, preparePositionUs);
} }
} }
...@@ -124,9 +124,8 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb ...@@ -124,9 +124,8 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
@Override @Override
public void prepare(Callback callback, long preparePositionUs) { public void prepare(Callback callback, long preparePositionUs) {
this.callback = callback; this.callback = callback;
this.preparePositionUs = preparePositionUs;
if (mediaPeriod != null) { if (mediaPeriod != null) {
mediaPeriod.prepare(this, preparePositionUs); mediaPeriod.prepare(this, getPreparePositionWithOverride(this.preparePositionUs));
} }
} }
...@@ -217,4 +216,9 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb ...@@ -217,4 +216,9 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
callback.onPrepared(this); callback.onPrepared(this);
} }
private long getPreparePositionWithOverride(long preparePositionUs) {
return preparePositionOverrideUs != C.TIME_UNSET
? preparePositionOverrideUs
: preparePositionUs;
}
} }
...@@ -346,18 +346,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -346,18 +346,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} else if (isPendingReset()) { } else if (isPendingReset()) {
return pendingResetPositionUs; return pendingResetPositionUs;
} }
long largestQueuedTimestampUs; long largestQueuedTimestampUs = C.TIME_UNSET;
if (haveAudioVideoTracks) { if (haveAudioVideoTracks) {
// Ignore non-AV tracks, which may be sparse or poorly interleaved. // Ignore non-AV tracks, which may be sparse or poorly interleaved.
largestQueuedTimestampUs = Long.MAX_VALUE; largestQueuedTimestampUs = Long.MAX_VALUE;
int trackCount = sampleQueues.length; int trackCount = sampleQueues.length;
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
if (trackIsAudioVideoFlags[i]) { if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) {
largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs, largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,
sampleQueues[i].getLargestQueuedTimestampUs()); sampleQueues[i].getLargestQueuedTimestampUs());
} }
} }
} else { }
if (largestQueuedTimestampUs == C.TIME_UNSET) {
largestQueuedTimestampUs = getLargestQueuedTimestampUs(); largestQueuedTimestampUs = getLargestQueuedTimestampUs();
} }
return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs
......
...@@ -370,7 +370,7 @@ public final class ExtractorMediaSource extends BaseMediaSource ...@@ -370,7 +370,7 @@ public final class ExtractorMediaSource extends BaseMediaSource
boolean isTopLevelSource, boolean isTopLevelSource,
@Nullable TransferListener mediaTransferListener) { @Nullable TransferListener mediaTransferListener) {
transferListener = mediaTransferListener; transferListener = mediaTransferListener;
notifySourceInfoRefreshed(timelineDurationUs, /* isSeekable= */ false); notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable);
} }
@Override @Override
...@@ -379,7 +379,7 @@ public final class ExtractorMediaSource extends BaseMediaSource ...@@ -379,7 +379,7 @@ public final class ExtractorMediaSource extends BaseMediaSource
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
DataSource dataSource = dataSourceFactory.createDataSource(); DataSource dataSource = dataSourceFactory.createDataSource();
if (transferListener != null) { if (transferListener != null) {
dataSource.addTransferListener(transferListener); dataSource.addTransferListener(transferListener);
......
...@@ -80,14 +80,15 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> { ...@@ -80,14 +80,15 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> {
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
if (loopCount == Integer.MAX_VALUE) { if (loopCount == Integer.MAX_VALUE) {
return childSource.createPeriod(id, allocator); return childSource.createPeriod(id, allocator, startPositionUs);
} }
Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid); Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid);
MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid); MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid);
childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id); childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id);
MediaPeriod mediaPeriod = childSource.createPeriod(childMediaPeriodId, allocator); MediaPeriod mediaPeriod =
childSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId); mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId);
return mediaPeriod; return mediaPeriod;
} }
......
...@@ -35,8 +35,8 @@ import java.io.IOException; ...@@ -35,8 +35,8 @@ import java.io.IOException;
* on the {@link SourceInfoRefreshListener}s passed to {@link #prepareSource(ExoPlayer, * on the {@link SourceInfoRefreshListener}s passed to {@link #prepareSource(ExoPlayer,
* boolean, SourceInfoRefreshListener, TransferListener)}. * boolean, SourceInfoRefreshListener, TransferListener)}.
* <li>To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are * <li>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 * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a
* the player to load and read the media. * way for the player to load and read the media.
* </ul> * </ul>
* *
* All methods are called on the player's internal playback thread, as described in the {@link * All methods are called on the player's internal playback thread, as described in the {@link
...@@ -274,9 +274,10 @@ public interface MediaSource { ...@@ -274,9 +274,10 @@ public interface MediaSource {
* *
* @param id The identifier of the period. * @param id The identifier of the period.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @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}. * @return A new {@link MediaPeriod}.
*/ */
MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator); MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs);
/** /**
* Releases the period. * Releases the period.
......
...@@ -124,13 +124,13 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> { ...@@ -124,13 +124,13 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
MediaPeriod[] periods = new MediaPeriod[mediaSources.length]; MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid); int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);
for (int i = 0; i < periods.length; i++) { for (int i = 0; i < periods.length; i++) {
MediaPeriodId childMediaPeriodId = MediaPeriodId childMediaPeriodId =
id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex)); 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); return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods);
} }
......
...@@ -57,6 +57,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -57,6 +57,7 @@ import com.google.android.exoplayer2.util.Util;
private long largestDiscardedTimestampUs; private long largestDiscardedTimestampUs;
private long largestQueuedTimestampUs; private long largestQueuedTimestampUs;
private boolean isLastSampleQueued;
private boolean upstreamKeyframeRequired; private boolean upstreamKeyframeRequired;
private boolean upstreamFormatRequired; private boolean upstreamFormatRequired;
private Format upstreamFormat; private Format upstreamFormat;
...@@ -93,6 +94,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -93,6 +94,7 @@ import com.google.android.exoplayer2.util.Util;
upstreamKeyframeRequired = true; upstreamKeyframeRequired = true;
largestDiscardedTimestampUs = Long.MIN_VALUE; largestDiscardedTimestampUs = Long.MIN_VALUE;
largestQueuedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE;
isLastSampleQueued = false;
if (resetUpstreamFormat) { if (resetUpstreamFormat) {
upstreamFormat = null; upstreamFormat = null;
upstreamFormatRequired = true; upstreamFormatRequired = true;
...@@ -118,6 +120,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -118,6 +120,7 @@ import com.google.android.exoplayer2.util.Util;
Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition));
length -= discardCount; length -= discardCount;
largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length));
isLastSampleQueued = discardCount == 0 && isLastSampleQueued;
if (length == 0) { if (length == 0) {
return 0; return 0;
} else { } else {
...@@ -186,6 +189,19 @@ import com.google.android.exoplayer2.util.Util; ...@@ -186,6 +189,19 @@ import com.google.android.exoplayer2.util.Util;
return largestQueuedTimestampUs; 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.
*
* <p>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. */ /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
public synchronized long getFirstTimestampUs() { public synchronized long getFirstTimestampUs() {
return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex];
...@@ -224,7 +240,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -224,7 +240,7 @@ import com.google.android.exoplayer2.util.Util;
boolean formatRequired, boolean loadingFinished, Format downstreamFormat, boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
SampleExtrasHolder extrasHolder) { SampleExtrasHolder extrasHolder) {
if (!hasNextSample()) { if (!hasNextSample()) {
if (loadingFinished) { if (loadingFinished || isLastSampleQueued) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ; return C.RESULT_BUFFER_READ;
} else if (upstreamFormat != null } else if (upstreamFormat != null
...@@ -388,7 +404,9 @@ import com.google.android.exoplayer2.util.Util; ...@@ -388,7 +404,9 @@ import com.google.android.exoplayer2.util.Util;
upstreamKeyframeRequired = false; upstreamKeyframeRequired = false;
} }
Assertions.checkState(!upstreamFormatRequired); Assertions.checkState(!upstreamFormatRequired);
commitSampleTimestamp(timeUs);
isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0;
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
int relativeEndIndex = getRelativeIndex(length); int relativeEndIndex = getRelativeIndex(length);
timesUs[relativeEndIndex] = timeUs; timesUs[relativeEndIndex] = timeUs;
...@@ -439,10 +457,6 @@ import com.google.android.exoplayer2.util.Util; ...@@ -439,10 +457,6 @@ import com.google.android.exoplayer2.util.Util;
} }
} }
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 * 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. * specified timestamp to be spliced in. Samples will not be discarded prior to the read position.
......
...@@ -224,6 +224,15 @@ public class SampleQueue implements TrackOutput { ...@@ -224,6 +224,15 @@ public class SampleQueue implements TrackOutput {
return metadataQueue.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. */ /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
public long getFirstTimestampUs() { public long getFirstTimestampUs() {
return metadataQueue.getFirstTimestampUs(); return metadataQueue.getFirstTimestampUs();
......
...@@ -318,7 +318,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { ...@@ -318,7 +318,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
return new SingleSampleMediaPeriod( return new SingleSampleMediaPeriod(
dataSpec, dataSpec,
dataSourceFactory, dataSourceFactory,
......
...@@ -341,7 +341,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> { ...@@ -341,7 +341,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
if (adPlaybackState.adGroupCount > 0 && id.isAd()) { if (adPlaybackState.adGroupCount > 0 && id.isAd()) {
int adGroupIndex = id.adGroupIndex; int adGroupIndex = id.adGroupIndex;
int adIndexInAdGroup = id.adIndexInAdGroup; int adIndexInAdGroup = id.adIndexInAdGroup;
...@@ -360,7 +360,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> { ...@@ -360,7 +360,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
prepareChildSource(id, adMediaSource); prepareChildSource(id, adMediaSource);
} }
MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup];
DeferredMediaPeriod deferredMediaPeriod = new DeferredMediaPeriod(mediaSource, id, allocator); DeferredMediaPeriod deferredMediaPeriod =
new DeferredMediaPeriod(mediaSource, id, allocator, startPositionUs);
deferredMediaPeriod.setPrepareErrorListener( deferredMediaPeriod.setPrepareErrorListener(
new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup)); new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup));
List<DeferredMediaPeriod> mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); List<DeferredMediaPeriod> mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource);
...@@ -376,7 +377,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> { ...@@ -376,7 +377,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
} }
return deferredMediaPeriod; return deferredMediaPeriod;
} else { } else {
DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(contentMediaSource, id, allocator); DeferredMediaPeriod mediaPeriod =
new DeferredMediaPeriod(contentMediaSource, id, allocator, startPositionUs);
mediaPeriod.createPeriod(id); mediaPeriod.createPeriod(id);
return mediaPeriod; return mediaPeriod;
} }
......
...@@ -64,11 +64,11 @@ public interface DataSource { ...@@ -64,11 +64,11 @@ public interface DataSource {
long open(DataSpec dataSpec) throws IOException; 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}. * index {@code offset}.
* <p> *
* If {@code length} is zero then 0 is returned. Otherwise, if no data is available because the * <p>If {@code readLength} is zero then 0 is returned. Otherwise, if no data is available because
* end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned. * 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 * Otherwise, the call will block until at least one byte of data has been read and the number of
* bytes read is returned. * bytes read is returned.
* *
......
...@@ -43,8 +43,8 @@ public final class CacheDataSink implements DataSink { ...@@ -43,8 +43,8 @@ public final class CacheDataSink implements DataSink {
private final Cache cache; private final Cache cache;
private final long maxCacheFileSize; private final long maxCacheFileSize;
private final int bufferSize; private final int bufferSize;
private final boolean syncFileDescriptor;
private boolean syncFileDescriptor;
private DataSpec dataSpec; private DataSpec dataSpec;
private File file; private File file;
private OutputStream outputStream; private OutputStream outputStream;
...@@ -68,25 +68,12 @@ public final class CacheDataSink implements DataSink { ...@@ -68,25 +68,12 @@ public final class CacheDataSink implements DataSink {
* Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}. * Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}.
* *
* @param cache The cache into which data should be written. * @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}.
*
* @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 * @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 * {@link DataSpec} whose size exceeds this value, then the data will be fragmented into
* multiple cache files. * multiple cache files.
* @param syncFileDescriptor Whether file descriptors are sync'd when closing output streams.
*/ */
public CacheDataSink(Cache cache, long maxCacheFileSize, boolean syncFileDescriptor) { public CacheDataSink(Cache cache, long maxCacheFileSize) {
this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE, syncFileDescriptor); this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE);
} }
/** /**
...@@ -98,23 +85,21 @@ public final class CacheDataSink implements DataSink { ...@@ -98,23 +85,21 @@ public final class CacheDataSink implements DataSink {
* value disables buffering. * value disables buffering.
*/ */
public CacheDataSink(Cache cache, long maxCacheFileSize, int bufferSize) { 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. * Sets whether file descriptors are synced when closing output streams.
* @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 * <p>This method is experimental, and will be renamed or removed in a future release. It should
* multiple cache files. * only be called before the renderer is used.
* @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 synced when closing output streams.
* @param syncFileDescriptor Whether file descriptors are sync'd when closing output streams.
*/ */
public CacheDataSink( public void experimental_setSyncFileDescriptor(boolean syncFileDescriptor) {
Cache cache, long maxCacheFileSize, int bufferSize, boolean syncFileDescriptor) {
this.cache = Assertions.checkNotNull(cache);
this.maxCacheFileSize = maxCacheFileSize;
this.bufferSize = bufferSize;
this.syncFileDescriptor = syncFileDescriptor; this.syncFileDescriptor = syncFileDescriptor;
} }
......
...@@ -29,7 +29,7 @@ import java.io.OutputStream; ...@@ -29,7 +29,7 @@ import java.io.OutputStream;
* has successfully completed. * has successfully completed.
* *
* <p>Atomic file guarantees file integrity by ensuring that a file has been completely written and * <p>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). * is considered to be invalid (left over from a previous attempt to write the file).
* *
* <p>Atomic file does not confer any file locking semantics. Do not use this class when the file * <p>Atomic file does not confer any file locking semantics. Do not use this class when the file
......
...@@ -1087,6 +1087,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1087,6 +1087,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
throws DecoderQueryException { throws DecoderQueryException {
int maxWidth = format.width; int maxWidth = format.width;
int maxHeight = format.height; int maxHeight = format.height;
if (codecNeedsMaxVideoSizeResetWorkaround(codecInfo.name)) {
maxWidth = Math.max(maxWidth, 1920);
maxHeight = Math.max(maxHeight, 1089);
}
int maxInputSize = getMaxInputSize(codecInfo, format); int maxInputSize = getMaxInputSize(codecInfo, format);
if (streamFormats.length == 1) { if (streamFormats.length == 1) {
// The single entry in streamFormats must correspond to the format for which the codec is // The single entry in streamFormats must correspond to the format for which the codec is
...@@ -1274,6 +1278,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1274,6 +1278,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return "NVIDIA".equals(Util.MANUFACTURER); 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.
*
* <p>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: * TODO:
* *
...@@ -1322,7 +1338,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1322,7 +1338,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
// https://github.com/google/ExoPlayer/issues/4315, // https://github.com/google/ExoPlayer/issues/4315,
// https://github.com/google/ExoPlayer/issues/4419, // https://github.com/google/ExoPlayer/issues/4419,
// https://github.com/google/ExoPlayer/issues/4460, // 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) { switch (Util.DEVICE) {
case "1601": case "1601":
case "1713": case "1713":
...@@ -1378,6 +1395,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1378,6 +1395,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "HWBLN-H": case "HWBLN-H":
case "HWCAM-H": case "HWCAM-H":
case "HWVNS-H": case "HWVNS-H":
case "HWWAS-H":
case "i9031": case "i9031":
case "iball8735_9806": case "iball8735_9806":
case "Infinix-X572": case "Infinix-X572":
......
...@@ -147,7 +147,7 @@ track 0: ...@@ -147,7 +147,7 @@ track 0:
data = length 530, hash C98BC6A8 data = length 530, hash C98BC6A8
sample 29: sample 29:
time = 934266 time = 934266
flags = 0 flags = 536870912
data = length 568, hash 4FE5C8EA data = length 568, hash 4FE5C8EA
track 1: track 1:
format: format:
...@@ -352,6 +352,6 @@ track 1: ...@@ -352,6 +352,6 @@ track 1:
data = length 229, hash FFF98DF0 data = length 229, hash FFF98DF0
sample 44: sample 44:
time = 1065678 time = 1065678
flags = 1 flags = 536870913
data = length 6, hash 31B22286 data = length 6, hash 31B22286
tracksEnded = true tracksEnded = true
...@@ -147,7 +147,7 @@ track 0: ...@@ -147,7 +147,7 @@ track 0:
data = length 530, hash C98BC6A8 data = length 530, hash C98BC6A8
sample 29: sample 29:
time = 934266 time = 934266
flags = 0 flags = 536870912
data = length 568, hash 4FE5C8EA data = length 568, hash 4FE5C8EA
track 1: track 1:
format: format:
...@@ -304,6 +304,6 @@ track 1: ...@@ -304,6 +304,6 @@ track 1:
data = length 229, hash FFF98DF0 data = length 229, hash FFF98DF0
sample 32: sample 32:
time = 1065678 time = 1065678
flags = 1 flags = 536870913
data = length 6, hash 31B22286 data = length 6, hash 31B22286
tracksEnded = true tracksEnded = true
...@@ -147,7 +147,7 @@ track 0: ...@@ -147,7 +147,7 @@ track 0:
data = length 530, hash C98BC6A8 data = length 530, hash C98BC6A8
sample 29: sample 29:
time = 934266 time = 934266
flags = 0 flags = 536870912
data = length 568, hash 4FE5C8EA data = length 568, hash 4FE5C8EA
track 1: track 1:
format: format:
...@@ -244,6 +244,6 @@ track 1: ...@@ -244,6 +244,6 @@ track 1:
data = length 229, hash FFF98DF0 data = length 229, hash FFF98DF0
sample 17: sample 17:
time = 1065678 time = 1065678
flags = 1 flags = 536870913
data = length 6, hash 31B22286 data = length 6, hash 31B22286
tracksEnded = true tracksEnded = true
...@@ -147,7 +147,7 @@ track 0: ...@@ -147,7 +147,7 @@ track 0:
data = length 530, hash C98BC6A8 data = length 530, hash C98BC6A8
sample 29: sample 29:
time = 934266 time = 934266
flags = 0 flags = 536870912
data = length 568, hash 4FE5C8EA data = length 568, hash 4FE5C8EA
track 1: track 1:
format: format:
...@@ -184,6 +184,6 @@ track 1: ...@@ -184,6 +184,6 @@ track 1:
data = length 229, hash FFF98DF0 data = length 229, hash FFF98DF0
sample 2: sample 2:
time = 1065678 time = 1065678
flags = 1 flags = 536870913
data = length 6, hash 31B22286 data = length 6, hash 31B22286
tracksEnded = true tracksEnded = true
/*
* 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();
}
}
...@@ -46,6 +46,7 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; ...@@ -46,6 +46,7 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
...@@ -452,13 +453,22 @@ import java.util.List; ...@@ -452,13 +453,22 @@ import java.util.List;
if (adaptationSetSwitchingProperty == null) { if (adaptationSetSwitchingProperty == null) {
groupedAdaptationSetIndices[groupCount++] = new int[] {i}; groupedAdaptationSetIndices[groupCount++] = new int[] {i};
} else { } else {
String[] extraAdaptationSetIds = adaptationSetSwitchingProperty.value.split(","); String[] extraAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ",");
int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length]; int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length];
adaptationSetIndices[0] = i; adaptationSetIndices[0] = i;
int outputIndex = 1;
for (int j = 0; j < extraAdaptationSetIds.length; j++) { for (int j = 0; j < extraAdaptationSetIds.length; j++) {
int extraIndex = idToIndexMap.get(Integer.parseInt(extraAdaptationSetIds[j])); int extraIndex =
idToIndexMap.get(
Integer.parseInt(extraAdaptationSetIds[j]), /* valueIfKeyNotFound= */ -1);
if (extraIndex != -1) {
adaptationSetUsedFlags[extraIndex] = true; adaptationSetUsedFlags[extraIndex] = true;
adaptationSetIndices[1 + j] = extraIndex; adaptationSetIndices[outputIndex] = extraIndex;
outputIndex++;
}
}
if (outputIndex < adaptationSetIndices.length) {
adaptationSetIndices = Arrays.copyOf(adaptationSetIndices, outputIndex);
} }
groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices; groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices;
} }
......
...@@ -635,7 +635,8 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -635,7 +635,8 @@ public final class DashMediaSource extends BaseMediaSource {
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId periodId, Allocator allocator) { public MediaPeriod createPeriod(
MediaPeriodId periodId, Allocator allocator, long startPositionUs) {
int periodIndex = (Integer) periodId.periodUid - firstPeriodId; int periodIndex = (Integer) periodId.periodUid - firstPeriodId;
EventDispatcher periodEventDispatcher = EventDispatcher periodEventDispatcher =
createEventDispatcher(periodId, manifest.getPeriod(periodIndex).startMs); createEventDispatcher(periodId, manifest.getPeriod(periodIndex).startMs);
......
...@@ -457,10 +457,10 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -457,10 +457,10 @@ public class DefaultDashChunkSource implements DashChunkSource {
} }
private ArrayList<Representation> getRepresentations() { private ArrayList<Representation> getRepresentations() {
List<AdaptationSet> manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets; List<AdaptationSet> manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets;
ArrayList<Representation> representations = new ArrayList<>(); ArrayList<Representation> representations = new ArrayList<>();
for (int adaptationSetIndex : adaptationSetIndices) { for (int adaptationSetIndex : adaptationSetIndices) {
representations.addAll(manifestAdapationSets.get(adaptationSetIndex).representations); representations.addAll(manifestAdaptationSets.get(adaptationSetIndex).representations);
} }
return representations; return representations;
} }
......
...@@ -412,7 +412,7 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -412,7 +412,7 @@ public final class HlsMediaSource extends BaseMediaSource
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
EventDispatcher eventDispatcher = createEventDispatcher(id); EventDispatcher eventDispatcher = createEventDispatcher(id);
return new HlsMediaPeriod( return new HlsMediaPeriod(
extractorFactory, extractorFactory,
......
...@@ -360,7 +360,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -360,7 +360,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
/* initializationData= */ null, /* initializationData= */ null,
selectionFlags, selectionFlags,
language); language);
if (uri == null) { if (isMediaTagMuxed(variants, uri)) {
muxedAudioFormat = format; muxedAudioFormat = format;
} else { } else {
audios.add(new HlsMasterPlaylist.HlsUrl(uri, format)); audios.add(new HlsMasterPlaylist.HlsUrl(uri, format));
...@@ -766,6 +766,20 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -766,6 +766,20 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
return Pattern.compile(attribute + "=(" + BOOLEAN_FALSE + "|" + BOOLEAN_TRUE + ")"); return Pattern.compile(attribute + "=(" + BOOLEAN_FALSE + "|" + BOOLEAN_TRUE + ")");
} }
private static boolean isMediaTagMuxed(
List<HlsMasterPlaylist.HlsUrl> 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 static class LineIterator {
private final BufferedReader reader; private final BufferedReader reader;
......
...@@ -134,6 +134,17 @@ public class HlsMasterPlaylistParserTest { ...@@ -134,6 +134,17 @@ public class HlsMasterPlaylistParserTest {
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"{$codecs}\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"{$codecs}\"\n"
+ "http://example.com/{$tricky}\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 @Test
public void testParseMasterPlaylist() throws IOException { public void testParseMasterPlaylist() throws IOException {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
...@@ -271,6 +282,14 @@ public class HlsMasterPlaylistParserTest { ...@@ -271,6 +282,14 @@ public class HlsMasterPlaylistParserTest {
assertThat(variant.url).isEqualTo("http://example.com/This/{$nested}/reference/shouldnt/work"); 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) private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString)
throws IOException { throws IOException {
Uri playlistUri = Uri.parse(uri); Uri playlistUri = Uri.parse(uri);
......
...@@ -61,14 +61,13 @@ public class DefaultSsChunkSource implements SsChunkSource { ...@@ -61,14 +61,13 @@ public class DefaultSsChunkSource implements SsChunkSource {
SsManifest manifest, SsManifest manifest,
int elementIndex, int elementIndex,
TrackSelection trackSelection, TrackSelection trackSelection,
TrackEncryptionBox[] trackEncryptionBoxes,
@Nullable TransferListener transferListener) { @Nullable TransferListener transferListener) {
DataSource dataSource = dataSourceFactory.createDataSource(); DataSource dataSource = dataSourceFactory.createDataSource();
if (transferListener != null) { if (transferListener != null) {
dataSource.addTransferListener(transferListener); dataSource.addTransferListener(transferListener);
} }
return new DefaultSsChunkSource(manifestLoaderErrorThrower, manifest, elementIndex, return new DefaultSsChunkSource(
trackSelection, dataSource, trackEncryptionBoxes); manifestLoaderErrorThrower, manifest, elementIndex, trackSelection, dataSource);
} }
} }
...@@ -90,15 +89,13 @@ public class DefaultSsChunkSource implements SsChunkSource { ...@@ -90,15 +89,13 @@ public class DefaultSsChunkSource implements SsChunkSource {
* @param streamElementIndex The index of the stream element in the manifest. * @param streamElementIndex The index of the stream element in the manifest.
* @param trackSelection The track selection. * @param trackSelection The track selection.
* @param dataSource A {@link DataSource} suitable for loading the media data. * @param dataSource A {@link DataSource} suitable for loading the media data.
* @param trackEncryptionBoxes Track encryption boxes for the stream.
*/ */
public DefaultSsChunkSource( public DefaultSsChunkSource(
LoaderErrorThrower manifestLoaderErrorThrower, LoaderErrorThrower manifestLoaderErrorThrower,
SsManifest manifest, SsManifest manifest,
int streamElementIndex, int streamElementIndex,
TrackSelection trackSelection, TrackSelection trackSelection,
DataSource dataSource, DataSource dataSource) {
TrackEncryptionBox[] trackEncryptionBoxes) {
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.manifest = manifest; this.manifest = manifest;
this.streamElementIndex = streamElementIndex; this.streamElementIndex = streamElementIndex;
...@@ -110,6 +107,8 @@ public class DefaultSsChunkSource implements SsChunkSource { ...@@ -110,6 +107,8 @@ public class DefaultSsChunkSource implements SsChunkSource {
for (int i = 0; i < extractorWrappers.length; i++) { for (int i = 0; i < extractorWrappers.length; i++) {
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i); int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i);
Format format = streamElement.formats[manifestTrackIndex]; Format format = streamElement.formats[manifestTrackIndex];
TrackEncryptionBox[] trackEncryptionBoxes =
format.drmInitData != null ? manifest.protectionElement.trackEncryptionBoxes : null;
int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : 0; int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : 0;
Track track = new Track(manifestTrackIndex, streamElement.type, streamElement.timescale, Track track = new Track(manifestTrackIndex, streamElement.type, streamElement.timescale,
C.TIME_UNSET, manifest.durationUs, format, Track.TRANSFORMATION_NONE, C.TIME_UNSET, manifest.durationUs, format, Track.TRANSFORMATION_NONE,
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.source.smoothstreaming; package com.google.android.exoplayer2.source.smoothstreaming;
import android.support.annotation.Nullable; 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.chunk.ChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
...@@ -38,7 +37,6 @@ public interface SsChunkSource extends ChunkSource { ...@@ -38,7 +37,6 @@ public interface SsChunkSource extends ChunkSource {
* @param manifest The initial manifest. * @param manifest The initial manifest.
* @param streamElementIndex The index of the corresponding stream element in the manifest. * @param streamElementIndex The index of the corresponding stream element in the manifest.
* @param trackSelection The track selection. * @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. * @param transferListener The transfer listener which should be informed of any data transfers.
* May be null if no listener is available. * May be null if no listener is available.
* @return The created {@link SsChunkSource}. * @return The created {@link SsChunkSource}.
...@@ -48,7 +46,6 @@ public interface SsChunkSource extends ChunkSource { ...@@ -48,7 +46,6 @@ public interface SsChunkSource extends ChunkSource {
SsManifest manifest, SsManifest manifest,
int streamElementIndex, int streamElementIndex,
TrackSelection trackSelection, TrackSelection trackSelection,
TrackEncryptionBox[] trackEncryptionBoxes,
@Nullable TransferListener transferListener); @Nullable TransferListener transferListener);
} }
......
...@@ -29,7 +29,6 @@ import com.google.android.exoplayer2.source.TrackGroup; ...@@ -29,7 +29,6 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; 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;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
...@@ -44,8 +43,6 @@ import java.util.ArrayList; ...@@ -44,8 +43,6 @@ import java.util.ArrayList;
/* package */ final class SsMediaPeriod implements MediaPeriod, /* package */ final class SsMediaPeriod implements MediaPeriod,
SequenceableLoader.Callback<ChunkSampleStream<SsChunkSource>> { SequenceableLoader.Callback<ChunkSampleStream<SsChunkSource>> {
private static final int INITIALIZATION_VECTOR_SIZE = 8;
private final SsChunkSource.Factory chunkSourceFactory; private final SsChunkSource.Factory chunkSourceFactory;
private final @Nullable TransferListener transferListener; private final @Nullable TransferListener transferListener;
private final LoaderErrorThrower manifestLoaderErrorThrower; private final LoaderErrorThrower manifestLoaderErrorThrower;
...@@ -53,7 +50,6 @@ import java.util.ArrayList; ...@@ -53,7 +50,6 @@ import java.util.ArrayList;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final Allocator allocator; private final Allocator allocator;
private final TrackGroupArray trackGroups; private final TrackGroupArray trackGroups;
private final TrackEncryptionBox[] trackEncryptionBoxes;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private @Nullable Callback callback; private @Nullable Callback callback;
...@@ -71,6 +67,7 @@ import java.util.ArrayList; ...@@ -71,6 +67,7 @@ import java.util.ArrayList;
EventDispatcher eventDispatcher, EventDispatcher eventDispatcher,
LoaderErrorThrower manifestLoaderErrorThrower, LoaderErrorThrower manifestLoaderErrorThrower,
Allocator allocator) { Allocator allocator) {
this.manifest = manifest;
this.chunkSourceFactory = chunkSourceFactory; this.chunkSourceFactory = chunkSourceFactory;
this.transferListener = transferListener; this.transferListener = transferListener;
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
...@@ -78,18 +75,7 @@ import java.util.ArrayList; ...@@ -78,18 +75,7 @@ import java.util.ArrayList;
this.eventDispatcher = eventDispatcher; this.eventDispatcher = eventDispatcher;
this.allocator = allocator; this.allocator = allocator;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
trackGroups = buildTrackGroups(manifest); 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); sampleStreams = newSampleStreamArray(0);
compositeSequenceableLoader = compositeSequenceableLoader =
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
...@@ -229,7 +215,6 @@ import java.util.ArrayList; ...@@ -229,7 +215,6 @@ import java.util.ArrayList;
manifest, manifest,
streamElementIndex, streamElementIndex,
selection, selection,
trackEncryptionBoxes,
transferListener); transferListener);
return new ChunkSampleStream<>( return new ChunkSampleStream<>(
manifest.streamElements[streamElementIndex].type, manifest.streamElements[streamElementIndex].type,
...@@ -277,5 +262,4 @@ import java.util.ArrayList; ...@@ -277,5 +262,4 @@ import java.util.ArrayList;
data[firstPosition] = data[secondPosition]; data[firstPosition] = data[secondPosition];
data[secondPosition] = temp; data[secondPosition] = temp;
} }
} }
...@@ -533,7 +533,7 @@ public final class SsMediaSource extends BaseMediaSource ...@@ -533,7 +533,7 @@ public final class SsMediaSource extends BaseMediaSource
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
EventDispatcher eventDispatcher = createEventDispatcher(id); EventDispatcher eventDispatcher = createEventDispatcher(id);
SsMediaPeriod period = SsMediaPeriod period =
new SsMediaPeriod( new SsMediaPeriod(
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.manifest; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.manifest;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; 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.FilterableManifest;
import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -41,10 +42,12 @@ public class SsManifest implements FilterableManifest<SsManifest> { ...@@ -41,10 +42,12 @@ public class SsManifest implements FilterableManifest<SsManifest> {
public final UUID uuid; public final UUID uuid;
public final byte[] data; 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.uuid = uuid;
this.data = data; this.data = data;
this.trackEncryptionBoxes = trackEncryptionBoxes;
} }
} }
......
...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ParserException; ...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; 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.ProtectionElement;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
...@@ -397,9 +398,10 @@ public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> { ...@@ -397,9 +398,10 @@ public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> {
public static final String TAG = "Protection"; public static final String TAG = "Protection";
public static final String TAG_PROTECTION_HEADER = "ProtectionHeader"; public static final String TAG_PROTECTION_HEADER = "ProtectionHeader";
public static final String KEY_SYSTEM_ID = "SystemID"; public static final String KEY_SYSTEM_ID = "SystemID";
private static final int INITIALIZATION_VECTOR_SIZE = 8;
private boolean inProtectionHeader; private boolean inProtectionHeader;
private UUID uuid; private UUID uuid;
private byte[] initData; private byte[] initData;
...@@ -439,7 +441,44 @@ public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> { ...@@ -439,7 +441,44 @@ public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> {
@Override @Override
public Object build() { 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("<KID>") + 5, initDataString.indexOf("</KID>"));
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) { private static String stripCurlyBraces(String uuidString) {
......
...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.source.TrackGroup; ...@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; 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.SsManifestParser;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -42,7 +43,7 @@ public final class SsDownloadHelper extends DownloadHelper { ...@@ -42,7 +43,7 @@ public final class SsDownloadHelper extends DownloadHelper {
private @MonotonicNonNull SsManifest manifest; private @MonotonicNonNull SsManifest manifest;
public SsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { public SsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) {
this.uri = uri; this.uri = SsUtil.fixManifestUri(uri);;
this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestDataSourceFactory = manifestDataSourceFactory;
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
Duration="2300000000" TimeScale="10000000"> Duration="2300000000" TimeScale="10000000">
<Protection> <Protection>
<ProtectionHeader SystemID="9A04F079-9840-4286-AB92-E65BE0885F95"> <ProtectionHeader SystemID="9A04F079-9840-4286-AB92-E65BE0885F95">
<!-- Base 64-Encoded data omitted for clarity --> fgMAAAEAAQB0AzwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQgBhAFUATQBPAEcAYwBzAGgAVQBDAEQAZAB3ADMANABZAGMAawBmAFoAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBnADcATgBhAFIARABJAEkATwA5ADAAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAcwA6AC8ALwBUAC0ATwBOAEwASQBOAEUALgBEAFUATQBNAFkALQBTAEUAUgBWAEUAUgAvAEEAcgB0AGUAbQBpAHMATABpAGMAZQBuAHMAZQBTAGUAcgB2AGUAcgAvAFAAbABhAHkAUgBlAGEAZAB5AE0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwAQwBJAEQAPgAxADcANQA4ADIANgA8AC8AQwBJAEQAPgA8AEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4ANwAuADEALgAxADUANgA1AC4ANAA8AC8ASQBJAFMAXwBEAFIATQBfAFYARQBSAFMASQBPAE4APgA8AC8AQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A
</ProtectionHeader> </ProtectionHeader>
</Protection> </Protection>
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
Duration="2300000000" TimeScale="10000000"> Duration="2300000000" TimeScale="10000000">
<Protection> <Protection>
<ProtectionHeader SystemID="{9A04F079-9840-4286-AB92-E65BE0885F95}"> <ProtectionHeader SystemID="{9A04F079-9840-4286-AB92-E65BE0885F95}">
<!-- Base 64-Encoded data omitted for clarity --> fgMAAAEAAQB0AzwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQgBhAFUATQBPAEcAYwBzAGgAVQBDAEQAZAB3ADMANABZAGMAawBmAFoAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBnADcATgBhAFIARABJAEkATwA5ADAAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAcwA6AC8ALwBUAC0ATwBOAEwASQBOAEUALgBEAFUATQBNAFkALQBTAEUAUgBWAEUAUgAvAEEAcgB0AGUAbQBpAHMATABpAGMAZQBuAHMAZQBTAGUAcgB2AGUAcgAvAFAAbABhAHkAUgBlAGEAZAB5AE0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwAQwBJAEQAPgAxADcANQA4ADIANgA8AC8AQwBJAEQAPgA8AEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4ANwAuADEALgAxADUANgA1AC4ANAA8AC8ASQBJAFMAXwBEAFIATQBfAFYARQBSAFMASQBPAE4APgA8AC8AQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A
</ProtectionHeader> </ProtectionHeader>
</Protection> </Protection>
......
...@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; 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.offline.StreamKey;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement; 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.source.smoothstreaming.manifest.SsManifest.StreamElement;
...@@ -36,7 +37,7 @@ import org.robolectric.RobolectricTestRunner; ...@@ -36,7 +37,7 @@ import org.robolectric.RobolectricTestRunner;
public class SsManifestTest { public class SsManifestTest {
private static final ProtectionElement DUMMY_PROTECTION_ELEMENT = 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 @Test
public void testCopy() throws Exception { public void testCopy() throws Exception {
......
...@@ -137,23 +137,40 @@ public class DebugTextViewHelper implements Player.EventListener, Runnable { ...@@ -137,23 +137,40 @@ public class DebugTextViewHelper implements Player.EventListener, Runnable {
/** Returns a string containing video debugging information. */ /** Returns a string containing video debugging information. */
protected String getVideoString() { protected String getVideoString() {
Format format = player.getVideoFormat(); Format format = player.getVideoFormat();
if (format == null) { DecoderCounters decoderCounters = player.getVideoDecoderCounters();
if (format == null || decoderCounters == null) {
return ""; return "";
} }
return "\n" + format.sampleMimeType + "(id:" + format.id + " r:" + format.width + "x" return "\n"
+ format.height + getPixelAspectRatioString(format.pixelWidthHeightRatio) + format.sampleMimeType
+ getDecoderCountersBufferCountString(player.getVideoDecoderCounters()) + ")"; + "(id:"
+ format.id
+ " r:"
+ format.width
+ "x"
+ format.height
+ getPixelAspectRatioString(format.pixelWidthHeightRatio)
+ getDecoderCountersBufferCountString(decoderCounters)
+ ")";
} }
/** Returns a string containing audio debugging information. */ /** Returns a string containing audio debugging information. */
protected String getAudioString() { protected String getAudioString() {
Format format = player.getAudioFormat(); Format format = player.getAudioFormat();
if (format == null) { DecoderCounters decoderCounters = player.getAudioDecoderCounters();
if (format == null || decoderCounters == null) {
return ""; 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 + format.channelCount
+ getDecoderCountersBufferCountString(player.getAudioDecoderCounters()) + ")"; + getDecoderCountersBufferCountString(decoderCounters)
+ ")";
} }
private static String getDecoderCountersBufferCountString(DecoderCounters counters) { private static String getDecoderCountersBufferCountString(DecoderCounters counters) {
......
...@@ -126,6 +126,18 @@ public class PlayerNotificationManager { ...@@ -126,6 +126,18 @@ public class PlayerNotificationManager {
String getCurrentContentText(Player player); String getCurrentContentText(Player player);
/** /**
* Gets the content sub text for the current media item.
*
* <p>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. * Gets the large icon for the current media item.
* *
* <p>When a bitmap initially needs to be asynchronously loaded, a placeholder (or null) can be * <p>When a bitmap initially needs to be asynchronously loaded, a placeholder (or null) can be
...@@ -832,6 +844,7 @@ public class PlayerNotificationManager { ...@@ -832,6 +844,7 @@ public class PlayerNotificationManager {
// Set media specific notification properties from MediaDescriptionAdapter. // Set media specific notification properties from MediaDescriptionAdapter.
builder.setContentTitle(mediaDescriptionAdapter.getCurrentContentTitle(player)); builder.setContentTitle(mediaDescriptionAdapter.getCurrentContentTitle(player));
builder.setContentText(mediaDescriptionAdapter.getCurrentContentText(player)); builder.setContentText(mediaDescriptionAdapter.getCurrentContentText(player));
builder.setSubText(mediaDescriptionAdapter.getCurrentSubText(player));
if (largeIcon == null) { if (largeIcon == null) {
largeIcon = largeIcon =
mediaDescriptionAdapter.getCurrentLargeIcon( mediaDescriptionAdapter.getCurrentLargeIcon(
......
...@@ -679,8 +679,9 @@ public class PlayerView extends FrameLayout { ...@@ -679,8 +679,9 @@ public class PlayerView extends FrameLayout {
/** /**
* Sets whether the currently displayed video frame or media artwork is kept visible when the * 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 * 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 * media, the player transitioning to unprepared media, {@link Player#stop(boolean)} being called
* replaced or cleared by calling {@link #setPlayer(Player)}. * with {@code reset=true}, or the player being replaced or cleared by calling {@link
* #setPlayer(Player)}.
* *
* <p>If enabled, the currently displayed video frame or media artwork will be kept visible until * <p>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 * the player set on the view has been successfully prepared with new media and loaded enough of
......
...@@ -116,7 +116,7 @@ public class FakeMediaSource extends BaseMediaSource { ...@@ -116,7 +116,7 @@ public class FakeMediaSource extends BaseMediaSource {
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
assertThat(preparedSource).isTrue(); assertThat(preparedSource).isTrue();
assertThat(releasedSource).isFalse(); assertThat(releasedSource).isFalse();
int periodIndex = timeline.getIndexOfPeriod(id.periodUid); int periodIndex = timeline.getIndexOfPeriod(id.periodUid);
......
...@@ -142,15 +142,28 @@ public class MediaSourceTestRunner { ...@@ -142,15 +142,28 @@ public class MediaSourceTestRunner {
} }
/** /**
* Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator)} on the playback * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator, long)} with a zero
* thread, asserting that a non-null {@link MediaPeriod} is returned. * start position on the playback thread, asserting that a non-null {@link MediaPeriod} is
* returned.
* *
* @param periodId The id of the period to create. * @param periodId The id of the period to create.
* @return The created {@link MediaPeriod}. * @return The created {@link MediaPeriod}.
*/ */
public MediaPeriod createPeriod(final MediaPeriodId periodId) { 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]; 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(); assertThat(holder[0]).isNotNull();
return holder[0]; return holder[0];
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment