mirror of
https://github.com/samsonjs/media.git
synced 2026-03-30 10:15:48 +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 ###
|
||||
|
||||
#### 1. Add repositories ####
|
||||
|
||||
The easiest way to get started using ExoPlayer is to add it as a gradle
|
||||
dependency. You need to make sure you have the Google and JCenter repositories
|
||||
included in the `build.gradle` file in the root of your project:
|
||||
|
|
@ -38,6 +40,8 @@ repositories {
|
|||
}
|
||||
```
|
||||
|
||||
#### 2. Add ExoPlayer module dependencies ####
|
||||
|
||||
Next add a dependency in the `build.gradle` file of your app module. The
|
||||
following will add a dependency to the full library:
|
||||
|
||||
|
|
@ -45,15 +49,7 @@ following will add a dependency to the full library:
|
|||
implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
|
||||
```
|
||||
|
||||
where `2.X.X` is your preferred version. If not enabled already, you also need
|
||||
to turn on Java 8 support in all `build.gradle` files depending on ExoPlayer, by
|
||||
adding the following to the `android` section:
|
||||
|
||||
```gradle
|
||||
compileOptions {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
```
|
||||
where `2.X.X` is your preferred version.
|
||||
|
||||
As an alternative to the full library, you can depend on only the library
|
||||
modules that you actually need. For example the following will add dependencies
|
||||
|
|
@ -87,6 +83,32 @@ JCenter can be found on [Bintray][].
|
|||
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
|
||||
[Bintray]: https://bintray.com/google/exoplayer
|
||||
|
||||
#### 3. Turn on Java 8 support ####
|
||||
|
||||
If not enabled already, you also need to turn on Java 8 support in all
|
||||
`build.gradle` files depending on ExoPlayer, by adding the following to the
|
||||
`android` section:
|
||||
|
||||
```gradle
|
||||
compileOptions {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
```
|
||||
|
||||
Note that if you want to use Java 8 features in your own code, the following
|
||||
additional options need to be set:
|
||||
|
||||
```gradle
|
||||
// For Java compilers:
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
// For Kotlin compilers:
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
```
|
||||
|
||||
### Locally ###
|
||||
|
||||
Cloning the repository and depending on the modules locally is required when
|
||||
|
|
|
|||
|
|
@ -1,5 +1,34 @@
|
|||
# Release notes #
|
||||
|
||||
### 2.9.4 ###
|
||||
|
||||
* IMA extension: Clear ads loader listeners on release
|
||||
([#4114](https://github.com/google/ExoPlayer/issues/4114)).
|
||||
* SmoothStreaming: Fix support for subtitles in DRM protected streams
|
||||
([#5378](https://github.com/google/ExoPlayer/issues/5378)).
|
||||
* FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior
|
||||
of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)).
|
||||
* GVR extension: upgrade GVR SDK dependency to 1.190.0.
|
||||
* Associate fatal player errors of type SOURCE with the loading source in
|
||||
`AnalyticsListener.EventTime`
|
||||
([#5407](https://github.com/google/ExoPlayer/issues/5407)).
|
||||
* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where
|
||||
using lazy preparation in `ConcatenatingMediaSource` with an
|
||||
`ExtractorMediaSource` overrides initial seek positions
|
||||
([#5350](https://github.com/google/ExoPlayer/issues/5350)).
|
||||
* Add subtext to the `MediaDescriptionAdapter` of the
|
||||
`PlayerNotificationManager`.
|
||||
* Add workaround for video quality problems with Amlogic decoders
|
||||
([#5003](https://github.com/google/ExoPlayer/issues/5003)).
|
||||
* Fix issue where sending callbacks for playlist changes may cause problems
|
||||
because of parallel player access
|
||||
([#5240](https://github.com/google/ExoPlayer/issues/5240)).
|
||||
* Fix issue with reusing a `ClippingMediaSource` with an inner
|
||||
`ExtractorMediaSource` and a non-zero start position
|
||||
([#5351](https://github.com/google/ExoPlayer/issues/5351)).
|
||||
* Fix issue where uneven track durations in MP4 streams can cause OOM problems
|
||||
([#3670](https://github.com/google/ExoPlayer/issues/3670)).
|
||||
|
||||
### 2.9.3 ###
|
||||
|
||||
* Captions: Support PNG subtitles in SMPTE-TT
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.9.3'
|
||||
releaseVersionCode = 2009003
|
||||
releaseVersion = '2.9.4'
|
||||
releaseVersionCode = 2009004
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 14 because various
|
||||
// components provided by the library may be of use on older devices.
|
||||
// However, please note that the core media playback functionality provided
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import com.google.android.exoplayer2.ui.PlayerControlView;
|
|||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import com.google.android.gms.dynamite.DynamiteModule;
|
||||
|
||||
/**
|
||||
* An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}.
|
||||
|
|
@ -61,7 +62,20 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Getting the cast context later than onStart can cause device discovery not to take place.
|
||||
castContext = CastContext.getSharedInstance(this);
|
||||
try {
|
||||
castContext = CastContext.getSharedInstance(this);
|
||||
} catch (RuntimeException e) {
|
||||
Throwable cause = e.getCause();
|
||||
while (cause != null) {
|
||||
if (cause instanceof DynamiteModule.LoadingException) {
|
||||
setContentView(R.layout.cast_context_error_message_layout);
|
||||
return;
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
// Unknown error. We propagate it.
|
||||
throw e;
|
||||
}
|
||||
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
|
|
@ -91,6 +105,10 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (castContext == null) {
|
||||
// There is no Cast context to work with. Do nothing.
|
||||
return;
|
||||
}
|
||||
playerManager =
|
||||
PlayerManager.createPlayerManager(
|
||||
/* queuePositionListener= */ this,
|
||||
|
|
@ -104,6 +122,10 @@ public class MainActivity extends AppCompatActivity implements OnClickListener,
|
|||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (castContext == null) {
|
||||
// Nothing to release.
|
||||
return;
|
||||
}
|
||||
mediaQueueListAdapter.notifyItemRangeRemoved(0, mediaQueueListAdapter.getItemCount());
|
||||
mediaQueueList.setAdapter(null);
|
||||
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="cast_context_error">Failed to get Cast context. Try updating Google Play Services and restart the app.</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api 'com.google.android.gms:play-services-cast-framework:16.0.3'
|
||||
api 'com.google.android.gms:play-services-cast-framework:16.1.2'
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ import java.util.List;
|
|||
private static final int OUTPUT_BUFFER_SIZE_16BIT = 65536;
|
||||
private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2;
|
||||
|
||||
// Error codes matching ffmpeg_jni.cc.
|
||||
private static final int DECODER_ERROR_INVALID_DATA = -1;
|
||||
private static final int DECODER_ERROR_OTHER = -2;
|
||||
|
||||
private final String codecName;
|
||||
private final @Nullable byte[] extraData;
|
||||
private final @C.Encoding int encoding;
|
||||
|
|
@ -106,8 +110,14 @@ import java.util.List;
|
|||
int inputSize = inputData.limit();
|
||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
|
||||
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
|
||||
if (result < 0) {
|
||||
return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result);
|
||||
if (result == DECODER_ERROR_INVALID_DATA) {
|
||||
// Treat invalid data errors as non-fatal to match the behavior of MediaCodec. No output will
|
||||
// be produced for this buffer, so mark it as decode-only to ensure that the audio sink's
|
||||
// position is reset when more audio is produced.
|
||||
outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
|
||||
return null;
|
||||
} else if (result == DECODER_ERROR_OTHER) {
|
||||
return new FfmpegDecoderException("Error decoding (see logcat).");
|
||||
}
|
||||
if (!hasOutputFormat) {
|
||||
channelCount = ffmpegGetChannelCount(nativeContext);
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16;
|
|||
// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT.
|
||||
static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT;
|
||||
|
||||
// Error codes matching FfmpegDecoder.java.
|
||||
static const int DECODER_ERROR_INVALID_DATA = -1;
|
||||
static const int DECODER_ERROR_OTHER = -2;
|
||||
|
||||
/**
|
||||
* Returns the AVCodec with the specified name, or NULL if it is not available.
|
||||
*/
|
||||
|
|
@ -79,7 +83,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
|
|||
|
||||
/**
|
||||
* Decodes the packet into the output buffer, returning the number of bytes
|
||||
* written, or a negative value in the case of an error.
|
||||
* written, or a negative DECODER_ERROR constant value in the case of an error.
|
||||
*/
|
||||
int decodePacket(AVCodecContext *context, AVPacket *packet,
|
||||
uint8_t *outputBuffer, int outputSize);
|
||||
|
|
@ -238,6 +242,7 @@ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, jbyteArray extraData,
|
|||
context->channels = rawChannelCount;
|
||||
context->channel_layout = av_get_default_channel_layout(rawChannelCount);
|
||||
}
|
||||
context->err_recognition = AV_EF_IGNORE_ERR;
|
||||
int result = avcodec_open2(context, codec, NULL);
|
||||
if (result < 0) {
|
||||
logError("avcodec_open2", result);
|
||||
|
|
@ -254,7 +259,8 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
|
|||
result = avcodec_send_packet(context, packet);
|
||||
if (result) {
|
||||
logError("avcodec_send_packet", result);
|
||||
return result;
|
||||
return result == AVERROR_INVALIDDATA ? DECODER_ERROR_INVALID_DATA
|
||||
: DECODER_ERROR_OTHER;
|
||||
}
|
||||
|
||||
// Dequeue output data until it runs out.
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ android {
|
|||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
implementation 'com.google.vr:sdk-audio:1.80.0'
|
||||
api 'com.google.vr:sdk-base:1.190.0'
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
|||
|
|
@ -597,6 +597,8 @@ public final class ImaAdsLoader
|
|||
adsManager.destroy();
|
||||
adsManager = null;
|
||||
}
|
||||
adsLoader.removeAdsLoadedListener(/* adsLoadedListener= */ this);
|
||||
adsLoader.removeAdErrorListener(/* adErrorListener= */ this);
|
||||
imaPausedContent = false;
|
||||
imaAdState = IMA_AD_STATE_NONE;
|
||||
pendingAdLoadError = null;
|
||||
|
|
|
|||
|
|
@ -97,8 +97,8 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn
|
|||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
return adsMediaSource.createPeriod(id, allocator);
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
return adsMediaSource.createPeriod(id, allocator, startPositionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -64,14 +64,17 @@ import java.util.Set;
|
|||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVastMediaWidth() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVastMediaHeight() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVastMediaBitrate() {
|
||||
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
|
||||
application code.
|
||||
|
||||
`DefaultDataSource` will automatically use uses the RTMP extension whenever it's
|
||||
`DefaultDataSource` will automatically use the RTMP extension whenever it's
|
||||
available. Hence if your application is using `DefaultDataSource` or
|
||||
`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as
|
||||
adding a dependency to the RTMP extension as described above. No changes to your
|
||||
|
|
|
|||
|
|
@ -460,8 +460,8 @@ public final class C {
|
|||
|
||||
/**
|
||||
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link
|
||||
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_ENCRYPTED} and
|
||||
* {@link #BUFFER_FLAG_DECODE_ONLY}.
|
||||
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE},
|
||||
* {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}.
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
|
|
@ -470,6 +470,7 @@ public final class C {
|
|||
value = {
|
||||
BUFFER_FLAG_KEY_FRAME,
|
||||
BUFFER_FLAG_END_OF_STREAM,
|
||||
BUFFER_FLAG_LAST_SAMPLE,
|
||||
BUFFER_FLAG_ENCRYPTED,
|
||||
BUFFER_FLAG_DECODE_ONLY
|
||||
})
|
||||
|
|
@ -482,6 +483,8 @@ public final class C {
|
|||
* Flag for empty buffers that signal that the end of the stream was reached.
|
||||
*/
|
||||
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
||||
/** Indicates that a buffer is known to contain the last media sample of the stream. */
|
||||
public static final int BUFFER_FLAG_LAST_SAMPLE = 1 << 29; // 0x20000000
|
||||
/** Indicates that a buffer is (at least partially) encrypted. */
|
||||
public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000
|
||||
/** Indicates that a buffer should be decoded but not rendered. */
|
||||
|
|
@ -896,6 +899,26 @@ public final class C {
|
|||
*/
|
||||
public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
|
||||
|
||||
/** Video projection types. */
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({
|
||||
Format.NO_VALUE,
|
||||
PROJECTION_RECTANGULAR,
|
||||
PROJECTION_EQUIRECTANGULAR,
|
||||
PROJECTION_CUBEMAP,
|
||||
PROJECTION_MESH
|
||||
})
|
||||
public @interface Projection {}
|
||||
/** Conventional rectangular projection. */
|
||||
public static final int PROJECTION_RECTANGULAR = 0;
|
||||
/** Equirectangular spherical projection. */
|
||||
public static final int PROJECTION_EQUIRECTANGULAR = 1;
|
||||
/** Cube map projection. */
|
||||
public static final int PROJECTION_CUBEMAP = 2;
|
||||
/** 3-D mesh projection. */
|
||||
public static final int PROJECTION_MESH = 3;
|
||||
|
||||
/**
|
||||
* Priority for media playback.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
|
|||
|
||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||
public static final String VERSION = "2.9.3";
|
||||
public static final String VERSION = "2.9.4";
|
||||
|
||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.3";
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.4";
|
||||
|
||||
/**
|
||||
* The version of the library expressed as an integer, for example 1002003.
|
||||
|
|
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
|
|||
* integer version 123045006 (123-045-006).
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final int VERSION_INT = 2009003;
|
||||
public static final int VERSION_INT = 2009004;
|
||||
|
||||
/**
|
||||
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
||||
|
|
|
|||
|
|
@ -1181,6 +1181,37 @@ public final class Format implements Parcelable {
|
|||
metadata);
|
||||
}
|
||||
|
||||
public Format copyWithFrameRate(float frameRate) {
|
||||
return new Format(
|
||||
id,
|
||||
label,
|
||||
containerMimeType,
|
||||
sampleMimeType,
|
||||
codecs,
|
||||
bitrate,
|
||||
maxInputSize,
|
||||
width,
|
||||
height,
|
||||
frameRate,
|
||||
rotationDegrees,
|
||||
pixelWidthHeightRatio,
|
||||
projectionData,
|
||||
stereoMode,
|
||||
colorInfo,
|
||||
channelCount,
|
||||
sampleRate,
|
||||
pcmEncoding,
|
||||
encoderDelay,
|
||||
encoderPadding,
|
||||
selectionFlags,
|
||||
language,
|
||||
accessibilityChannel,
|
||||
subsampleOffsetUs,
|
||||
initializationData,
|
||||
drmInitData,
|
||||
metadata);
|
||||
}
|
||||
|
||||
public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {
|
||||
return new Format(
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ import com.google.android.exoplayer2.util.Log;
|
|||
this.info = info;
|
||||
sampleStreams = new SampleStream[rendererCapabilities.length];
|
||||
mayRetainStreamFlags = new boolean[rendererCapabilities.length];
|
||||
MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, allocator);
|
||||
MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, allocator, info.startPositionUs);
|
||||
if (info.id.endPositionUs != C.TIME_END_OF_SOURCE) {
|
||||
mediaPeriod =
|
||||
new ClippingMediaPeriod(
|
||||
|
|
|
|||
|
|
@ -94,25 +94,25 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
|
||||
private final AudioFocusManager audioFocusManager;
|
||||
|
||||
private Format videoFormat;
|
||||
private Format audioFormat;
|
||||
@Nullable private Format videoFormat;
|
||||
@Nullable private Format audioFormat;
|
||||
|
||||
private Surface surface;
|
||||
@Nullable private Surface surface;
|
||||
private boolean ownsSurface;
|
||||
private @C.VideoScalingMode int videoScalingMode;
|
||||
private SurfaceHolder surfaceHolder;
|
||||
private TextureView textureView;
|
||||
@Nullable private SurfaceHolder surfaceHolder;
|
||||
@Nullable private TextureView textureView;
|
||||
private int surfaceWidth;
|
||||
private int surfaceHeight;
|
||||
private DecoderCounters videoDecoderCounters;
|
||||
private DecoderCounters audioDecoderCounters;
|
||||
@Nullable private DecoderCounters videoDecoderCounters;
|
||||
@Nullable private DecoderCounters audioDecoderCounters;
|
||||
private int audioSessionId;
|
||||
private AudioAttributes audioAttributes;
|
||||
private float audioVolume;
|
||||
private MediaSource mediaSource;
|
||||
@Nullable private MediaSource mediaSource;
|
||||
private List<Cue> currentCues;
|
||||
private VideoFrameMetadataListener videoFrameMetadataListener;
|
||||
private CameraMotionListener cameraMotionListener;
|
||||
@Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
|
||||
@Nullable private CameraMotionListener cameraMotionListener;
|
||||
private boolean hasNotifiedFullWrongThreadWarning;
|
||||
|
||||
/**
|
||||
|
|
@ -558,30 +558,26 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
setPlaybackParameters(playbackParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the video format currently being played, or null if no video is being played.
|
||||
*/
|
||||
/** Returns the video format currently being played, or null if no video is being played. */
|
||||
@Nullable
|
||||
public Format getVideoFormat() {
|
||||
return videoFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the audio format currently being played, or null if no audio is being played.
|
||||
*/
|
||||
/** Returns the audio format currently being played, or null if no audio is being played. */
|
||||
@Nullable
|
||||
public Format getAudioFormat() {
|
||||
return audioFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link DecoderCounters} for video, or null if no video is being played.
|
||||
*/
|
||||
/** Returns {@link DecoderCounters} for video, or null if no video is being played. */
|
||||
@Nullable
|
||||
public DecoderCounters getVideoDecoderCounters() {
|
||||
return videoDecoderCounters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link DecoderCounters} for audio, or null if no audio is being played.
|
||||
*/
|
||||
/** Returns {@link DecoderCounters} for audio, or null if no audio is being played. */
|
||||
@Nullable
|
||||
public DecoderCounters getAudioDecoderCounters() {
|
||||
return audioDecoderCounters;
|
||||
}
|
||||
|
|
@ -1048,7 +1044,8 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getCurrentManifest() {
|
||||
@Nullable
|
||||
public Object getCurrentManifest() {
|
||||
verifyApplicationThread();
|
||||
return player.getCurrentManifest();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -488,7 +488,10 @@ public class AnalyticsCollector
|
|||
|
||||
@Override
|
||||
public final void onPlayerError(ExoPlaybackException error) {
|
||||
EventTime eventTime = generatePlayingMediaPeriodEventTime();
|
||||
EventTime eventTime =
|
||||
error.type == ExoPlaybackException.TYPE_SOURCE
|
||||
? generateLoadingMediaPeriodEventTime()
|
||||
: generatePlayingMediaPeriodEventTime();
|
||||
for (AnalyticsListener listener : listeners) {
|
||||
listener.onPlayerError(eventTime, error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -366,7 +366,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
if (outputBuffer == null) {
|
||||
return false;
|
||||
}
|
||||
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
|
||||
if (outputBuffer.skippedOutputBufferCount > 0) {
|
||||
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
|
||||
audioSink.handleDiscontinuity();
|
||||
}
|
||||
}
|
||||
|
||||
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_LANGUAGE = 0x22B59C;
|
||||
private static final int ID_PROJECTION = 0x7670;
|
||||
private static final int ID_PROJECTION_TYPE = 0x7671;
|
||||
private static final int ID_PROJECTION_PRIVATE = 0x7672;
|
||||
private static final int ID_PROJECTION_POSE_YAW = 0x7673;
|
||||
private static final int ID_PROJECTION_POSE_PITCH = 0x7674;
|
||||
private static final int ID_PROJECTION_POSE_ROLL = 0x7675;
|
||||
private static final int ID_STEREO_MODE = 0x53B8;
|
||||
private static final int ID_COLOUR = 0x55B0;
|
||||
private static final int ID_COLOUR_RANGE = 0x55B9;
|
||||
|
|
@ -760,6 +764,24 @@ public final class MatroskaExtractor implements Extractor {
|
|||
case ID_MAX_FALL:
|
||||
currentTrack.maxFrameAverageLuminance = (int) value;
|
||||
break;
|
||||
case ID_PROJECTION_TYPE:
|
||||
switch ((int) value) {
|
||||
case 0:
|
||||
currentTrack.projectionType = C.PROJECTION_RECTANGULAR;
|
||||
break;
|
||||
case 1:
|
||||
currentTrack.projectionType = C.PROJECTION_EQUIRECTANGULAR;
|
||||
break;
|
||||
case 2:
|
||||
currentTrack.projectionType = C.PROJECTION_CUBEMAP;
|
||||
break;
|
||||
case 3:
|
||||
currentTrack.projectionType = C.PROJECTION_MESH;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -803,6 +825,15 @@ public final class MatroskaExtractor implements Extractor {
|
|||
case ID_LUMNINANCE_MIN:
|
||||
currentTrack.minMasteringLuminance = (float) value;
|
||||
break;
|
||||
case ID_PROJECTION_POSE_YAW:
|
||||
currentTrack.projectionPoseYaw = (float) value;
|
||||
break;
|
||||
case ID_PROJECTION_POSE_PITCH:
|
||||
currentTrack.projectionPosePitch = (float) value;
|
||||
break;
|
||||
case ID_PROJECTION_POSE_ROLL:
|
||||
currentTrack.projectionPoseRoll = (float) value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -1465,6 +1496,7 @@ public final class MatroskaExtractor implements Extractor {
|
|||
case ID_COLOUR_PRIMARIES:
|
||||
case ID_MAX_CLL:
|
||||
case ID_MAX_FALL:
|
||||
case ID_PROJECTION_TYPE:
|
||||
return TYPE_UNSIGNED_INT;
|
||||
case ID_DOC_TYPE:
|
||||
case ID_NAME:
|
||||
|
|
@ -1491,6 +1523,9 @@ public final class MatroskaExtractor implements Extractor {
|
|||
case ID_WHITE_POINT_CHROMATICITY_Y:
|
||||
case ID_LUMNINANCE_MAX:
|
||||
case ID_LUMNINANCE_MIN:
|
||||
case ID_PROJECTION_POSE_YAW:
|
||||
case ID_PROJECTION_POSE_PITCH:
|
||||
case ID_PROJECTION_POSE_ROLL:
|
||||
return TYPE_FLOAT;
|
||||
default:
|
||||
return TYPE_UNKNOWN;
|
||||
|
|
@ -1631,6 +1666,10 @@ public final class MatroskaExtractor implements Extractor {
|
|||
public int displayWidth = Format.NO_VALUE;
|
||||
public int displayHeight = Format.NO_VALUE;
|
||||
public int displayUnit = DISPLAY_UNIT_PIXELS;
|
||||
@C.Projection public int projectionType = Format.NO_VALUE;
|
||||
public float projectionPoseYaw = 0f;
|
||||
public float projectionPosePitch = 0f;
|
||||
public float projectionPoseRoll = 0f;
|
||||
public byte[] projectionData = null;
|
||||
@C.StereoMode
|
||||
public int stereoMode = Format.NO_VALUE;
|
||||
|
|
@ -1850,6 +1889,21 @@ public final class MatroskaExtractor implements Extractor {
|
|||
} else if ("htc_video_rotA-270".equals(name)) {
|
||||
rotationDegrees = 270;
|
||||
}
|
||||
if (projectionType == C.PROJECTION_RECTANGULAR
|
||||
&& Float.compare(projectionPoseYaw, 0f) == 0
|
||||
&& Float.compare(projectionPosePitch, 0f) == 0) {
|
||||
// The range of projectionPoseRoll is [-180, 180].
|
||||
if (Float.compare(projectionPoseRoll, 0f) == 0) {
|
||||
rotationDegrees = 0;
|
||||
} else if (Float.compare(projectionPosePitch, 90f) == 0) {
|
||||
rotationDegrees = 90;
|
||||
} else if (Float.compare(projectionPosePitch, -180f) == 0
|
||||
|| Float.compare(projectionPosePitch, 180f) == 0) {
|
||||
rotationDegrees = 180;
|
||||
} else if (Float.compare(projectionPosePitch, -90f) == 0) {
|
||||
rotationDegrees = 270;
|
||||
}
|
||||
}
|
||||
format =
|
||||
Format.createVideoSampleFormat(
|
||||
Integer.toString(trackId),
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import java.util.ArrayList;
|
|||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("ConstantField")
|
||||
/* package*/ abstract class Atom {
|
||||
@SuppressWarnings({"ConstantField", "ConstantCaseForConstants"})
|
||||
/* package */ abstract class Atom {
|
||||
|
||||
/**
|
||||
* Size of an atom header, in bytes.
|
||||
|
|
@ -130,6 +130,7 @@ import java.util.List;
|
|||
public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb");
|
||||
public static final int TYPE_udta = Util.getIntegerCodeForString("udta");
|
||||
public static final int TYPE_meta = Util.getIntegerCodeForString("meta");
|
||||
public static final int TYPE_keys = Util.getIntegerCodeForString("keys");
|
||||
public static final int TYPE_ilst = Util.getIntegerCodeForString("ilst");
|
||||
public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
|
||||
public static final int TYPE_name = Util.getIntegerCodeForString("name");
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.mp4;
|
|||
|
||||
import static com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
|
|
@ -39,7 +40,7 @@ import java.util.Collections;
|
|||
import java.util.List;
|
||||
|
||||
/** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */
|
||||
@SuppressWarnings("ConstantField")
|
||||
@SuppressWarnings({"ConstantField", "ConstantCaseForConstants"})
|
||||
/* package */ final class AtomParsers {
|
||||
|
||||
private static final String TAG = "AtomParsers";
|
||||
|
|
@ -51,6 +52,7 @@ import java.util.List;
|
|||
private static final int TYPE_subt = Util.getIntegerCodeForString("subt");
|
||||
private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
|
||||
private static final int TYPE_meta = Util.getIntegerCodeForString("meta");
|
||||
private static final int TYPE_mdta = Util.getIntegerCodeForString("mdta");
|
||||
|
||||
/**
|
||||
* The threshold number of samples to trim from the start/end of an audio track when applying an
|
||||
|
|
@ -77,7 +79,7 @@ import java.util.List;
|
|||
DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime)
|
||||
throws ParserException {
|
||||
Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
|
||||
int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data);
|
||||
int trackType = getTrackTypeForHdlr(parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data));
|
||||
if (trackType == C.TRACK_TYPE_UNKNOWN) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -485,6 +487,7 @@ import java.util.List;
|
|||
* @param isQuickTime True for QuickTime media. False otherwise.
|
||||
* @return Parsed metadata, or null.
|
||||
*/
|
||||
@Nullable
|
||||
public static Metadata parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) {
|
||||
if (isQuickTime) {
|
||||
// Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and
|
||||
|
|
@ -499,14 +502,69 @@ import java.util.List;
|
|||
int atomType = udtaData.readInt();
|
||||
if (atomType == Atom.TYPE_meta) {
|
||||
udtaData.setPosition(atomPosition);
|
||||
return parseMetaAtom(udtaData, atomPosition + atomSize);
|
||||
return parseUdtaMeta(udtaData, atomPosition + atomSize);
|
||||
}
|
||||
udtaData.skipBytes(atomSize - Atom.HEADER_SIZE);
|
||||
udtaData.setPosition(atomPosition + atomSize);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Metadata parseMetaAtom(ParsableByteArray meta, int limit) {
|
||||
/**
|
||||
* Parses a metadata meta atom if it contains metadata with handler 'mdta'.
|
||||
*
|
||||
* @param meta The metadata atom to decode.
|
||||
* @return Parsed metadata, or null.
|
||||
*/
|
||||
@Nullable
|
||||
public static Metadata parseMdtaFromMeta(Atom.ContainerAtom meta) {
|
||||
Atom.LeafAtom hdlrAtom = meta.getLeafAtomOfType(Atom.TYPE_hdlr);
|
||||
Atom.LeafAtom keysAtom = meta.getLeafAtomOfType(Atom.TYPE_keys);
|
||||
Atom.LeafAtom ilstAtom = meta.getLeafAtomOfType(Atom.TYPE_ilst);
|
||||
if (hdlrAtom == null
|
||||
|| keysAtom == null
|
||||
|| ilstAtom == null
|
||||
|| AtomParsers.parseHdlr(hdlrAtom.data) != TYPE_mdta) {
|
||||
// There isn't enough information to parse the metadata, or the handler type is unexpected.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse metadata keys.
|
||||
ParsableByteArray keys = keysAtom.data;
|
||||
keys.setPosition(Atom.FULL_HEADER_SIZE);
|
||||
int entryCount = keys.readInt();
|
||||
String[] keyNames = new String[entryCount];
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
int entrySize = keys.readInt();
|
||||
keys.skipBytes(4); // keyNamespace
|
||||
int keySize = entrySize - 8;
|
||||
keyNames[i] = keys.readString(keySize);
|
||||
}
|
||||
|
||||
// Parse metadata items.
|
||||
ParsableByteArray ilst = ilstAtom.data;
|
||||
ilst.setPosition(Atom.HEADER_SIZE);
|
||||
ArrayList<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);
|
||||
while (meta.getPosition() < limit) {
|
||||
int atomPosition = meta.getPosition();
|
||||
|
|
@ -516,11 +574,12 @@ import java.util.List;
|
|||
meta.setPosition(atomPosition);
|
||||
return parseIlst(meta, atomPosition + atomSize);
|
||||
}
|
||||
meta.skipBytes(atomSize - Atom.HEADER_SIZE);
|
||||
meta.setPosition(atomPosition + atomSize);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Metadata parseIlst(ParsableByteArray ilst, int limit) {
|
||||
ilst.skipBytes(Atom.HEADER_SIZE);
|
||||
ArrayList<Metadata.Entry> entries = new ArrayList<>();
|
||||
|
|
@ -610,19 +669,22 @@ import java.util.List;
|
|||
* Parses an hdlr atom.
|
||||
*
|
||||
* @param hdlr The hdlr atom to decode.
|
||||
* @return The track type.
|
||||
* @return The handler value.
|
||||
*/
|
||||
private static int parseHdlr(ParsableByteArray hdlr) {
|
||||
hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
|
||||
int trackType = hdlr.readInt();
|
||||
if (trackType == TYPE_soun) {
|
||||
return hdlr.readInt();
|
||||
}
|
||||
|
||||
/** Returns the track type for a given handler value. */
|
||||
private static int getTrackTypeForHdlr(int hdlr) {
|
||||
if (hdlr == TYPE_soun) {
|
||||
return C.TRACK_TYPE_AUDIO;
|
||||
} else if (trackType == TYPE_vide) {
|
||||
} else if (hdlr == TYPE_vide) {
|
||||
return C.TRACK_TYPE_VIDEO;
|
||||
} else if (trackType == TYPE_text || trackType == TYPE_sbtl || trackType == TYPE_subt
|
||||
|| trackType == TYPE_clcp) {
|
||||
} else if (hdlr == TYPE_text || hdlr == TYPE_sbtl || hdlr == TYPE_subt || hdlr == TYPE_clcp) {
|
||||
return C.TRACK_TYPE_TEXT;
|
||||
} else if (trackType == TYPE_meta) {
|
||||
} else if (hdlr == TYPE_meta) {
|
||||
return C.TRACK_TYPE_METADATA;
|
||||
} else {
|
||||
return C.TRACK_TYPE_UNKNOWN;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
||||
|
|
@ -25,10 +28,9 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
|
|||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Parses metadata items stored in ilst atoms.
|
||||
*/
|
||||
/** Utilities for handling metadata in MP4. */
|
||||
/* package */ final class MetadataUtil {
|
||||
|
||||
private static final String TAG = "MetadataUtil";
|
||||
|
|
@ -103,24 +105,73 @@ import com.google.android.exoplayer2.util.Util;
|
|||
|
||||
private static final String LANGUAGE_UNDEFINED = "und";
|
||||
|
||||
private static final int TYPE_TOP_BYTE_COPYRIGHT = 0xA9;
|
||||
private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \uFFFD.
|
||||
|
||||
private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps";
|
||||
private static final int MDTA_TYPE_INDICATOR_FLOAT = 23;
|
||||
|
||||
private MetadataUtil() {}
|
||||
|
||||
/**
|
||||
* Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting
|
||||
* from the current position of the {@link ParsableByteArray}, and the position is advanced by the
|
||||
* size of the element. The position is advanced even if the element's type is unrecognized.
|
||||
* Returns a {@link Format} that is the same as the input format but includes information from the
|
||||
* specified sources of metadata.
|
||||
*/
|
||||
public static Format getFormatWithMetadata(
|
||||
int trackType,
|
||||
Format format,
|
||||
@Nullable Metadata udtaMetadata,
|
||||
@Nullable Metadata mdtaMetadata,
|
||||
GaplessInfoHolder gaplessInfoHolder) {
|
||||
if (trackType == C.TRACK_TYPE_AUDIO) {
|
||||
if (gaplessInfoHolder.hasGaplessInfo()) {
|
||||
format =
|
||||
format.copyWithGaplessInfo(
|
||||
gaplessInfoHolder.encoderDelay, gaplessInfoHolder.encoderPadding);
|
||||
}
|
||||
// We assume all udta metadata is associated with the audio track.
|
||||
if (udtaMetadata != null) {
|
||||
format = format.copyWithMetadata(udtaMetadata);
|
||||
}
|
||||
} else if (trackType == C.TRACK_TYPE_VIDEO && mdtaMetadata != null) {
|
||||
// Populate only metadata keys that are known to be specific to video.
|
||||
for (int i = 0; i < mdtaMetadata.length(); i++) {
|
||||
Metadata.Entry entry = mdtaMetadata.get(i);
|
||||
if (entry instanceof MdtaMetadataEntry) {
|
||||
MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry;
|
||||
if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key)
|
||||
&& mdtaMetadataEntry.typeIndicator == MDTA_TYPE_INDICATOR_FLOAT) {
|
||||
try {
|
||||
float fps = ByteBuffer.wrap(mdtaMetadataEntry.value).asFloatBuffer().get();
|
||||
format = format.copyWithFrameRate(fps);
|
||||
format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry));
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Ignoring invalid framerate");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a single userdata ilst element from a {@link ParsableByteArray}. The element is read
|
||||
* starting from the current position of the {@link ParsableByteArray}, and the position is
|
||||
* advanced by the size of the element. The position is advanced even if the element's type is
|
||||
* unrecognized.
|
||||
*
|
||||
* @param ilst Holds the data to be parsed.
|
||||
* @return The parsed element, or null if the element's type was not recognized.
|
||||
*/
|
||||
public static @Nullable Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
|
||||
@Nullable
|
||||
public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
|
||||
int position = ilst.getPosition();
|
||||
int endPosition = position + ilst.readInt();
|
||||
int type = ilst.readInt();
|
||||
int typeTopByte = (type >> 24) & 0xFF;
|
||||
try {
|
||||
if (typeTopByte == '\u00A9' /* Copyright char */
|
||||
|| typeTopByte == '\uFFFD' /* Replacement char */) {
|
||||
if (typeTopByte == TYPE_TOP_BYTE_COPYRIGHT || typeTopByte == TYPE_TOP_BYTE_REPLACEMENT) {
|
||||
int shortType = type & 0x00FFFFFF;
|
||||
if (shortType == SHORT_TYPE_COMMENT) {
|
||||
return parseCommentAttribute(type, ilst);
|
||||
|
|
@ -185,7 +236,36 @@ 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 atomSize = data.readInt();
|
||||
int atomType = data.readInt();
|
||||
|
|
@ -198,7 +278,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||
return null;
|
||||
}
|
||||
|
||||
private static @Nullable CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {
|
||||
@Nullable
|
||||
private static CommentFrame parseCommentAttribute(int type, ParsableByteArray data) {
|
||||
int atomSize = data.readInt();
|
||||
int atomType = data.readInt();
|
||||
if (atomType == Atom.TYPE_data) {
|
||||
|
|
@ -210,7 +291,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||
return null;
|
||||
}
|
||||
|
||||
private static @Nullable Id3Frame parseUint8Attribute(
|
||||
@Nullable
|
||||
private static Id3Frame parseUint8Attribute(
|
||||
int type,
|
||||
String id,
|
||||
ParsableByteArray data,
|
||||
|
|
@ -229,7 +311,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||
return null;
|
||||
}
|
||||
|
||||
private static @Nullable TextInformationFrame parseIndexAndCountAttribute(
|
||||
@Nullable
|
||||
private static TextInformationFrame parseIndexAndCountAttribute(
|
||||
int type, String attributeName, ParsableByteArray data) {
|
||||
int atomSize = data.readInt();
|
||||
int atomType = data.readInt();
|
||||
|
|
@ -249,8 +332,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||
return null;
|
||||
}
|
||||
|
||||
private static @Nullable TextInformationFrame parseStandardGenreAttribute(
|
||||
ParsableByteArray data) {
|
||||
@Nullable
|
||||
private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) {
|
||||
int genreCode = parseUint8AttributeValue(data);
|
||||
String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
|
||||
? STANDARD_GENRES[genreCode - 1] : null;
|
||||
|
|
@ -261,7 +344,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||
return null;
|
||||
}
|
||||
|
||||
private static @Nullable ApicFrame parseCoverArt(ParsableByteArray data) {
|
||||
@Nullable
|
||||
private static ApicFrame parseCoverArt(ParsableByteArray data) {
|
||||
int atomSize = data.readInt();
|
||||
int atomType = data.readInt();
|
||||
if (atomType == Atom.TYPE_data) {
|
||||
|
|
@ -285,8 +369,8 @@ import com.google.android.exoplayer2.util.Util;
|
|||
return null;
|
||||
}
|
||||
|
||||
private static @Nullable Id3Frame parseInternalAttribute(
|
||||
ParsableByteArray data, int endPosition) {
|
||||
@Nullable
|
||||
private static Id3Frame parseInternalAttribute(ParsableByteArray data, int endPosition) {
|
||||
String domain = null;
|
||||
String name = null;
|
||||
int dataAtomPosition = -1;
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
private static final int STATE_READING_ATOM_PAYLOAD = 1;
|
||||
private static final int STATE_READING_SAMPLE = 2;
|
||||
|
||||
// Brand stored in the ftyp atom for QuickTime media.
|
||||
/** Brand stored in the ftyp atom for QuickTime media. */
|
||||
private static final int BRAND_QUICKTIME = Util.getIntegerCodeForString("qt ");
|
||||
|
||||
/**
|
||||
|
|
@ -377,15 +377,21 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
long durationUs = C.TIME_UNSET;
|
||||
List<Mp4Track> tracks = new ArrayList<>();
|
||||
|
||||
Metadata metadata = null;
|
||||
// Process metadata.
|
||||
Metadata udtaMetadata = null;
|
||||
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
|
||||
Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
|
||||
if (udta != null) {
|
||||
metadata = AtomParsers.parseUdta(udta, isQuickTime);
|
||||
if (metadata != null) {
|
||||
gaplessInfoHolder.setFromMetadata(metadata);
|
||||
udtaMetadata = AtomParsers.parseUdta(udta, isQuickTime);
|
||||
if (udtaMetadata != null) {
|
||||
gaplessInfoHolder.setFromMetadata(udtaMetadata);
|
||||
}
|
||||
}
|
||||
Metadata mdtaMetadata = null;
|
||||
Atom.ContainerAtom meta = moov.getContainerAtomOfType(Atom.TYPE_meta);
|
||||
if (meta != null) {
|
||||
mdtaMetadata = AtomParsers.parseMdtaFromMeta(meta);
|
||||
}
|
||||
|
||||
boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0;
|
||||
ArrayList<TrackSampleTable> trackSampleTables =
|
||||
|
|
@ -401,15 +407,9 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
// Allow ten source samples per output sample, like the platform extractor.
|
||||
int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
|
||||
Format format = track.format.copyWithMaxInputSize(maxInputSize);
|
||||
if (track.type == C.TRACK_TYPE_AUDIO) {
|
||||
if (gaplessInfoHolder.hasGaplessInfo()) {
|
||||
format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
|
||||
gaplessInfoHolder.encoderPadding);
|
||||
}
|
||||
if (metadata != null) {
|
||||
format = format.copyWithMetadata(metadata);
|
||||
}
|
||||
}
|
||||
format =
|
||||
MetadataUtil.getFormatWithMetadata(
|
||||
track.type, format, udtaMetadata, mdtaMetadata, gaplessInfoHolder);
|
||||
mp4Track.trackOutput.format(format);
|
||||
|
||||
durationUs =
|
||||
|
|
@ -716,24 +716,37 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the extractor should decode a leaf atom with type {@code atom}.
|
||||
*/
|
||||
/** Returns whether the extractor should decode a leaf atom with type {@code atom}. */
|
||||
private static boolean shouldParseLeafAtom(int atom) {
|
||||
return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr
|
||||
|| atom == Atom.TYPE_stsd || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss
|
||||
|| atom == Atom.TYPE_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc
|
||||
|| atom == Atom.TYPE_stsz || atom == Atom.TYPE_stz2 || atom == Atom.TYPE_stco
|
||||
|| atom == Atom.TYPE_co64 || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp
|
||||
|| atom == Atom.TYPE_udta;
|
||||
return atom == Atom.TYPE_mdhd
|
||||
|| atom == Atom.TYPE_mvhd
|
||||
|| atom == Atom.TYPE_hdlr
|
||||
|| atom == Atom.TYPE_stsd
|
||||
|| atom == Atom.TYPE_stts
|
||||
|| atom == Atom.TYPE_stss
|
||||
|| atom == Atom.TYPE_ctts
|
||||
|| atom == Atom.TYPE_elst
|
||||
|| atom == Atom.TYPE_stsc
|
||||
|| atom == Atom.TYPE_stsz
|
||||
|| atom == Atom.TYPE_stz2
|
||||
|| atom == Atom.TYPE_stco
|
||||
|| atom == Atom.TYPE_co64
|
||||
|| atom == Atom.TYPE_tkhd
|
||||
|| atom == Atom.TYPE_ftyp
|
||||
|| atom == Atom.TYPE_udta
|
||||
|| atom == Atom.TYPE_keys
|
||||
|| atom == Atom.TYPE_ilst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the extractor should decode a container atom with type {@code atom}.
|
||||
*/
|
||||
/** Returns whether the extractor should decode a container atom with type {@code atom}. */
|
||||
private static boolean shouldParseContainerAtom(int atom) {
|
||||
return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia
|
||||
|| atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_edts;
|
||||
return atom == Atom.TYPE_moov
|
||||
|| atom == Atom.TYPE_trak
|
||||
|| atom == Atom.TYPE_mdia
|
||||
|| atom == Atom.TYPE_minf
|
||||
|| atom == Atom.TYPE_stbl
|
||||
|| atom == Atom.TYPE_edts
|
||||
|| atom == Atom.TYPE_meta;
|
||||
}
|
||||
|
||||
private static final class Mp4Track {
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ import java.io.IOException;
|
|||
*/
|
||||
/* package */ final class Sniffer {
|
||||
|
||||
/**
|
||||
* The maximum number of bytes to peek when sniffing.
|
||||
*/
|
||||
/** The maximum number of bytes to peek when sniffing. */
|
||||
private static final int SEARCH_LENGTH = 4 * 1024;
|
||||
|
||||
private static final int[] COMPATIBLE_BRANDS = new int[] {
|
||||
|
|
@ -109,15 +107,19 @@ import java.io.IOException;
|
|||
headerSize = Atom.LONG_HEADER_SIZE;
|
||||
input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
|
||||
buffer.setLimit(Atom.LONG_HEADER_SIZE);
|
||||
atomSize = buffer.readUnsignedLongToLong();
|
||||
atomSize = buffer.readLong();
|
||||
} else if (atomSize == Atom.EXTENDS_TO_END_SIZE) {
|
||||
// The atom extends to the end of the file.
|
||||
long endPosition = input.getLength();
|
||||
if (endPosition != C.LENGTH_UNSET) {
|
||||
atomSize = endPosition - input.getPosition() + headerSize;
|
||||
long fileEndPosition = input.getLength();
|
||||
if (fileEndPosition != C.LENGTH_UNSET) {
|
||||
atomSize = fileEndPosition - input.getPeekPosition() + headerSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (inputLength != C.LENGTH_UNSET && bytesSearched + atomSize > inputLength) {
|
||||
// The file is invalid because the atom extends past the end of the file.
|
||||
return false;
|
||||
}
|
||||
if (atomSize < headerSize) {
|
||||
// The file is invalid because the atom size is too small for its header.
|
||||
return false;
|
||||
|
|
@ -125,6 +127,13 @@ import java.io.IOException;
|
|||
bytesSearched += headerSize;
|
||||
|
||||
if (atomType == Atom.TYPE_moov) {
|
||||
// We have seen the moov atom. We increase the search size to make sure we don't miss an
|
||||
// mvex atom because the moov's size exceeds the search length.
|
||||
bytesToSearch += (int) atomSize;
|
||||
if (inputLength != C.LENGTH_UNSET && bytesToSearch > inputLength) {
|
||||
// Make sure we don't exceed the file size.
|
||||
bytesToSearch = (int) inputLength;
|
||||
}
|
||||
// Check for an mvex atom inside the moov atom to identify whether the file is fragmented.
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ import com.google.android.exoplayer2.util.Util;
|
|||
this.flags = flags;
|
||||
this.durationUs = durationUs;
|
||||
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-G386")
|
||||
|| Util.MODEL.startsWith("SM-T231")
|
||||
|| Util.MODEL.startsWith("SM-T530"))) {
|
||||
|| Util.MODEL.startsWith("SM-T530")
|
||||
|| Util.MODEL.startsWith("SCH-I535")
|
||||
|| Util.MODEL.startsWith("SPH-L710"))) {
|
||||
return false;
|
||||
}
|
||||
if ("OMX.brcm.audio.mp3.decoder".equals(name)
|
||||
|
|
|
|||
|
|
@ -240,10 +240,10 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
ClippingMediaPeriod mediaPeriod =
|
||||
new ClippingMediaPeriod(
|
||||
mediaSource.createPeriod(id, allocator),
|
||||
mediaSource.createPeriod(id, allocator, startPositionUs),
|
||||
enableInitialDiscontinuity,
|
||||
periodStartUs,
|
||||
periodEndUs);
|
||||
|
|
|
|||
|
|
@ -16,13 +16,12 @@
|
|||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.PlayerMessage;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
|
||||
|
|
@ -45,8 +44,7 @@ import java.util.Map;
|
|||
* during playback. It is valid for the same {@link MediaSource} instance to be present more than
|
||||
* once in the concatenation. Access to this class is thread-safe.
|
||||
*/
|
||||
public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHolder>
|
||||
implements PlayerMessage.Target {
|
||||
public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHolder> {
|
||||
|
||||
private static final int MSG_ADD = 0;
|
||||
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.Period period;
|
||||
|
||||
private @Nullable ExoPlayer player;
|
||||
private @Nullable Handler playerApplicationHandler;
|
||||
@Nullable private Handler playbackThreadHandler;
|
||||
@Nullable private Handler applicationThreadHandler;
|
||||
private boolean listenerNotificationScheduled;
|
||||
private ShuffleOrder shuffleOrder;
|
||||
private int windowCount;
|
||||
|
|
@ -239,12 +237,10 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
mediaSourceHolders.add(new MediaSourceHolder(mediaSource));
|
||||
}
|
||||
mediaSourcesPublic.addAll(index, mediaSourceHolders);
|
||||
if (player != null && !mediaSources.isEmpty()) {
|
||||
player
|
||||
.createMessage(this)
|
||||
.setType(MSG_ADD)
|
||||
.setPayload(new MessageData<>(index, mediaSourceHolders, actionOnCompletion))
|
||||
.send();
|
||||
if (playbackThreadHandler != null && !mediaSources.isEmpty()) {
|
||||
playbackThreadHandler
|
||||
.obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, actionOnCompletion))
|
||||
.sendToTarget();
|
||||
} else if (actionOnCompletion != null) {
|
||||
actionOnCompletion.run();
|
||||
}
|
||||
|
|
@ -328,12 +324,10 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
}
|
||||
return;
|
||||
}
|
||||
if (player != null) {
|
||||
player
|
||||
.createMessage(this)
|
||||
.setType(MSG_REMOVE)
|
||||
.setPayload(new MessageData<>(fromIndex, toIndex, actionOnCompletion))
|
||||
.send();
|
||||
if (playbackThreadHandler != null) {
|
||||
playbackThreadHandler
|
||||
.obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, actionOnCompletion))
|
||||
.sendToTarget();
|
||||
} else if (actionOnCompletion != null) {
|
||||
actionOnCompletion.run();
|
||||
}
|
||||
|
|
@ -371,12 +365,10 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
return;
|
||||
}
|
||||
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
|
||||
if (player != null) {
|
||||
player
|
||||
.createMessage(this)
|
||||
.setType(MSG_MOVE)
|
||||
.setPayload(new MessageData<>(currentIndex, newIndex, actionOnCompletion))
|
||||
.send();
|
||||
if (playbackThreadHandler != null) {
|
||||
playbackThreadHandler
|
||||
.obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, actionOnCompletion))
|
||||
.sendToTarget();
|
||||
} else if (actionOnCompletion != null) {
|
||||
actionOnCompletion.run();
|
||||
}
|
||||
|
|
@ -430,8 +422,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
*/
|
||||
public final synchronized void setShuffleOrder(
|
||||
ShuffleOrder shuffleOrder, @Nullable Runnable actionOnCompletion) {
|
||||
ExoPlayer player = this.player;
|
||||
if (player != null) {
|
||||
Handler playbackThreadHandler = this.playbackThreadHandler;
|
||||
if (playbackThreadHandler != null) {
|
||||
int size = getSize();
|
||||
if (shuffleOrder.getLength() != size) {
|
||||
shuffleOrder =
|
||||
|
|
@ -439,11 +431,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
.cloneAndClear()
|
||||
.cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
|
||||
}
|
||||
player
|
||||
.createMessage(this)
|
||||
.setType(MSG_SET_SHUFFLE_ORDER)
|
||||
.setPayload(new MessageData<>(/* index= */ 0, shuffleOrder, actionOnCompletion))
|
||||
.send();
|
||||
playbackThreadHandler
|
||||
.obtainMessage(
|
||||
MSG_SET_SHUFFLE_ORDER,
|
||||
new MessageData<>(/* index= */ 0, shuffleOrder, actionOnCompletion))
|
||||
.sendToTarget();
|
||||
} else {
|
||||
this.shuffleOrder =
|
||||
shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
|
||||
|
|
@ -465,8 +457,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
boolean isTopLevelSource,
|
||||
@Nullable TransferListener mediaTransferListener) {
|
||||
super.prepareSourceInternal(player, isTopLevelSource, mediaTransferListener);
|
||||
this.player = player;
|
||||
playerApplicationHandler = new Handler(player.getApplicationLooper());
|
||||
playbackThreadHandler = new Handler(/* callback= */ this::handleMessage);
|
||||
applicationThreadHandler = new Handler(player.getApplicationLooper());
|
||||
if (mediaSourcesPublic.isEmpty()) {
|
||||
notifyListener();
|
||||
} else {
|
||||
|
|
@ -484,7 +476,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
}
|
||||
|
||||
@Override
|
||||
public final MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public final MediaPeriod createPeriod(
|
||||
MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
|
||||
MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid);
|
||||
if (holder == null) {
|
||||
|
|
@ -492,7 +485,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
holder = new MediaSourceHolder(new DummyMediaSource());
|
||||
holder.hasStartedPreparing = true;
|
||||
}
|
||||
DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, id, allocator);
|
||||
DeferredMediaPeriod mediaPeriod =
|
||||
new DeferredMediaPeriod(holder.mediaSource, id, allocator, startPositionUs);
|
||||
mediaSourceByMediaPeriod.put(mediaPeriod, holder);
|
||||
holder.activeMediaPeriods.add(mediaPeriod);
|
||||
if (!holder.hasStartedPreparing) {
|
||||
|
|
@ -519,8 +513,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
super.releaseSourceInternal();
|
||||
mediaSourceHolders.clear();
|
||||
mediaSourceByUid.clear();
|
||||
player = null;
|
||||
playerApplicationHandler = null;
|
||||
playbackThreadHandler = null;
|
||||
applicationThreadHandler = null;
|
||||
shuffleOrder = shuffleOrder.cloneAndClear();
|
||||
windowCount = 0;
|
||||
periodCount = 0;
|
||||
|
|
@ -556,24 +550,22 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
return windowIndex + mediaSourceHolder.firstWindowIndexInChild;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public final void handleMessage(int messageType, @Nullable Object message)
|
||||
throws ExoPlaybackException {
|
||||
if (player == null) {
|
||||
private boolean handleMessage(Message msg) {
|
||||
if (playbackThreadHandler == null) {
|
||||
// Stale event.
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
switch (messageType) {
|
||||
switch (msg.what) {
|
||||
case MSG_ADD:
|
||||
MessageData<Collection<MediaSourceHolder>> addMessage =
|
||||
(MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(message);
|
||||
(MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj);
|
||||
shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size());
|
||||
addMediaSourcesInternal(addMessage.index, addMessage.customData);
|
||||
scheduleListenerNotification(addMessage.actionOnCompletion);
|
||||
break;
|
||||
case MSG_REMOVE:
|
||||
MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(message);
|
||||
MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
|
||||
int fromIndex = removeMessage.index;
|
||||
int toIndex = removeMessage.customData;
|
||||
if (fromIndex == 0 && toIndex == shuffleOrder.getLength()) {
|
||||
|
|
@ -587,7 +579,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
scheduleListenerNotification(removeMessage.actionOnCompletion);
|
||||
break;
|
||||
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.cloneAndInsert(moveMessage.customData, 1);
|
||||
moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
|
||||
|
|
@ -595,7 +587,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
break;
|
||||
case MSG_SET_SHUFFLE_ORDER:
|
||||
MessageData<ShuffleOrder> shuffleOrderMessage =
|
||||
(MessageData<ShuffleOrder>) Util.castNonNull(message);
|
||||
(MessageData<ShuffleOrder>) Util.castNonNull(msg.obj);
|
||||
shuffleOrder = shuffleOrderMessage.customData;
|
||||
scheduleListenerNotification(shuffleOrderMessage.actionOnCompletion);
|
||||
break;
|
||||
|
|
@ -603,8 +595,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
notifyListener();
|
||||
break;
|
||||
case MSG_ON_COMPLETION:
|
||||
List<Runnable> actionsOnCompletion = (List<Runnable>) Util.castNonNull(message);
|
||||
Handler handler = Assertions.checkNotNull(playerApplicationHandler);
|
||||
List<Runnable> actionsOnCompletion = (List<Runnable>) Util.castNonNull(msg.obj);
|
||||
Handler handler = Assertions.checkNotNull(applicationThreadHandler);
|
||||
for (int i = 0; i < actionsOnCompletion.size(); i++) {
|
||||
handler.post(actionsOnCompletion.get(i));
|
||||
}
|
||||
|
|
@ -612,11 +604,14 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void scheduleListenerNotification(@Nullable Runnable actionOnCompletion) {
|
||||
if (!listenerNotificationScheduled) {
|
||||
Assertions.checkNotNull(player).createMessage(this).setType(MSG_NOTIFY_LISTENER).send();
|
||||
Assertions.checkNotNull(playbackThreadHandler)
|
||||
.obtainMessage(MSG_NOTIFY_LISTENER)
|
||||
.sendToTarget();
|
||||
listenerNotificationScheduled = true;
|
||||
}
|
||||
if (actionOnCompletion != null) {
|
||||
|
|
@ -636,11 +631,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),
|
||||
/* manifest= */ null);
|
||||
if (!actionsOnCompletion.isEmpty()) {
|
||||
Assertions.checkNotNull(player)
|
||||
.createMessage(this)
|
||||
.setType(MSG_ON_COMPLETION)
|
||||
.setPayload(actionsOnCompletion)
|
||||
.send();
|
||||
Assertions.checkNotNull(playbackThreadHandler)
|
||||
.obtainMessage(MSG_ON_COMPLETION, actionsOnCompletion)
|
||||
.sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -718,6 +711,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
// unlikely to be a problem as a non-zero default position usually only occurs for live
|
||||
// playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions
|
||||
// anyway.
|
||||
timeline.getWindow(/* windowIndex= */ 0, window);
|
||||
long windowStartPositionUs = window.getDefaultPositionUs();
|
||||
if (deferredMediaPeriod != null) {
|
||||
long periodPreparePositionUs = deferredMediaPeriod.getPreparePositionUs();
|
||||
|
|
@ -1101,7 +1095,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
|
|||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import java.io.IOException;
|
|||
|
||||
/**
|
||||
* Media period that wraps a media source and defers calling its {@link
|
||||
* MediaSource#createPeriod(MediaPeriodId, Allocator)} method until {@link
|
||||
* MediaSource#createPeriod(MediaPeriodId, Allocator, long)} method until {@link
|
||||
* #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media
|
||||
* period immediately but the media source that should create it is not yet prepared.
|
||||
*/
|
||||
|
|
@ -60,11 +60,14 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
|||
* @param mediaSource The media source to wrap.
|
||||
* @param id The identifier used to create the deferred media period.
|
||||
* @param allocator The allocator used to create the media period.
|
||||
* @param preparePositionUs The expected start position, in microseconds.
|
||||
*/
|
||||
public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) {
|
||||
public DeferredMediaPeriod(
|
||||
MediaSource mediaSource, MediaPeriodId id, Allocator allocator, long preparePositionUs) {
|
||||
this.id = id;
|
||||
this.allocator = allocator;
|
||||
this.mediaSource = mediaSource;
|
||||
this.preparePositionUs = preparePositionUs;
|
||||
preparePositionOverrideUs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
|
|
@ -86,28 +89,25 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
|||
|
||||
/**
|
||||
* Overrides the default prepare position at which to prepare the media period. This value is only
|
||||
* used if the call to {@link MediaPeriod#prepare(Callback, long)} is being deferred.
|
||||
* used if called before {@link #createPeriod(MediaPeriodId)}.
|
||||
*
|
||||
* @param defaultPreparePositionUs The default prepare position to use, in microseconds.
|
||||
* @param preparePositionUs The default prepare position to use, in microseconds.
|
||||
*/
|
||||
public void overridePreparePositionUs(long defaultPreparePositionUs) {
|
||||
preparePositionOverrideUs = defaultPreparePositionUs;
|
||||
public void overridePreparePositionUs(long preparePositionUs) {
|
||||
preparePositionOverrideUs = preparePositionUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then
|
||||
* prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()}
|
||||
* to release the period.
|
||||
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source
|
||||
* then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link
|
||||
* #releasePeriod()} to release the period.
|
||||
*
|
||||
* @param id The identifier that should be used to create the media period from the media source.
|
||||
*/
|
||||
public void createPeriod(MediaPeriodId id) {
|
||||
mediaPeriod = mediaSource.createPeriod(id, allocator);
|
||||
long preparePositionUs = getPreparePositionWithOverride(this.preparePositionUs);
|
||||
mediaPeriod = mediaSource.createPeriod(id, allocator, preparePositionUs);
|
||||
if (callback != null) {
|
||||
long preparePositionUs =
|
||||
preparePositionOverrideUs != C.TIME_UNSET
|
||||
? preparePositionOverrideUs
|
||||
: this.preparePositionUs;
|
||||
mediaPeriod.prepare(this, preparePositionUs);
|
||||
}
|
||||
}
|
||||
|
|
@ -124,9 +124,8 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
|||
@Override
|
||||
public void prepare(Callback callback, long preparePositionUs) {
|
||||
this.callback = callback;
|
||||
this.preparePositionUs = preparePositionUs;
|
||||
if (mediaPeriod != null) {
|
||||
mediaPeriod.prepare(this, preparePositionUs);
|
||||
mediaPeriod.prepare(this, getPreparePositionWithOverride(this.preparePositionUs));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -217,4 +216,9 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
|||
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()) {
|
||||
return pendingResetPositionUs;
|
||||
}
|
||||
long largestQueuedTimestampUs;
|
||||
long largestQueuedTimestampUs = C.TIME_UNSET;
|
||||
if (haveAudioVideoTracks) {
|
||||
// Ignore non-AV tracks, which may be sparse or poorly interleaved.
|
||||
largestQueuedTimestampUs = Long.MAX_VALUE;
|
||||
int trackCount = sampleQueues.length;
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
if (trackIsAudioVideoFlags[i]) {
|
||||
if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) {
|
||||
largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,
|
||||
sampleQueues[i].getLargestQueuedTimestampUs());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
if (largestQueuedTimestampUs == C.TIME_UNSET) {
|
||||
largestQueuedTimestampUs = getLargestQueuedTimestampUs();
|
||||
}
|
||||
return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@ public final class ExtractorMediaSource extends BaseMediaSource
|
|||
boolean isTopLevelSource,
|
||||
@Nullable TransferListener mediaTransferListener) {
|
||||
transferListener = mediaTransferListener;
|
||||
notifySourceInfoRefreshed(timelineDurationUs, /* isSeekable= */ false);
|
||||
notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -379,7 +379,7 @@ public final class ExtractorMediaSource extends BaseMediaSource
|
|||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
DataSource dataSource = dataSourceFactory.createDataSource();
|
||||
if (transferListener != null) {
|
||||
dataSource.addTransferListener(transferListener);
|
||||
|
|
|
|||
|
|
@ -80,14 +80,15 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
if (loopCount == Integer.MAX_VALUE) {
|
||||
return childSource.createPeriod(id, allocator);
|
||||
return childSource.createPeriod(id, allocator, startPositionUs);
|
||||
}
|
||||
Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid);
|
||||
MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid);
|
||||
childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id);
|
||||
MediaPeriod mediaPeriod = childSource.createPeriod(childMediaPeriodId, allocator);
|
||||
MediaPeriod mediaPeriod =
|
||||
childSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
|
||||
mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId);
|
||||
return mediaPeriod;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ import java.io.IOException;
|
|||
* on the {@link SourceInfoRefreshListener}s passed to {@link #prepareSource(ExoPlayer,
|
||||
* boolean, SourceInfoRefreshListener, TransferListener)}.
|
||||
* <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
|
||||
* the player to load and read the media.
|
||||
* obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a
|
||||
* way for the player to load and read the media.
|
||||
* </ul>
|
||||
*
|
||||
* 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 allocator An {@link Allocator} from which to obtain media buffer allocations.
|
||||
* @param startPositionUs The expected start position, in microseconds.
|
||||
* @return A new {@link MediaPeriod}.
|
||||
*/
|
||||
MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator);
|
||||
MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs);
|
||||
|
||||
/**
|
||||
* Releases the period.
|
||||
|
|
|
|||
|
|
@ -124,13 +124,13 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
|
||||
int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid);
|
||||
for (int i = 0; i < periods.length; i++) {
|
||||
MediaPeriodId childMediaPeriodId =
|
||||
id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex));
|
||||
periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator);
|
||||
periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator, startPositionUs);
|
||||
}
|
||||
return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||
|
||||
private long largestDiscardedTimestampUs;
|
||||
private long largestQueuedTimestampUs;
|
||||
private boolean isLastSampleQueued;
|
||||
private boolean upstreamKeyframeRequired;
|
||||
private boolean upstreamFormatRequired;
|
||||
private Format upstreamFormat;
|
||||
|
|
@ -93,6 +94,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||
upstreamKeyframeRequired = true;
|
||||
largestDiscardedTimestampUs = Long.MIN_VALUE;
|
||||
largestQueuedTimestampUs = Long.MIN_VALUE;
|
||||
isLastSampleQueued = false;
|
||||
if (resetUpstreamFormat) {
|
||||
upstreamFormat = null;
|
||||
upstreamFormatRequired = true;
|
||||
|
|
@ -118,6 +120,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||
Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition));
|
||||
length -= discardCount;
|
||||
largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length));
|
||||
isLastSampleQueued = discardCount == 0 && isLastSampleQueued;
|
||||
if (length == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
|
|
@ -186,6 +189,19 @@ import com.google.android.exoplayer2.util.Util;
|
|||
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. */
|
||||
public synchronized long getFirstTimestampUs() {
|
||||
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,
|
||||
SampleExtrasHolder extrasHolder) {
|
||||
if (!hasNextSample()) {
|
||||
if (loadingFinished) {
|
||||
if (loadingFinished || isLastSampleQueued) {
|
||||
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
return C.RESULT_BUFFER_READ;
|
||||
} else if (upstreamFormat != null
|
||||
|
|
@ -388,7 +404,9 @@ import com.google.android.exoplayer2.util.Util;
|
|||
upstreamKeyframeRequired = false;
|
||||
}
|
||||
Assertions.checkState(!upstreamFormatRequired);
|
||||
commitSampleTimestamp(timeUs);
|
||||
|
||||
isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0;
|
||||
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
|
||||
|
||||
int relativeEndIndex = getRelativeIndex(length);
|
||||
timesUs[relativeEndIndex] = timeUs;
|
||||
|
|
@ -439,10 +457,6 @@ 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
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the last sample of the stream has knowingly been queued. A return value of
|
||||
* {@code false} means that the last sample had not been queued or that it's unknown whether the
|
||||
* last sample has been queued.
|
||||
*/
|
||||
public boolean isLastSampleQueued() {
|
||||
return metadataQueue.isLastSampleQueued();
|
||||
}
|
||||
|
||||
/** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
|
||||
public long getFirstTimestampUs() {
|
||||
return metadataQueue.getFirstTimestampUs();
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
|
|||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
return new SingleSampleMediaPeriod(
|
||||
dataSpec,
|
||||
dataSourceFactory,
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
if (adPlaybackState.adGroupCount > 0 && id.isAd()) {
|
||||
int adGroupIndex = id.adGroupIndex;
|
||||
int adIndexInAdGroup = id.adIndexInAdGroup;
|
||||
|
|
@ -360,7 +360,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||
prepareChildSource(id, adMediaSource);
|
||||
}
|
||||
MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup];
|
||||
DeferredMediaPeriod deferredMediaPeriod = new DeferredMediaPeriod(mediaSource, id, allocator);
|
||||
DeferredMediaPeriod deferredMediaPeriod =
|
||||
new DeferredMediaPeriod(mediaSource, id, allocator, startPositionUs);
|
||||
deferredMediaPeriod.setPrepareErrorListener(
|
||||
new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup));
|
||||
List<DeferredMediaPeriod> mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource);
|
||||
|
|
@ -376,7 +377,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||
}
|
||||
return deferredMediaPeriod;
|
||||
} else {
|
||||
DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(contentMediaSource, id, allocator);
|
||||
DeferredMediaPeriod mediaPeriod =
|
||||
new DeferredMediaPeriod(contentMediaSource, id, allocator, startPositionUs);
|
||||
mediaPeriod.createPeriod(id);
|
||||
return mediaPeriod;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,11 +64,11 @@ public interface DataSource {
|
|||
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}.
|
||||
* <p>
|
||||
* If {@code length} is zero then 0 is returned. Otherwise, if no data is available because the
|
||||
* end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned.
|
||||
*
|
||||
* <p>If {@code readLength} is zero then 0 is returned. Otherwise, if no data is available because
|
||||
* the end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned.
|
||||
* Otherwise, the call will block until at least one byte of data has been read and the number of
|
||||
* bytes read is returned.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ public final class CacheDataSink implements DataSink {
|
|||
private final Cache cache;
|
||||
private final long maxCacheFileSize;
|
||||
private final int bufferSize;
|
||||
private final boolean syncFileDescriptor;
|
||||
|
||||
private boolean syncFileDescriptor;
|
||||
private DataSpec dataSpec;
|
||||
private File file;
|
||||
private OutputStream outputStream;
|
||||
|
|
@ -64,18 +64,6 @@ public 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}.
|
||||
*
|
||||
|
|
@ -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
|
||||
* {@link DataSpec} whose size exceeds this value, then the data will be fragmented into
|
||||
* multiple cache files.
|
||||
* @param syncFileDescriptor Whether file descriptors are sync'd when closing output streams.
|
||||
*/
|
||||
public CacheDataSink(Cache cache, long maxCacheFileSize, boolean syncFileDescriptor) {
|
||||
this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE, syncFileDescriptor);
|
||||
public CacheDataSink(Cache cache, long maxCacheFileSize) {
|
||||
this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -98,23 +85,21 @@ public final class CacheDataSink implements DataSink {
|
|||
* value disables buffering.
|
||||
*/
|
||||
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.maxCacheFileSize = maxCacheFileSize;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import java.io.OutputStream;
|
|||
* has successfully completed.
|
||||
*
|
||||
* <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).
|
||||
*
|
||||
* <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 {
|
||||
int maxWidth = format.width;
|
||||
int maxHeight = format.height;
|
||||
if (codecNeedsMaxVideoSizeResetWorkaround(codecInfo.name)) {
|
||||
maxWidth = Math.max(maxWidth, 1920);
|
||||
maxHeight = Math.max(maxHeight, 1089);
|
||||
}
|
||||
int maxInputSize = getMaxInputSize(codecInfo, format);
|
||||
if (streamFormats.length == 1) {
|
||||
// The single entry in streamFormats must correspond to the format for which the codec is
|
||||
|
|
@ -1274,6 +1278,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
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:
|
||||
*
|
||||
|
|
@ -1322,7 +1338,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
// https://github.com/google/ExoPlayer/issues/4315,
|
||||
// https://github.com/google/ExoPlayer/issues/4419,
|
||||
// https://github.com/google/ExoPlayer/issues/4460,
|
||||
// https://github.com/google/ExoPlayer/issues/4468.
|
||||
// https://github.com/google/ExoPlayer/issues/4468,
|
||||
// https://github.com/google/ExoPlayer/issues/5312.
|
||||
switch (Util.DEVICE) {
|
||||
case "1601":
|
||||
case "1713":
|
||||
|
|
@ -1378,6 +1395,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
case "HWBLN-H":
|
||||
case "HWCAM-H":
|
||||
case "HWVNS-H":
|
||||
case "HWWAS-H":
|
||||
case "i9031":
|
||||
case "iball8735_9806":
|
||||
case "Infinix-X572":
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ track 0:
|
|||
data = length 530, hash C98BC6A8
|
||||
sample 29:
|
||||
time = 934266
|
||||
flags = 0
|
||||
flags = 536870912
|
||||
data = length 568, hash 4FE5C8EA
|
||||
track 1:
|
||||
format:
|
||||
|
|
@ -352,6 +352,6 @@ track 1:
|
|||
data = length 229, hash FFF98DF0
|
||||
sample 44:
|
||||
time = 1065678
|
||||
flags = 1
|
||||
flags = 536870913
|
||||
data = length 6, hash 31B22286
|
||||
tracksEnded = true
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ track 0:
|
|||
data = length 530, hash C98BC6A8
|
||||
sample 29:
|
||||
time = 934266
|
||||
flags = 0
|
||||
flags = 536870912
|
||||
data = length 568, hash 4FE5C8EA
|
||||
track 1:
|
||||
format:
|
||||
|
|
@ -304,6 +304,6 @@ track 1:
|
|||
data = length 229, hash FFF98DF0
|
||||
sample 32:
|
||||
time = 1065678
|
||||
flags = 1
|
||||
flags = 536870913
|
||||
data = length 6, hash 31B22286
|
||||
tracksEnded = true
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ track 0:
|
|||
data = length 530, hash C98BC6A8
|
||||
sample 29:
|
||||
time = 934266
|
||||
flags = 0
|
||||
flags = 536870912
|
||||
data = length 568, hash 4FE5C8EA
|
||||
track 1:
|
||||
format:
|
||||
|
|
@ -244,6 +244,6 @@ track 1:
|
|||
data = length 229, hash FFF98DF0
|
||||
sample 17:
|
||||
time = 1065678
|
||||
flags = 1
|
||||
flags = 536870913
|
||||
data = length 6, hash 31B22286
|
||||
tracksEnded = true
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ track 0:
|
|||
data = length 530, hash C98BC6A8
|
||||
sample 29:
|
||||
time = 934266
|
||||
flags = 0
|
||||
flags = 536870912
|
||||
data = length 568, hash 4FE5C8EA
|
||||
track 1:
|
||||
format:
|
||||
|
|
@ -184,6 +184,6 @@ track 1:
|
|||
data = length 229, hash FFF98DF0
|
||||
sample 2:
|
||||
time = 1065678
|
||||
flags = 1
|
||||
flags = 536870913
|
||||
data = length 6, hash 31B22286
|
||||
tracksEnded = true
|
||||
|
|
|
|||
|
|
@ -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.TransferListener;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
|
|
@ -452,13 +453,22 @@ import java.util.List;
|
|||
if (adaptationSetSwitchingProperty == null) {
|
||||
groupedAdaptationSetIndices[groupCount++] = new int[] {i};
|
||||
} else {
|
||||
String[] extraAdaptationSetIds = adaptationSetSwitchingProperty.value.split(",");
|
||||
String[] extraAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ",");
|
||||
int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length];
|
||||
adaptationSetIndices[0] = i;
|
||||
int outputIndex = 1;
|
||||
for (int j = 0; j < extraAdaptationSetIds.length; j++) {
|
||||
int extraIndex = idToIndexMap.get(Integer.parseInt(extraAdaptationSetIds[j]));
|
||||
adaptationSetUsedFlags[extraIndex] = true;
|
||||
adaptationSetIndices[1 + j] = extraIndex;
|
||||
int extraIndex =
|
||||
idToIndexMap.get(
|
||||
Integer.parseInt(extraAdaptationSetIds[j]), /* valueIfKeyNotFound= */ -1);
|
||||
if (extraIndex != -1) {
|
||||
adaptationSetUsedFlags[extraIndex] = true;
|
||||
adaptationSetIndices[outputIndex] = extraIndex;
|
||||
outputIndex++;
|
||||
}
|
||||
}
|
||||
if (outputIndex < adaptationSetIndices.length) {
|
||||
adaptationSetIndices = Arrays.copyOf(adaptationSetIndices, outputIndex);
|
||||
}
|
||||
groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -635,7 +635,8 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId periodId, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(
|
||||
MediaPeriodId periodId, Allocator allocator, long startPositionUs) {
|
||||
int periodIndex = (Integer) periodId.periodUid - firstPeriodId;
|
||||
EventDispatcher periodEventDispatcher =
|
||||
createEventDispatcher(periodId, manifest.getPeriod(periodIndex).startMs);
|
||||
|
|
|
|||
|
|
@ -457,10 +457,10 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
|||
}
|
||||
|
||||
private ArrayList<Representation> getRepresentations() {
|
||||
List<AdaptationSet> manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets;
|
||||
List<AdaptationSet> manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets;
|
||||
ArrayList<Representation> representations = new ArrayList<>();
|
||||
for (int adaptationSetIndex : adaptationSetIndices) {
|
||||
representations.addAll(manifestAdapationSets.get(adaptationSetIndex).representations);
|
||||
representations.addAll(manifestAdaptationSets.get(adaptationSetIndex).representations);
|
||||
}
|
||||
return representations;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -412,7 +412,7 @@ public final class HlsMediaSource extends BaseMediaSource
|
|||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
EventDispatcher eventDispatcher = createEventDispatcher(id);
|
||||
return new HlsMediaPeriod(
|
||||
extractorFactory,
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
/* initializationData= */ null,
|
||||
selectionFlags,
|
||||
language);
|
||||
if (uri == null) {
|
||||
if (isMediaTagMuxed(variants, uri)) {
|
||||
muxedAudioFormat = format;
|
||||
} else {
|
||||
audios.add(new HlsMasterPlaylist.HlsUrl(uri, format));
|
||||
|
|
@ -766,6 +766,20 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
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 final BufferedReader reader;
|
||||
|
|
|
|||
|
|
@ -134,6 +134,17 @@ public class HlsMasterPlaylistParserTest {
|
|||
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"{$codecs}\"\n"
|
||||
+ "http://example.com/{$tricky}\n";
|
||||
|
||||
private static final String PLAYLIST_WITH_MULTIPLE_MUXED_MEDIA_TAGS =
|
||||
"#EXTM3U\n"
|
||||
+ "#EXT-X-VERSION:3\n"
|
||||
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"a\",NAME=\"audio_0\",DEFAULT=YES,URI=\"0/0.m3u8\"\n"
|
||||
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"b\",NAME=\"audio_0\",DEFAULT=YES,URI=\"1/1.m3u8\"\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=140800,CODECS=\"mp4a.40.2\",AUDIO=\"a\"\n"
|
||||
+ "0/0.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=281600,CODECS=\"mp4a.40.2\",AUDIO=\"b\"\n"
|
||||
+ "1/1.m3u8\n";
|
||||
|
||||
@Test
|
||||
public void testParseMasterPlaylist() throws IOException {
|
||||
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
|
||||
|
|
@ -271,6 +282,14 @@ public class HlsMasterPlaylistParserTest {
|
|||
assertThat(variant.url).isEqualTo("http://example.com/This/{$nested}/reference/shouldnt/work");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleMuxedMediaTags() throws IOException {
|
||||
HlsMasterPlaylist playlistWithMultipleMuxedMediaTags =
|
||||
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_MULTIPLE_MUXED_MEDIA_TAGS);
|
||||
assertThat(playlistWithMultipleMuxedMediaTags.variants).hasSize(2);
|
||||
assertThat(playlistWithMultipleMuxedMediaTags.audios).isEmpty();
|
||||
}
|
||||
|
||||
private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString)
|
||||
throws IOException {
|
||||
Uri playlistUri = Uri.parse(uri);
|
||||
|
|
|
|||
|
|
@ -61,14 +61,13 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
|||
SsManifest manifest,
|
||||
int elementIndex,
|
||||
TrackSelection trackSelection,
|
||||
TrackEncryptionBox[] trackEncryptionBoxes,
|
||||
@Nullable TransferListener transferListener) {
|
||||
DataSource dataSource = dataSourceFactory.createDataSource();
|
||||
if (transferListener != null) {
|
||||
dataSource.addTransferListener(transferListener);
|
||||
}
|
||||
return new DefaultSsChunkSource(manifestLoaderErrorThrower, manifest, elementIndex,
|
||||
trackSelection, dataSource, trackEncryptionBoxes);
|
||||
return new DefaultSsChunkSource(
|
||||
manifestLoaderErrorThrower, manifest, elementIndex, trackSelection, dataSource);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -90,15 +89,13 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
|||
* @param streamElementIndex The index of the stream element in the manifest.
|
||||
* @param trackSelection The track selection.
|
||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||
* @param trackEncryptionBoxes Track encryption boxes for the stream.
|
||||
*/
|
||||
public DefaultSsChunkSource(
|
||||
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
SsManifest manifest,
|
||||
int streamElementIndex,
|
||||
TrackSelection trackSelection,
|
||||
DataSource dataSource,
|
||||
TrackEncryptionBox[] trackEncryptionBoxes) {
|
||||
DataSource dataSource) {
|
||||
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
||||
this.manifest = manifest;
|
||||
this.streamElementIndex = streamElementIndex;
|
||||
|
|
@ -110,6 +107,8 @@ public class DefaultSsChunkSource implements SsChunkSource {
|
|||
for (int i = 0; i < extractorWrappers.length; i++) {
|
||||
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i);
|
||||
Format format = streamElement.formats[manifestTrackIndex];
|
||||
TrackEncryptionBox[] trackEncryptionBoxes =
|
||||
format.drmInitData != null ? manifest.protectionElement.trackEncryptionBoxes : null;
|
||||
int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : 0;
|
||||
Track track = new Track(manifestTrackIndex, streamElement.type, streamElement.timescale,
|
||||
C.TIME_UNSET, manifest.durationUs, format, Track.TRANSFORMATION_NONE,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
package com.google.android.exoplayer2.source.smoothstreaming;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
|
|
@ -38,7 +37,6 @@ public interface SsChunkSource extends ChunkSource {
|
|||
* @param manifest The initial manifest.
|
||||
* @param streamElementIndex The index of the corresponding stream element in the manifest.
|
||||
* @param trackSelection The track selection.
|
||||
* @param trackEncryptionBoxes Track encryption boxes for the stream.
|
||||
* @param transferListener The transfer listener which should be informed of any data transfers.
|
||||
* May be null if no listener is available.
|
||||
* @return The created {@link SsChunkSource}.
|
||||
|
|
@ -48,7 +46,6 @@ public interface SsChunkSource extends ChunkSource {
|
|||
SsManifest manifest,
|
||||
int streamElementIndex,
|
||||
TrackSelection trackSelection,
|
||||
TrackEncryptionBox[] trackEncryptionBoxes,
|
||||
@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.chunk.ChunkSampleStream;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
|
|
@ -44,8 +43,6 @@ import java.util.ArrayList;
|
|||
/* package */ final class SsMediaPeriod implements MediaPeriod,
|
||||
SequenceableLoader.Callback<ChunkSampleStream<SsChunkSource>> {
|
||||
|
||||
private static final int INITIALIZATION_VECTOR_SIZE = 8;
|
||||
|
||||
private final SsChunkSource.Factory chunkSourceFactory;
|
||||
private final @Nullable TransferListener transferListener;
|
||||
private final LoaderErrorThrower manifestLoaderErrorThrower;
|
||||
|
|
@ -53,7 +50,6 @@ import java.util.ArrayList;
|
|||
private final EventDispatcher eventDispatcher;
|
||||
private final Allocator allocator;
|
||||
private final TrackGroupArray trackGroups;
|
||||
private final TrackEncryptionBox[] trackEncryptionBoxes;
|
||||
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||
|
||||
private @Nullable Callback callback;
|
||||
|
|
@ -71,6 +67,7 @@ import java.util.ArrayList;
|
|||
EventDispatcher eventDispatcher,
|
||||
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
Allocator allocator) {
|
||||
this.manifest = manifest;
|
||||
this.chunkSourceFactory = chunkSourceFactory;
|
||||
this.transferListener = transferListener;
|
||||
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
||||
|
|
@ -78,18 +75,7 @@ import java.util.ArrayList;
|
|||
this.eventDispatcher = eventDispatcher;
|
||||
this.allocator = allocator;
|
||||
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
||||
|
||||
trackGroups = buildTrackGroups(manifest);
|
||||
ProtectionElement protectionElement = manifest.protectionElement;
|
||||
if (protectionElement != null) {
|
||||
byte[] keyId = getProtectionElementKeyId(protectionElement.data);
|
||||
// We assume pattern encryption does not apply.
|
||||
trackEncryptionBoxes = new TrackEncryptionBox[] {
|
||||
new TrackEncryptionBox(true, null, INITIALIZATION_VECTOR_SIZE, keyId, 0, 0, null)};
|
||||
} else {
|
||||
trackEncryptionBoxes = null;
|
||||
}
|
||||
this.manifest = manifest;
|
||||
sampleStreams = newSampleStreamArray(0);
|
||||
compositeSequenceableLoader =
|
||||
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams);
|
||||
|
|
@ -229,7 +215,6 @@ import java.util.ArrayList;
|
|||
manifest,
|
||||
streamElementIndex,
|
||||
selection,
|
||||
trackEncryptionBoxes,
|
||||
transferListener);
|
||||
return new ChunkSampleStream<>(
|
||||
manifest.streamElements[streamElementIndex].type,
|
||||
|
|
@ -277,5 +262,4 @@ import java.util.ArrayList;
|
|||
data[firstPosition] = data[secondPosition];
|
||||
data[secondPosition] = temp;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -533,7 +533,7 @@ public final class SsMediaSource extends BaseMediaSource
|
|||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
EventDispatcher eventDispatcher = createEventDispatcher(id);
|
||||
SsMediaPeriod period =
|
||||
new SsMediaPeriod(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.manifest;
|
|||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;
|
||||
import com.google.android.exoplayer2.offline.FilterableManifest;
|
||||
import com.google.android.exoplayer2.offline.StreamKey;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
|
@ -41,10 +42,12 @@ public class SsManifest implements FilterableManifest<SsManifest> {
|
|||
|
||||
public final UUID uuid;
|
||||
public final byte[] data;
|
||||
public final TrackEncryptionBox[] trackEncryptionBoxes;
|
||||
|
||||
public ProtectionElement(UUID uuid, byte[] data) {
|
||||
public ProtectionElement(UUID uuid, byte[] data, TrackEncryptionBox[] trackEncryptionBoxes) {
|
||||
this.uuid = uuid;
|
||||
this.data = data;
|
||||
this.trackEncryptionBoxes = trackEncryptionBoxes;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ParserException;
|
|||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
|
||||
import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;
|
||||
import com.google.android.exoplayer2.upstream.ParsingLoadable;
|
||||
|
|
@ -397,9 +398,10 @@ public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> {
|
|||
|
||||
public static final String TAG = "Protection";
|
||||
public static final String TAG_PROTECTION_HEADER = "ProtectionHeader";
|
||||
|
||||
public static final String KEY_SYSTEM_ID = "SystemID";
|
||||
|
||||
private static final int INITIALIZATION_VECTOR_SIZE = 8;
|
||||
|
||||
private boolean inProtectionHeader;
|
||||
private UUID uuid;
|
||||
private byte[] initData;
|
||||
|
|
@ -439,7 +441,44 @@ public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> {
|
|||
|
||||
@Override
|
||||
public Object build() {
|
||||
return new ProtectionElement(uuid, PsshAtomUtil.buildPsshAtom(uuid, initData));
|
||||
return new ProtectionElement(
|
||||
uuid, PsshAtomUtil.buildPsshAtom(uuid, initData), buildTrackEncryptionBoxes(initData));
|
||||
}
|
||||
|
||||
private static TrackEncryptionBox[] buildTrackEncryptionBoxes(byte[] initData) {
|
||||
return new TrackEncryptionBox[] {
|
||||
new TrackEncryptionBox(
|
||||
/* isEncrypted= */ true,
|
||||
/* schemeType= */ null,
|
||||
INITIALIZATION_VECTOR_SIZE,
|
||||
getProtectionElementKeyId(initData),
|
||||
/* defaultEncryptedBlocks= */ 0,
|
||||
/* defaultClearBlocks= */ 0,
|
||||
/* defaultInitializationVector= */ null)
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] getProtectionElementKeyId(byte[] initData) {
|
||||
StringBuilder initDataStringBuilder = new StringBuilder();
|
||||
for (int i = 0; i < initData.length; i += 2) {
|
||||
initDataStringBuilder.append((char) initData[i]);
|
||||
}
|
||||
String initDataString = initDataStringBuilder.toString();
|
||||
String keyIdString =
|
||||
initDataString.substring(
|
||||
initDataString.indexOf("<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) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import com.google.android.exoplayer2.source.TrackGroup;
|
|||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.ParsingLoadable;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
|
@ -42,7 +43,7 @@ public final class SsDownloadHelper extends DownloadHelper {
|
|||
private @MonotonicNonNull SsManifest manifest;
|
||||
|
||||
public SsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) {
|
||||
this.uri = uri;
|
||||
this.uri = SsUtil.fixManifestUri(uri);;
|
||||
this.manifestDataSourceFactory = manifestDataSourceFactory;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Duration="2300000000" TimeScale="10000000">
|
||||
<Protection>
|
||||
<ProtectionHeader SystemID="9A04F079-9840-4286-AB92-E65BE0885F95">
|
||||
<!-- Base 64-Encoded data omitted for clarity -->
|
||||
fgMAAAEAAQB0AzwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQgBhAFUATQBPAEcAYwBzAGgAVQBDAEQAZAB3ADMANABZAGMAawBmAFoAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBnADcATgBhAFIARABJAEkATwA5ADAAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAcwA6AC8ALwBUAC0ATwBOAEwASQBOAEUALgBEAFUATQBNAFkALQBTAEUAUgBWAEUAUgAvAEEAcgB0AGUAbQBpAHMATABpAGMAZQBuAHMAZQBTAGUAcgB2AGUAcgAvAFAAbABhAHkAUgBlAGEAZAB5AE0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwAQwBJAEQAPgAxADcANQA4ADIANgA8AC8AQwBJAEQAPgA8AEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4ANwAuADEALgAxADUANgA1AC4ANAA8AC8ASQBJAFMAXwBEAFIATQBfAFYARQBSAFMASQBPAE4APgA8AC8AQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A
|
||||
</ProtectionHeader>
|
||||
</Protection>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Duration="2300000000" TimeScale="10000000">
|
||||
<Protection>
|
||||
<ProtectionHeader SystemID="{9A04F079-9840-4286-AB92-E65BE0885F95}">
|
||||
<!-- Base 64-Encoded data omitted for clarity -->
|
||||
fgMAAAEAAQB0AzwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQgBhAFUATQBPAEcAYwBzAGgAVQBDAEQAZAB3ADMANABZAGMAawBmAFoAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBnADcATgBhAFIARABJAEkATwA5ADAAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAcwA6AC8ALwBUAC0ATwBOAEwASQBOAEUALgBEAFUATQBNAFkALQBTAEUAUgBWAEUAUgAvAEEAcgB0AGUAbQBpAHMATABpAGMAZQBuAHMAZQBTAGUAcgB2AGUAcgAvAFAAbABhAHkAUgBlAGEAZAB5AE0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwAQwBJAEQAPgAxADcANQA4ADIANgA8AC8AQwBJAEQAPgA8AEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4ANwAuADEALgAxADUANgA1AC4ANAA8AC8ASQBJAFMAXwBEAFIATQBfAFYARQBSAFMASQBPAE4APgA8AC8AQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A
|
||||
</ProtectionHeader>
|
||||
</Protection>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
|
|||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;
|
||||
import com.google.android.exoplayer2.offline.StreamKey;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;
|
||||
|
|
@ -36,7 +37,7 @@ import org.robolectric.RobolectricTestRunner;
|
|||
public class SsManifestTest {
|
||||
|
||||
private static final ProtectionElement DUMMY_PROTECTION_ELEMENT =
|
||||
new ProtectionElement(C.WIDEVINE_UUID, new byte[] {0, 1, 2});
|
||||
new ProtectionElement(C.WIDEVINE_UUID, new byte[0], new TrackEncryptionBox[0]);
|
||||
|
||||
@Test
|
||||
public void testCopy() throws Exception {
|
||||
|
|
|
|||
|
|
@ -137,23 +137,40 @@ public class DebugTextViewHelper implements Player.EventListener, Runnable {
|
|||
/** Returns a string containing video debugging information. */
|
||||
protected String getVideoString() {
|
||||
Format format = player.getVideoFormat();
|
||||
if (format == null) {
|
||||
DecoderCounters decoderCounters = player.getVideoDecoderCounters();
|
||||
if (format == null || decoderCounters == null) {
|
||||
return "";
|
||||
}
|
||||
return "\n" + format.sampleMimeType + "(id:" + format.id + " r:" + format.width + "x"
|
||||
+ format.height + getPixelAspectRatioString(format.pixelWidthHeightRatio)
|
||||
+ getDecoderCountersBufferCountString(player.getVideoDecoderCounters()) + ")";
|
||||
return "\n"
|
||||
+ format.sampleMimeType
|
||||
+ "(id:"
|
||||
+ format.id
|
||||
+ " r:"
|
||||
+ format.width
|
||||
+ "x"
|
||||
+ format.height
|
||||
+ getPixelAspectRatioString(format.pixelWidthHeightRatio)
|
||||
+ getDecoderCountersBufferCountString(decoderCounters)
|
||||
+ ")";
|
||||
}
|
||||
|
||||
/** Returns a string containing audio debugging information. */
|
||||
protected String getAudioString() {
|
||||
Format format = player.getAudioFormat();
|
||||
if (format == null) {
|
||||
DecoderCounters decoderCounters = player.getAudioDecoderCounters();
|
||||
if (format == null || decoderCounters == null) {
|
||||
return "";
|
||||
}
|
||||
return "\n" + format.sampleMimeType + "(id:" + format.id + " hz:" + format.sampleRate + " ch:"
|
||||
return "\n"
|
||||
+ format.sampleMimeType
|
||||
+ "(id:"
|
||||
+ format.id
|
||||
+ " hz:"
|
||||
+ format.sampleRate
|
||||
+ " ch:"
|
||||
+ format.channelCount
|
||||
+ getDecoderCountersBufferCountString(player.getAudioDecoderCounters()) + ")";
|
||||
+ getDecoderCountersBufferCountString(decoderCounters)
|
||||
+ ")";
|
||||
}
|
||||
|
||||
private static String getDecoderCountersBufferCountString(DecoderCounters counters) {
|
||||
|
|
|
|||
|
|
@ -125,6 +125,18 @@ public class PlayerNotificationManager {
|
|||
@Nullable
|
||||
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.
|
||||
*
|
||||
|
|
@ -832,6 +844,7 @@ public class PlayerNotificationManager {
|
|||
// Set media specific notification properties from MediaDescriptionAdapter.
|
||||
builder.setContentTitle(mediaDescriptionAdapter.getCurrentContentTitle(player));
|
||||
builder.setContentText(mediaDescriptionAdapter.getCurrentContentText(player));
|
||||
builder.setSubText(mediaDescriptionAdapter.getCurrentSubText(player));
|
||||
if (largeIcon == null) {
|
||||
largeIcon =
|
||||
mediaDescriptionAdapter.getCurrentLargeIcon(
|
||||
|
|
|
|||
|
|
@ -679,8 +679,9 @@ public class PlayerView extends FrameLayout {
|
|||
/**
|
||||
* Sets whether the currently displayed video frame or media artwork is kept visible when the
|
||||
* player is reset. A player reset is defined to mean the player being re-prepared with different
|
||||
* media, {@link Player#stop(boolean)} being called with {@code reset=true}, or the player being
|
||||
* replaced or cleared by calling {@link #setPlayer(Player)}.
|
||||
* media, the player transitioning to unprepared media, {@link Player#stop(boolean)} being called
|
||||
* with {@code reset=true}, or the player being replaced or cleared by calling {@link
|
||||
* #setPlayer(Player)}.
|
||||
*
|
||||
* <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
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||
}
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
|
||||
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
assertThat(preparedSource).isTrue();
|
||||
assertThat(releasedSource).isFalse();
|
||||
int periodIndex = timeline.getIndexOfPeriod(id.periodUid);
|
||||
|
|
|
|||
|
|
@ -142,15 +142,28 @@ public class MediaSourceTestRunner {
|
|||
}
|
||||
|
||||
/**
|
||||
* Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator)} on the playback
|
||||
* thread, asserting that a non-null {@link MediaPeriod} is returned.
|
||||
* Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator, long)} with a zero
|
||||
* start position on the playback thread, asserting that a non-null {@link MediaPeriod} is
|
||||
* returned.
|
||||
*
|
||||
* @param periodId The id of the period to create.
|
||||
* @return The created {@link MediaPeriod}.
|
||||
*/
|
||||
public MediaPeriod createPeriod(final MediaPeriodId periodId) {
|
||||
return createPeriod(periodId, /* startPositionUs= */ 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator, long)} on the
|
||||
* playback thread, asserting that a non-null {@link MediaPeriod} is returned.
|
||||
*
|
||||
* @param periodId The id of the period to create.
|
||||
* @return The created {@link MediaPeriod}.
|
||||
*/
|
||||
public MediaPeriod createPeriod(final MediaPeriodId periodId, long startPositionUs) {
|
||||
final MediaPeriod[] holder = new MediaPeriod[1];
|
||||
runOnPlaybackThread(() -> holder[0] = mediaSource.createPeriod(periodId, allocator));
|
||||
runOnPlaybackThread(
|
||||
() -> holder[0] = mediaSource.createPeriod(periodId, allocator, startPositionUs));
|
||||
assertThat(holder[0]).isNotNull();
|
||||
return holder[0];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue