mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
commit
1c4ea26ff0
72 changed files with 1031 additions and 318 deletions
40
README.md
40
README.md
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 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:
|
||||||
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][].
|
||||||
[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
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,34 @@
|
||||||
# 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 @@
|
||||||
// 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;
|
||||||
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,
|
||||||
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.
|
||||||
castContext = CastContext.getSharedInstance(this);
|
try {
|
||||||
|
castContext = CastContext.getSharedInstance(this);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
Throwable cause = e.getCause();
|
||||||
|
while (cause != null) {
|
||||||
|
if (cause instanceof DynamiteModule.LoadingException) {
|
||||||
|
setContentView(R.layout.cast_context_error_message_layout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cause = cause.getCause();
|
||||||
|
}
|
||||||
|
// Unknown error. We propagate it.
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
setContentView(R.layout.main_activity);
|
setContentView(R.layout.main_activity);
|
||||||
|
|
||||||
|
|
@ -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,
|
||||||
@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();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?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 @@
|
||||||
|
|
||||||
<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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
// 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,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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,
|
||||||
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,
|
||||||
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 {
|
||||||
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
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
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 {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
||||||
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 {
|
||||||
* 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 {
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
|
|
||||||
/** 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 {
|
||||||
* 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 {
|
||||||
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;
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
|
|
||||||
@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
|
||||||
if (outputBuffer == null) {
|
if (outputBuffer == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
|
if (outputBuffer.skippedOutputBufferCount > 0) {
|
||||||
|
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
|
||||||
|
audioSink.handleDiscontinuity();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outputBuffer.isEndOfStream()) {
|
if (outputBuffer.isEndOfStream()) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
} 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;
|
||||||
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;
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
* @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;
|
||||||
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;
|
||||||
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;
|
||||||
* 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;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.extractor.mp4;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores extensible metadata with handler type 'mdta'. See also the QuickTime File Format
|
||||||
|
* Specification.
|
||||||
|
*/
|
||||||
|
public final class MdtaMetadataEntry implements Metadata.Entry {
|
||||||
|
|
||||||
|
/** The metadata key name. */
|
||||||
|
public final String key;
|
||||||
|
/** The payload. The interpretation of the value depends on {@link #typeIndicator}. */
|
||||||
|
public final byte[] value;
|
||||||
|
/** The four byte locale indicator. */
|
||||||
|
public final int localeIndicator;
|
||||||
|
/** The four byte type indicator. */
|
||||||
|
public final int typeIndicator;
|
||||||
|
|
||||||
|
/** Creates a new metadata entry for the specified metadata key/value. */
|
||||||
|
public MdtaMetadataEntry(String key, byte[] value, int localeIndicator, int typeIndicator) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
this.localeIndicator = localeIndicator;
|
||||||
|
this.typeIndicator = typeIndicator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MdtaMetadataEntry(Parcel in) {
|
||||||
|
key = Util.castNonNull(in.readString());
|
||||||
|
value = new byte[in.readInt()];
|
||||||
|
in.readByteArray(value);
|
||||||
|
localeIndicator = in.readInt();
|
||||||
|
typeIndicator = in.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
MdtaMetadataEntry other = (MdtaMetadataEntry) obj;
|
||||||
|
return key.equals(other.key)
|
||||||
|
&& Arrays.equals(value, other.value)
|
||||||
|
&& localeIndicator == other.localeIndicator
|
||||||
|
&& typeIndicator == other.typeIndicator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = 17;
|
||||||
|
result = 31 * result + key.hashCode();
|
||||||
|
result = 31 * result + Arrays.hashCode(value);
|
||||||
|
result = 31 * result + localeIndicator;
|
||||||
|
result = 31 * result + typeIndicator;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "mdta: key=" + key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parcelable implementation.
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeString(key);
|
||||||
|
dest.writeInt(value.length);
|
||||||
|
dest.writeByteArray(value);
|
||||||
|
dest.writeInt(localeIndicator);
|
||||||
|
dest.writeInt(typeIndicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Parcelable.Creator<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 @@
|
||||||
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;
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
// 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 {
|
||||||
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
|
||||||
|
|| atom == Atom.TYPE_stsz
|
||||||
|
|| atom == Atom.TYPE_stz2
|
||||||
|
|| atom == Atom.TYPE_stco
|
||||||
|
|| atom == Atom.TYPE_co64
|
||||||
|
|| atom == Atom.TYPE_tkhd
|
||||||
|
|| atom == Atom.TYPE_ftyp
|
||||||
|
|| atom == Atom.TYPE_udta
|
||||||
|
|| atom == Atom.TYPE_keys
|
||||||
|
|| atom == Atom.TYPE_ilst;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns whether the extractor should decode a container atom with type {@code atom}. */
|
||||||
* Returns whether the extractor should decode a container atom with type {@code atom}.
|
|
||||||
*/
|
|
||||||
private static boolean shouldParseContainerAtom(int atom) {
|
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;
|
||||||
*/
|
*/
|
||||||
/* 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;
|
||||||
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;
|
||||||
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;
|
||||||
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 {
|
||||||
|| 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> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 @@
|
||||||
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;
|
||||||
* 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
|
||||||
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
|
||||||
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
|
||||||
}
|
}
|
||||||
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
|
||||||
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
|
||||||
*/
|
*/
|
||||||
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
|
||||||
.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
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* @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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
@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
|
||||||
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;
|
||||||
} 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
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@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> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
||||||
* 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 {
|
||||||
*
|
*
|
||||||
* @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> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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> {
|
||||||
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> {
|
||||||
}
|
}
|
||||||
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 {
|
||||||
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 {
|
||||||
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;
|
||||||
|
|
@ -64,18 +64,6 @@ public final class CacheDataSink implements DataSink {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}.
|
|
||||||
*
|
|
||||||
* @param cache The cache into which data should be written.
|
|
||||||
* @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for
|
|
||||||
* a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into
|
|
||||||
* multiple cache files.
|
|
||||||
*/
|
|
||||||
public CacheDataSink(Cache cache, long maxCacheFileSize) {
|
|
||||||
this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}.
|
* Constructs a CacheDataSink using the {@link #DEFAULT_BUFFER_SIZE}.
|
||||||
*
|
*
|
||||||
|
|
@ -83,10 +71,9 @@ public final class CacheDataSink implements DataSink {
|
||||||
* @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 {
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param cache The cache into which data should be written.
|
|
||||||
* @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for a
|
|
||||||
* {@link DataSpec} whose size exceeds this value, then the data will be fragmented into
|
|
||||||
* multiple cache files.
|
|
||||||
* @param bufferSize The buffer size in bytes for writing to a cache file. A zero or negative
|
|
||||||
* value disables buffering.
|
|
||||||
* @param syncFileDescriptor Whether file descriptors are sync'd when closing output streams.
|
|
||||||
*/
|
|
||||||
public CacheDataSink(
|
|
||||||
Cache cache, long maxCacheFileSize, int bufferSize, boolean syncFileDescriptor) {
|
|
||||||
this.cache = Assertions.checkNotNull(cache);
|
this.cache = Assertions.checkNotNull(cache);
|
||||||
this.maxCacheFileSize = maxCacheFileSize;
|
this.maxCacheFileSize = maxCacheFileSize;
|
||||||
this.bufferSize = bufferSize;
|
this.bufferSize = bufferSize;
|
||||||
|
syncFileDescriptor = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether file descriptors are synced when closing output streams.
|
||||||
|
*
|
||||||
|
* <p>This method is experimental, and will be renamed or removed in a future release. It should
|
||||||
|
* only be called before the renderer is used.
|
||||||
|
*
|
||||||
|
* @param syncFileDescriptor Whether file descriptors are synced when closing output streams.
|
||||||
|
*/
|
||||||
|
public void experimental_setSyncFileDescriptor(boolean syncFileDescriptor) {
|
||||||
this.syncFileDescriptor = syncFileDescriptor;
|
this.syncFileDescriptor = syncFileDescriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
// 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 {
|
||||||
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:
|
||||||
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:
|
||||||
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:
|
||||||
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:
|
||||||
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:
|
||||||
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:
|
||||||
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:
|
||||||
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:
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.extractor.mp4;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
|
||||||
|
/** Test for {@link MdtaMetadataEntry}. */
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
public final class MdtaMetadataEntryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParcelable() {
|
||||||
|
MdtaMetadataEntry mdtaMetadataEntryToParcel =
|
||||||
|
new MdtaMetadataEntry("test", new byte[] {1, 2}, 3, 4);
|
||||||
|
|
||||||
|
Parcel parcel = Parcel.obtain();
|
||||||
|
mdtaMetadataEntryToParcel.writeToParcel(parcel, 0);
|
||||||
|
parcel.setDataPosition(0);
|
||||||
|
|
||||||
|
MdtaMetadataEntry mdtaMetadataEntryFromParcel =
|
||||||
|
MdtaMetadataEntry.CREATOR.createFromParcel(parcel);
|
||||||
|
assertThat(mdtaMetadataEntryFromParcel).isEqualTo(mdtaMetadataEntryToParcel);
|
||||||
|
|
||||||
|
parcel.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
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 =
|
||||||
adaptationSetUsedFlags[extraIndex] = true;
|
idToIndexMap.get(
|
||||||
adaptationSetIndices[1 + j] = extraIndex;
|
Integer.parseInt(extraAdaptationSetIds[j]), /* valueIfKeyNotFound= */ -1);
|
||||||
|
if (extraIndex != -1) {
|
||||||
|
adaptationSetUsedFlags[extraIndex] = true;
|
||||||
|
adaptationSetIndices[outputIndex] = extraIndex;
|
||||||
|
outputIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (outputIndex < adaptationSetIndices.length) {
|
||||||
|
adaptationSetIndices = Arrays.copyOf(adaptationSetIndices, outputIndex);
|
||||||
}
|
}
|
||||||
groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices;
|
groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
/* 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
|
||||||
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 {
|
||||||
+ "#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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
* @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 {
|
||||||
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 @@
|
||||||
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 {
|
||||||
* @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 {
|
||||||
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;
|
||||||
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;
|
||||||
/* 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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
data[firstPosition] = data[secondPosition];
|
data[firstPosition] = data[secondPosition];
|
||||||
data[secondPosition] = temp;
|
data[secondPosition] = temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
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> {
|
||||||
|
|
||||||
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;
|
||||||
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> {
|
||||||
|
|
||||||
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> {
|
||||||
|
|
||||||
@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;
|
||||||
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 {
|
||||||
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 @@
|
||||||
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 @@
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
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 {
|
||||||
/** 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) {
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,18 @@ public class PlayerNotificationManager {
|
||||||
@Nullable
|
@Nullable
|
||||||
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.
|
||||||
*
|
*
|
||||||
|
|
@ -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 {
|
||||||
/**
|
/**
|
||||||
* 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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue