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

r2.9.4
This commit is contained in:
Oliver Woodman 2019-01-21 20:57:18 +00:00 committed by GitHub
commit 1c4ea26ff0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 1031 additions and 318 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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();

View file

@ -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>

View file

@ -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>

View file

@ -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')

View file

@ -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);

View file

@ -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.

View file

@ -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 {

View file

@ -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;

View file

@ -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

View file

@ -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();
}

View file

@ -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

View file

@ -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.
*

View file

@ -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}

View file

@ -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,

View file

@ -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(

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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()) {

View file

@ -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),

View file

@ -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");

View file

@ -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;

View file

@ -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];
}
};
}

View file

@ -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;

View file

@ -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 {

View file

@ -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;
}

View file

@ -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;
}
}
/**

View file

@ -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)

View file

@ -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);

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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

View file

@ -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);

View file

@ -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;
}

View file

@ -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.

View file

@ -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);
}

View file

@ -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.

View file

@ -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();

View file

@ -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,

View file

@ -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;
}

View file

@ -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.
*

View file

@ -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;
}

View file

@ -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

View 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":

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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,

View file

@ -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;

View file

@ -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);

View file

@ -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,

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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(

View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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;
}

View file

@ -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>

View file

@ -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>

View file

@ -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 {

View file

@ -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) {

View file

@ -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(

View file

@ -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

View file

@ -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);

View file

@ -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];
}