Use {@link #DEFAULT} to output to {@link android.util.Log}.
+ */
+ public interface Logger {
+
+ /** The default instance logging to {@link android.util.Log}. */
+ Logger DEFAULT =
+ new Logger() {
+ @Override
+ public void d(String tag, String message) {
+ android.util.Log.d(tag, message);
+ }
+
+ @Override
+ public void i(String tag, String message) {
+ android.util.Log.i(tag, message);
+ }
+
+ @Override
+ public void w(String tag, String message) {
+ android.util.Log.w(tag, message);
+ }
+
+ @Override
+ public void e(String tag, String message) {
+ android.util.Log.e(tag, message);
+ }
+ };
+
+ /**
+ * Logs a debug-level message.
+ *
+ * @param tag The tag of the message.
+ * @param message The message.
+ */
+ void d(String tag, String message);
+
+ /**
+ * Logs an information-level message.
+ *
+ * @param tag The tag of the message.
+ * @param message The message.
+ */
+ void i(String tag, String message);
+
+ /**
+ * Logs a warning-level message.
+ *
+ * @param tag The tag of the message.
+ * @param message The message.
+ */
+ void w(String tag, String message);
+
+ /**
+ * Logs an error-level message.
+ *
+ * @param tag The tag of the message.
+ * @param message The message.
+ */
+ void e(String tag, String message);
+ }
+
+ private static final Object lock = new Object();
+
+ @GuardedBy("lock")
private static int logLevel = LOG_LEVEL_ALL;
+
+ @GuardedBy("lock")
private static boolean logStackTraces = true;
+ @GuardedBy("lock")
+ private static Logger logger = Logger.DEFAULT;
+
private Log() {}
/** Returns current {@link LogLevel} for ExoPlayer logcat logging. */
@Pure
public static @LogLevel int getLogLevel() {
- return logLevel;
+ synchronized (lock) {
+ return logLevel;
+ }
}
/**
@@ -69,7 +147,9 @@ public final class Log {
* @param logLevel The new {@link LogLevel}.
*/
public static void setLogLevel(@LogLevel int logLevel) {
- Log.logLevel = logLevel;
+ synchronized (lock) {
+ Log.logLevel = logLevel;
+ }
}
/**
@@ -79,7 +159,20 @@ public final class Log {
* @param logStackTraces Whether stack traces will be logged.
*/
public static void setLogStackTraces(boolean logStackTraces) {
- Log.logStackTraces = logStackTraces;
+ synchronized (lock) {
+ Log.logStackTraces = logStackTraces;
+ }
+ }
+
+ /**
+ * Sets a custom {@link Logger} as the output.
+ *
+ * @param logger The {@link Logger}.
+ */
+ public static void setLogger(Logger logger) {
+ synchronized (lock) {
+ Log.logger = logger;
+ }
}
/**
@@ -87,8 +180,10 @@ public final class Log {
*/
@Pure
public static void d(@Size(max = 23) String tag, String message) {
- if (logLevel == LOG_LEVEL_ALL) {
- android.util.Log.d(tag, message);
+ synchronized (lock) {
+ if (logLevel == LOG_LEVEL_ALL) {
+ logger.d(tag, message);
+ }
}
}
@@ -105,8 +200,10 @@ public final class Log {
*/
@Pure
public static void i(@Size(max = 23) String tag, String message) {
- if (logLevel <= LOG_LEVEL_INFO) {
- android.util.Log.i(tag, message);
+ synchronized (lock) {
+ if (logLevel <= LOG_LEVEL_INFO) {
+ logger.i(tag, message);
+ }
}
}
@@ -123,8 +220,10 @@ public final class Log {
*/
@Pure
public static void w(@Size(max = 23) String tag, String message) {
- if (logLevel <= LOG_LEVEL_WARNING) {
- android.util.Log.w(tag, message);
+ synchronized (lock) {
+ if (logLevel <= LOG_LEVEL_WARNING) {
+ logger.w(tag, message);
+ }
}
}
@@ -141,8 +240,10 @@ public final class Log {
*/
@Pure
public static void e(@Size(max = 23) String tag, String message) {
- if (logLevel <= LOG_LEVEL_ERROR) {
- android.util.Log.e(tag, message);
+ synchronized (lock) {
+ if (logLevel <= LOG_LEVEL_ERROR) {
+ logger.e(tag, message);
+ }
}
}
@@ -168,20 +269,23 @@ public final class Log {
@Nullable
@Pure
public static String getThrowableString(@Nullable Throwable throwable) {
- if (throwable == null) {
- return null;
- } else if (isCausedByUnknownHostException(throwable)) {
- // UnknownHostException implies the device doesn't have network connectivity.
- // UnknownHostException.getMessage() may return a string that's more verbose than desired for
- // logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has
- // special handling to return the empty string, which can result in logging that doesn't
- // indicate the failure mode at all. Hence we special case this exception to always return a
- // concise but useful message.
- return "UnknownHostException (no network)";
- } else if (!logStackTraces) {
- return throwable.getMessage();
- } else {
- return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " ");
+ synchronized (lock) {
+ if (throwable == null) {
+ return null;
+ } else if (isCausedByUnknownHostException(throwable)) {
+ // UnknownHostException implies the device doesn't have network connectivity.
+ // UnknownHostException.getMessage() may return a string that's more verbose than desired
+ // for
+ // logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has
+ // special handling to return the empty string, which can result in logging that doesn't
+ // indicate the failure mode at all. Hence we special case this exception to always return a
+ // concise but useful message.
+ return "UnknownHostException (no network)";
+ } else if (!logStackTraces) {
+ return throwable.getMessage();
+ } else {
+ return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " ");
+ }
}
}
diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java
index 7238fad0fe..7ea6c3d1f8 100644
--- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java
+++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java
@@ -55,6 +55,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Parcel;
import android.os.SystemClock;
+import android.provider.MediaStore;
import android.security.NetworkSecurityPolicy;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -199,7 +200,7 @@ public final class Util {
@UnstableApi
@Nullable
public static ComponentName startForegroundService(Context context, Intent intent) {
- if (Util.SDK_INT >= 26) {
+ if (SDK_INT >= 26) {
return context.startForegroundService(intent);
} else {
return context.startService(intent);
@@ -215,12 +216,12 @@ public final class Util {
* @return Whether a permission request was made.
*/
public static boolean maybeRequestReadExternalStoragePermission(Activity activity, Uri... uris) {
- if (Util.SDK_INT < 23) {
+ if (SDK_INT < 23) {
return false;
}
for (Uri uri : uris) {
- if (isLocalFileUri(uri)) {
- return requestExternalStoragePermission(activity);
+ if (maybeRequestReadExternalStoragePermission(activity, uri)) {
+ return true;
}
}
return false;
@@ -238,25 +239,46 @@ public final class Util {
*/
public static boolean maybeRequestReadExternalStoragePermission(
Activity activity, MediaItem... mediaItems) {
- if (Util.SDK_INT < 23) {
+ if (SDK_INT < 23) {
return false;
}
for (MediaItem mediaItem : mediaItems) {
if (mediaItem.localConfiguration == null) {
continue;
}
- if (isLocalFileUri(mediaItem.localConfiguration.uri)) {
- return requestExternalStoragePermission(activity);
+ if (maybeRequestReadExternalStoragePermission(activity, mediaItem.localConfiguration.uri)) {
+ return true;
}
- for (int i = 0; i < mediaItem.localConfiguration.subtitleConfigurations.size(); i++) {
- if (isLocalFileUri(mediaItem.localConfiguration.subtitleConfigurations.get(i).uri)) {
- return requestExternalStoragePermission(activity);
+ List Both {@link #onCues(List)} and {@link #onCues(CueGroup)} are called when there is a change
- * in the cues You should only implement one or the other.
+ * in the cues. You should only implement one or the other.
*/
void onCues(CueGroup cueGroup);
}
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
index 7773fa039e..6c091844a4 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
@@ -629,7 +629,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
surface = placeholderSurface;
} else {
MediaCodecInfo codecInfo = getCodecInfo();
- if (codecInfo != null && shouldUseDummySurface(codecInfo)) {
+ if (codecInfo != null && shouldUsePlaceholderSurface(codecInfo)) {
placeholderSurface = PlaceholderSurface.newInstanceV17(context, codecInfo.secure);
surface = placeholderSurface;
}
@@ -675,7 +675,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override
protected boolean shouldInitCodec(MediaCodecInfo codecInfo) {
- return surface != null || shouldUseDummySurface(codecInfo);
+ return surface != null || shouldUsePlaceholderSurface(codecInfo);
}
@Override
@@ -706,7 +706,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
deviceNeedsNoPostProcessWorkaround,
tunneling ? tunnelingAudioSessionId : C.AUDIO_SESSION_ID_UNSET);
if (surface == null) {
- if (!shouldUseDummySurface(codecInfo)) {
+ if (!shouldUsePlaceholderSurface(codecInfo)) {
throw new IllegalStateException();
}
if (placeholderSurface == null) {
@@ -1333,7 +1333,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
maybeNotifyRenderedFirstFrame();
}
- private boolean shouldUseDummySurface(MediaCodecInfo codecInfo) {
+ private boolean shouldUsePlaceholderSurface(MediaCodecInfo codecInfo) {
return Util.SDK_INT >= 23
&& !tunneling
&& !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name)
@@ -1572,7 +1572,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
if (haveUnknownDimensions) {
Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight);
- Point codecMaxSize = getCodecMaxSize(codecInfo, format);
+ @Nullable Point codecMaxSize = getCodecMaxSize(codecInfo, format);
if (codecMaxSize != null) {
maxWidth = max(maxWidth, codecMaxSize.x);
maxHeight = max(maxHeight, codecMaxSize.y);
@@ -1600,8 +1600,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
*
* @param codecInfo Information about the {@link MediaCodec} being configured.
* @param format The {@link Format} for which the codec is being configured.
- * @return The maximum video size to use, or null if the size of {@code format} should be used.
+ * @return The maximum video size to use, or {@code null} if the size of {@code format} should be
+ * used.
*/
+ @Nullable
private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) {
boolean isVerticalVideo = format.height > format.width;
int formatLongEdgePx = isVerticalVideo ? format.height : format.width;
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
index 784f6c23df..7c1382bba9 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
@@ -53,13 +53,13 @@ import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.o
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
import static androidx.media3.test.utils.TestUtil.assertTimelinesSame;
+import static androidx.media3.test.utils.TestUtil.timelinesAreSame;
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilPosition;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilStartOfMediaItem;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPositionDiscontinuity;
-import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilReceiveOffloadSchedulingEnabledNewState;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilSleepingForOffload;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilTimelineChanged;
import static com.google.common.truth.Truth.assertThat;
@@ -125,6 +125,7 @@ import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.MediaSourceEventListener;
+import androidx.media3.exoplayer.source.ShuffleOrder;
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource;
@@ -157,7 +158,6 @@ import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import androidx.media3.test.utils.FakeTrackSelection;
import androidx.media3.test.utils.FakeTrackSelector;
import androidx.media3.test.utils.FakeVideoRenderer;
-import androidx.media3.test.utils.NoUidTimeline;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
@@ -6512,6 +6512,53 @@ public final class ExoPlayerTest {
assertThat(positionAfterSetShuffleOrder.get()).isAtLeast(5000);
}
+ @Test
+ public void setShuffleOrder_notifiesTimelineChanged() throws Exception {
+ ExoPlayer player =
+ new TestExoPlayerBuilder(context)
+ .setClock(new FakeClock(/* isAutoAdvancing= */ true))
+ .build();
+ // No callback expected for this call, because the (empty) timeline doesn't change. We start
+ // with a deterministic shuffle order, to ensure when we call setShuffleOrder again below the
+ // order is definitely different (otherwise the test is flaky when the existing shuffle order
+ // matches the shuffle order passed in below).
+ player.setShuffleOrder(new FakeShuffleOrder(0));
+ player.setMediaSources(
+ ImmutableList.of(new FakeMediaSource(), new FakeMediaSource(), new FakeMediaSource()));
+ Player.Listener mockListener = mock(Player.Listener.class);
+ player.addListener(mockListener);
+ player.prepare();
+ TestPlayerRunHelper.playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 5000);
+ player.play();
+ ShuffleOrder.DefaultShuffleOrder newShuffleOrder =
+ new ShuffleOrder.DefaultShuffleOrder(player.getMediaItemCount(), /* randomSeed= */ 5);
+ player.setShuffleOrder(newShuffleOrder);
+ TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
+ player.release();
+
+ ArgumentCaptor Call this method only after receiving an end of a VP8 partition.
+ */
+ private void outputSampleMetadataForFragmentedPackets() {
+ checkNotNull(trackOutput)
+ .sampleMetadata(
+ fragmentedSampleTimeUs,
+ isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
+ fragmentedSampleSizeBytes,
+ /* offset= */ 0,
+ /* cryptoData= */ null);
+ fragmentedSampleSizeBytes = 0;
+ fragmentedSampleTimeUs = C.TIME_UNSET;
+ gotFirstPacketOfVp8Frame = false;
+ }
+
private static long toSampleUs(
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) {
return startTimeOffsetUs
diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8ReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8ReaderTest.java
new file mode 100644
index 0000000000..61f80c6c2d
--- /dev/null
+++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8ReaderTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2022 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 androidx.media3.exoplayer.rtsp.reader;
+
+import static androidx.media3.common.util.Util.getBytesFromHexString;
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.media3.common.C;
+import androidx.media3.common.Format;
+import androidx.media3.common.MimeTypes;
+import androidx.media3.common.util.ParsableByteArray;
+import androidx.media3.common.util.Util;
+import androidx.media3.exoplayer.rtsp.RtpPacket;
+import androidx.media3.exoplayer.rtsp.RtpPayloadFormat;
+import androidx.media3.test.utils.FakeExtractorOutput;
+import androidx.media3.test.utils.FakeTrackOutput;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.primitives.Bytes;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link RtpVp8Reader}. */
+@RunWith(AndroidJUnit4.class)
+public final class RtpVp8ReaderTest {
+
+ /** VP9 uses a 90 KHz media clock (RFC7741 Section 4.1). */
+ private static final long MEDIA_CLOCK_FREQUENCY = 90_000;
+
+ private static final byte[] PARTITION_1 = getBytesFromHexString("000102030405060708090A0B0C0D0E");
+ // 000102030405060708090A
+ private static final byte[] PARTITION_1_FRAGMENT_1 =
+ Arrays.copyOf(PARTITION_1, /* newLength= */ 11);
+ // 0B0C0D0E
+ private static final byte[] PARTITION_1_FRAGMENT_2 =
+ Arrays.copyOfRange(PARTITION_1, /* from= */ 11, /* to= */ 15);
+ private static final long PARTITION_1_RTP_TIMESTAMP = 2599168056L;
+ private static final RtpPacket PACKET_PARTITION_1_FRAGMENT_1 =
+ new RtpPacket.Builder()
+ .setTimestamp(PARTITION_1_RTP_TIMESTAMP)
+ .setSequenceNumber(40289)
+ .setMarker(false)
+ .setPayloadData(Bytes.concat(getBytesFromHexString("10"), PARTITION_1_FRAGMENT_1))
+ .build();
+ private static final RtpPacket PACKET_PARTITION_1_FRAGMENT_2 =
+ new RtpPacket.Builder()
+ .setTimestamp(PARTITION_1_RTP_TIMESTAMP)
+ .setSequenceNumber(40290)
+ .setMarker(false)
+ .setPayloadData(Bytes.concat(getBytesFromHexString("00"), PARTITION_1_FRAGMENT_2))
+ .build();
+
+ private static final byte[] PARTITION_2 = getBytesFromHexString("0D0C0B0A09080706050403020100");
+ // 0D0C0B0A090807060504
+ private static final byte[] PARTITION_2_FRAGMENT_1 =
+ Arrays.copyOf(PARTITION_2, /* newLength= */ 10);
+ // 03020100
+ private static final byte[] PARTITION_2_FRAGMENT_2 =
+ Arrays.copyOfRange(PARTITION_2, /* from= */ 10, /* to= */ 14);
+ private static final long PARTITION_2_RTP_TIMESTAMP = 2599168344L;
+ private static final RtpPacket PACKET_PARTITION_2_FRAGMENT_1 =
+ new RtpPacket.Builder()
+ .setTimestamp(PARTITION_2_RTP_TIMESTAMP)
+ .setSequenceNumber(40291)
+ .setMarker(false)
+ .setPayloadData(Bytes.concat(getBytesFromHexString("10"), PARTITION_2_FRAGMENT_1))
+ .build();
+ private static final RtpPacket PACKET_PARTITION_2_FRAGMENT_2 =
+ new RtpPacket.Builder()
+ .setTimestamp(PARTITION_2_RTP_TIMESTAMP)
+ .setSequenceNumber(40292)
+ .setMarker(true)
+ .setPayloadData(
+ Bytes.concat(
+ getBytesFromHexString("80"),
+ // Optional header.
+ getBytesFromHexString("D6AA953961"),
+ PARTITION_2_FRAGMENT_2))
+ .build();
+ private static final long PARTITION_2_PRESENTATION_TIMESTAMP_US =
+ Util.scaleLargeTimestamp(
+ (PARTITION_2_RTP_TIMESTAMP - PARTITION_1_RTP_TIMESTAMP),
+ /* multiplier= */ C.MICROS_PER_SECOND,
+ /* divisor= */ MEDIA_CLOCK_FREQUENCY);
+
+ private FakeExtractorOutput extractorOutput;
+
+ @Before
+ public void setUp() {
+ extractorOutput =
+ new FakeExtractorOutput(
+ (id, type) -> new FakeTrackOutput(/* deduplicateConsecutiveFormats= */ true));
+ }
+
+ @Test
+ public void consume_validPackets() {
+ RtpVp8Reader vp8Reader = createVp8Reader();
+
+ vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
+ vp8Reader.onReceivingFirstPacket(
+ PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
+ consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
+
+ FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
+ assertThat(trackOutput.getSampleCount()).isEqualTo(2);
+ assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1);
+ assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
+ assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
+ assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
+ }
+
+ @Test
+ public void consume_fragmentedFrameMissingFirstFragment() {
+ RtpVp8Reader vp8Reader = createVp8Reader();
+
+ vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
+ // First packet timing information is transmitted over RTSP, not RTP.
+ vp8Reader.onReceivingFirstPacket(
+ PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
+ consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
+
+ FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
+ assertThat(trackOutput.getSampleCount()).isEqualTo(1);
+ assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_2);
+ assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
+ }
+
+ @Test
+ public void consume_fragmentedFrameMissingBoundaryFragment() {
+ RtpVp8Reader vp8Reader = createVp8Reader();
+
+ vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
+ vp8Reader.onReceivingFirstPacket(
+ PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
+ consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
+
+ FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
+ assertThat(trackOutput.getSampleCount()).isEqualTo(2);
+ assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1_FRAGMENT_1);
+ assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
+ assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
+ assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
+ }
+
+ @Test
+ public void consume_outOfOrderFragmentedFrame() {
+ RtpVp8Reader vp8Reader = createVp8Reader();
+
+ vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
+ vp8Reader.onReceivingFirstPacket(
+ PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
+ consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
+ consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
+ consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
+
+ FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
+ assertThat(trackOutput.getSampleCount()).isEqualTo(2);
+ assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1_FRAGMENT_1);
+ assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
+ assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
+ assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
+ }
+
+ private static RtpVp8Reader createVp8Reader() {
+ return new RtpVp8Reader(
+ new RtpPayloadFormat(
+ new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_VP8).build(),
+ /* rtpPayloadType= */ 96,
+ /* clockRate= */ (int) MEDIA_CLOCK_FREQUENCY,
+ /* fmtpParameters= */ ImmutableMap.of()));
+ }
+
+ private static void consume(RtpVp8Reader vp8Reader, RtpPacket rtpPacket) {
+ vp8Reader.consume(
+ new ParsableByteArray(rtpPacket.payloadData),
+ rtpPacket.timestamp,
+ rtpPacket.sequenceNumber,
+ rtpPacket.marker);
+ }
+}
diff --git a/libraries/exoplayer_smoothstreaming/build.gradle b/libraries/exoplayer_smoothstreaming/build.gradle
index a379d25558..4b145ec6b3 100644
--- a/libraries/exoplayer_smoothstreaming/build.gradle
+++ b/libraries/exoplayer_smoothstreaming/build.gradle
@@ -29,6 +29,7 @@ dependencies {
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
+ testImplementation project(modulePrefix + 'test-utils-robolectric')
testImplementation project(modulePrefix + 'test-utils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}
diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultMediaSourceFactoryTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultMediaSourceFactoryTest.java
index f5a205fcbe..4036fb9472 100644
--- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultMediaSourceFactoryTest.java
+++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultMediaSourceFactoryTest.java
@@ -15,16 +15,21 @@
*/
package androidx.media3.exoplayer.smoothstreaming;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
+import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
+import androidx.media3.test.utils.FakeDataSource;
+import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -93,4 +98,53 @@ public class DefaultMediaSourceFactoryTest {
assertThat(supportedTypes).asList().containsExactly(C.CONTENT_TYPE_OTHER, C.CONTENT_TYPE_SS);
}
+
+ @Test
+ public void createMediaSource_withSetDataSourceFactory_usesDataSourceFactory() throws Exception {
+ FakeDataSource fakeDataSource = new FakeDataSource();
+ DefaultMediaSourceFactory defaultMediaSourceFactory =
+ new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext())
+ .setDataSourceFactory(() -> fakeDataSource);
+
+ prepareSsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
+
+ assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
+ }
+
+ @Test
+ public void
+ createMediaSource_usingDefaultDataSourceFactoryAndSetDataSourceFactory_usesUpdatesDataSourceFactory()
+ throws Exception {
+ FakeDataSource fakeDataSource = new FakeDataSource();
+ DefaultMediaSourceFactory defaultMediaSourceFactory =
+ new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext());
+
+ // Use default DataSource.Factory first.
+ prepareSsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
+ defaultMediaSourceFactory.setDataSourceFactory(() -> fakeDataSource);
+ prepareSsUrlAndWaitForPrepareError(defaultMediaSourceFactory);
+
+ assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty();
+ }
+
+ private static void prepareSsUrlAndWaitForPrepareError(
+ DefaultMediaSourceFactory defaultMediaSourceFactory) throws Exception {
+ MediaSource mediaSource =
+ defaultMediaSourceFactory.createMediaSource(MediaItem.fromUri(URI_MEDIA + "/file.ism"));
+ getInstrumentation()
+ .runOnMainSync(
+ () ->
+ mediaSource.prepareSource(
+ (source, timeline) -> {}, /* mediaTransferListener= */ null, PlayerId.UNSET));
+ // We don't expect this to prepare successfully.
+ RobolectricUtil.runMainLooperUntil(
+ () -> {
+ try {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ return false;
+ } catch (IOException e) {
+ return true;
+ }
+ });
+ }
}
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java
index c61d7eaba5..2a401cbbae 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/NalUnitUtil.java
@@ -18,6 +18,7 @@ package androidx.media3.extractor;
import static java.lang.Math.min;
import androidx.annotation.Nullable;
+import androidx.media3.common.C;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
@@ -786,40 +787,105 @@ public final class NalUnitUtil {
}
}
+ /**
+ * Skips any short term reference picture sets contained in a SPS.
+ *
+ * Note: The st_ref_pic_set parsing in this method is simplified for the case where they're
+ * contained in a SPS, and would need generalizing for use elsewhere.
+ */
private static void skipShortTermReferencePictureSets(ParsableNalUnitBitArray bitArray) {
int numShortTermRefPicSets = bitArray.readUnsignedExpGolombCodedInt();
- boolean interRefPicSetPredictionFlag = false;
- int numNegativePics;
- int numPositivePics;
- // As this method applies in a SPS, the only element of NumDeltaPocs accessed is the previous
- // one, so we just keep track of that rather than storing the whole array.
- // RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1) and delta_idx_minus1 is always zero in SPS.
- int previousNumDeltaPocs = 0;
+ // As this method applies in a SPS, each short term reference picture set only accesses data
+ // from the previous one. This is because RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1), and
+ // delta_idx_minus1 is always zero in a SPS. Hence we just keep track of variables from the
+ // previous one as we iterate.
+ int previousNumNegativePics = C.INDEX_UNSET;
+ int previousNumPositivePics = C.INDEX_UNSET;
+ int[] previousDeltaPocS0 = new int[0];
+ int[] previousDeltaPocS1 = new int[0];
for (int stRpsIdx = 0; stRpsIdx < numShortTermRefPicSets; stRpsIdx++) {
- if (stRpsIdx != 0) {
- interRefPicSetPredictionFlag = bitArray.readBit();
- }
+ int numNegativePics;
+ int numPositivePics;
+ int[] deltaPocS0;
+ int[] deltaPocS1;
+
+ boolean interRefPicSetPredictionFlag = stRpsIdx != 0 && bitArray.readBit();
if (interRefPicSetPredictionFlag) {
- bitArray.skipBit(); // delta_rps_sign
- bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1
+ int previousNumDeltaPocs = previousNumNegativePics + previousNumPositivePics;
+
+ int deltaRpsSign = bitArray.readBit() ? 1 : 0;
+ int absDeltaRps = bitArray.readUnsignedExpGolombCodedInt() + 1;
+ int deltaRps = (1 - 2 * deltaRpsSign) * absDeltaRps;
+
+ boolean[] useDeltaFlags = new boolean[previousNumDeltaPocs + 1];
for (int j = 0; j <= previousNumDeltaPocs; j++) {
if (!bitArray.readBit()) { // used_by_curr_pic_flag[j]
- bitArray.skipBit(); // use_delta_flag[j]
+ useDeltaFlags[j] = bitArray.readBit();
+ } else {
+ // When use_delta_flag[j] is not present, its value is 1.
+ useDeltaFlags[j] = true;
}
}
+
+ // Derive numNegativePics, numPositivePics, deltaPocS0 and deltaPocS1 as per Rec. ITU-T
+ // H.265 v6 (06/2019) Section 7.4.8
+ int i = 0;
+ deltaPocS0 = new int[previousNumDeltaPocs + 1];
+ deltaPocS1 = new int[previousNumDeltaPocs + 1];
+ for (int j = previousNumPositivePics - 1; j >= 0; j--) {
+ int dPoc = previousDeltaPocS1[j] + deltaRps;
+ if (dPoc < 0 && useDeltaFlags[previousNumNegativePics + j]) {
+ deltaPocS0[i++] = dPoc;
+ }
+ }
+ if (deltaRps < 0 && useDeltaFlags[previousNumDeltaPocs]) {
+ deltaPocS0[i++] = deltaRps;
+ }
+ for (int j = 0; j < previousNumNegativePics; j++) {
+ int dPoc = previousDeltaPocS0[j] + deltaRps;
+ if (dPoc < 0 && useDeltaFlags[j]) {
+ deltaPocS0[i++] = dPoc;
+ }
+ }
+ numNegativePics = i;
+ deltaPocS0 = Arrays.copyOf(deltaPocS0, numNegativePics);
+
+ i = 0;
+ for (int j = previousNumNegativePics - 1; j >= 0; j--) {
+ int dPoc = previousDeltaPocS0[j] + deltaRps;
+ if (dPoc > 0 && useDeltaFlags[j]) {
+ deltaPocS1[i++] = dPoc;
+ }
+ }
+ if (deltaRps > 0 && useDeltaFlags[previousNumDeltaPocs]) {
+ deltaPocS1[i++] = deltaRps;
+ }
+ for (int j = 0; j < previousNumPositivePics; j++) {
+ int dPoc = previousDeltaPocS1[j] + deltaRps;
+ if (dPoc > 0 && useDeltaFlags[previousNumNegativePics + j]) {
+ deltaPocS1[i++] = dPoc;
+ }
+ }
+ numPositivePics = i;
+ deltaPocS1 = Arrays.copyOf(deltaPocS1, numPositivePics);
} else {
numNegativePics = bitArray.readUnsignedExpGolombCodedInt();
numPositivePics = bitArray.readUnsignedExpGolombCodedInt();
- previousNumDeltaPocs = numNegativePics + numPositivePics;
+ deltaPocS0 = new int[numNegativePics];
for (int i = 0; i < numNegativePics; i++) {
- bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[i]
+ deltaPocS0[i] = bitArray.readUnsignedExpGolombCodedInt() + 1;
bitArray.skipBit(); // used_by_curr_pic_s0_flag[i]
}
+ deltaPocS1 = new int[numPositivePics];
for (int i = 0; i < numPositivePics; i++) {
- bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i]
+ deltaPocS1[i] = bitArray.readUnsignedExpGolombCodedInt() + 1;
bitArray.skipBit(); // used_by_curr_pic_s1_flag[i]
}
}
+ previousNumNegativePics = numNegativePics;
+ previousNumPositivePics = numPositivePics;
+ previousDeltaPocS0 = deltaPocS0;
+ previousDeltaPocS1 = deltaPocS1;
}
}
diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java
index 05511fc7e5..4543d32819 100644
--- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java
+++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java
@@ -45,6 +45,7 @@ import androidx.media3.extractor.OpusUtil;
import androidx.media3.extractor.metadata.mp4.SmtaMetadataEntry;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
@@ -1303,7 +1304,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
if (esdsData != null) {
- formatBuilder.setAverageBitrate(esdsData.bitrate).setPeakBitrate(esdsData.peakBitrate);
+ formatBuilder
+ .setAverageBitrate(Ints.saturatedCast(esdsData.bitrate))
+ .setPeakBitrate(Ints.saturatedCast(esdsData.peakBitrate));
}
out.format = formatBuilder.build();
@@ -1609,7 +1612,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
.setLanguage(language);
if (esdsData != null) {
- formatBuilder.setAverageBitrate(esdsData.bitrate).setPeakBitrate(esdsData.peakBitrate);
+ formatBuilder
+ .setAverageBitrate(Ints.saturatedCast(esdsData.bitrate))
+ .setPeakBitrate(Ints.saturatedCast(esdsData.peakBitrate));
}
out.format = formatBuilder.build();
@@ -1659,7 +1664,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
parent.skipBytes(2);
}
if ((flags & 0x40 /* URL_Flag */) != 0) {
- parent.skipBytes(parent.readUnsignedShort());
+ parent.skipBytes(parent.readUnsignedByte());
}
if ((flags & 0x20 /* OCRstreamFlag */) != 0) {
parent.skipBytes(2);
@@ -1683,8 +1688,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
parent.skipBytes(4);
- int peakBitrate = parent.readUnsignedIntToInt();
- int bitrate = parent.readUnsignedIntToInt();
+ long peakBitrate = parent.readUnsignedInt();
+ long bitrate = parent.readUnsignedInt();
// Start of the DecoderSpecificInfo.
parent.skipBytes(1); // DecoderSpecificInfo tag
@@ -1943,14 +1948,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private static final class EsdsData {
private final @NullableType String mimeType;
private final byte @NullableType [] initializationData;
- private final int bitrate;
- private final int peakBitrate;
+ private final long bitrate;
+ private final long peakBitrate;
public EsdsData(
@NullableType String mimeType,
byte @NullableType [] initializationData,
- int bitrate,
- int peakBitrate) {
+ long bitrate,
+ long peakBitrate) {
this.mimeType = mimeType;
this.initializationData = initializationData;
this.bitrate = bitrate;
diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java
index 01d7fe15f9..59dd8543db 100644
--- a/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java
+++ b/libraries/extractor/src/test/java/androidx/media3/extractor/NalUnitUtilTest.java
@@ -170,6 +170,32 @@ public final class NalUnitUtilTest {
assertDiscardToSpsMatchesExpected("FF00000001660000000167FF", "0000000167FF");
}
+ /** Regression test for https://github.com/google/ExoPlayer/issues/10316. */
+ @Test
+ public void parseH265SpsNalUnitPayload_exoghi_10316() {
+ byte[] spsNalUnitPayload =
+ new byte[] {
+ 1, 2, 32, 0, 0, 3, 0, -112, 0, 0, 3, 0, 0, 3, 0, -106, -96, 1, -32, 32, 2, 28, 77, -98,
+ 87, -110, 66, -111, -123, 22, 74, -86, -53, -101, -98, -68, -28, 9, 119, -21, -103, 120,
+ -16, 22, -95, 34, 1, 54, -62, 0, 0, 7, -46, 0, 0, -69, -127, -12, 85, -17, 126, 0, -29,
+ -128, 28, 120, 1, -57, 0, 56, -15
+ };
+
+ NalUnitUtil.H265SpsData spsData =
+ NalUnitUtil.parseH265SpsNalUnitPayload(spsNalUnitPayload, 0, spsNalUnitPayload.length);
+
+ assertThat(spsData.constraintBytes).isEqualTo(new int[] {144, 0, 0, 0, 0, 0});
+ assertThat(spsData.generalLevelIdc).isEqualTo(150);
+ assertThat(spsData.generalProfileCompatibilityFlags).isEqualTo(4);
+ assertThat(spsData.generalProfileIdc).isEqualTo(2);
+ assertThat(spsData.generalProfileSpace).isEqualTo(0);
+ assertThat(spsData.generalTierFlag).isFalse();
+ assertThat(spsData.height).isEqualTo(2160);
+ assertThat(spsData.pixelWidthHeightRatio).isEqualTo(1);
+ assertThat(spsData.seqParameterSetId).isEqualTo(0);
+ assertThat(spsData.width).isEqualTo(3840);
+ }
+
private static byte[] buildTestData() {
byte[] data = new byte[20];
for (int i = 0; i < data.length; i++) {
diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java
index 07c663d426..269ac4291c 100644
--- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java
+++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java
@@ -122,6 +122,15 @@ public final class FragmentedMp4ExtractorTest {
simulationConfig);
}
+ /** https://github.com/google/ExoPlayer/issues/10381 */
+ @Test
+ public void sampleWithLargeBitrates() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ getExtractorFactory(ImmutableList.of()),
+ "media/mp4/sample_fragmented_large_bitrates.mp4",
+ simulationConfig);
+ }
+
private static ExtractorFactory getExtractorFactory(final List To make the custom layout and commands work, you need to {@linkplain
+ * MediaSession#setCustomLayout(List) set the custom layout of commands} and add the custom
+ * commands to the available commands when a controller {@linkplain
+ * MediaSession.Callback#onConnect(MediaSession, MediaSession.ControllerInfo) connects to the
+ * session}. Controllers that connect after you called {@link MediaSession#setCustomLayout(List)}
+ * need the custom command set in {@link MediaSession.Callback#onPostConnect(MediaSession,
+ * MediaSession.ControllerInfo)} also.
+ *
* @param playerCommands The available player commands.
* @param customLayout The {@linkplain MediaSession#setCustomLayout(List) custom layout of
* commands}.
diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump
new file mode 100644
index 0000000000..5b9a721cb6
--- /dev/null
+++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump
@@ -0,0 +1,339 @@
+seekMap:
+ isSeekable = true
+ duration = 1067733
+ getPosition(0) = [[timeUs=66733, position=1325]]
+ getPosition(1) = [[timeUs=66733, position=1325]]
+ getPosition(533866) = [[timeUs=66733, position=1325]]
+ getPosition(1067733) = [[timeUs=66733, position=1325]]
+numberOfTracks = 2
+track 0:
+ total output bytes = 85933
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ codecs = avc1.64001F
+ width = 1080
+ height = 720
+ initializationData:
+ data = length 29, hash 4746B5D9
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 38070, hash B58E1AEE
+ sample 1:
+ time = 200200
+ flags = 0
+ data = length 8340, hash 8AC449FF
+ sample 2:
+ time = 133466
+ flags = 0
+ data = length 1295, hash C0DA5090
+ sample 3:
+ time = 100100
+ flags = 0
+ data = length 469, hash D6E0A200
+ sample 4:
+ time = 166833
+ flags = 0
+ data = length 564, hash E5F56C5B
+ sample 5:
+ time = 333666
+ flags = 0
+ data = length 6075, hash 8756E49E
+ sample 6:
+ time = 266933
+ flags = 0
+ data = length 847, hash DCC2B618
+ sample 7:
+ time = 233566
+ flags = 0
+ data = length 455, hash B9CCE047
+ sample 8:
+ time = 300300
+ flags = 0
+ data = length 467, hash 69806D94
+ sample 9:
+ time = 467133
+ flags = 0
+ data = length 4549, hash 3944F501
+ sample 10:
+ time = 400400
+ flags = 0
+ data = length 1087, hash 491BF106
+ sample 11:
+ time = 367033
+ flags = 0
+ data = length 380, hash 5FED016A
+ sample 12:
+ time = 433766
+ flags = 0
+ data = length 455, hash 8A0610
+ sample 13:
+ time = 600600
+ flags = 0
+ data = length 5190, hash B9031D8
+ sample 14:
+ time = 533866
+ flags = 0
+ data = length 1071, hash 684E7DC8
+ sample 15:
+ time = 500500
+ flags = 0
+ data = length 653, hash 8494F326
+ sample 16:
+ time = 567233
+ flags = 0
+ data = length 485, hash 2CCC85F4
+ sample 17:
+ time = 734066
+ flags = 0
+ data = length 4884, hash D16B6A96
+ sample 18:
+ time = 667333
+ flags = 0
+ data = length 997, hash 164FF210
+ sample 19:
+ time = 633966
+ flags = 0
+ data = length 640, hash F664125B
+ sample 20:
+ time = 700700
+ flags = 0
+ data = length 491, hash B5930C7C
+ sample 21:
+ time = 867533
+ flags = 0
+ data = length 2989, hash 92CF4FCF
+ sample 22:
+ time = 800800
+ flags = 0
+ data = length 838, hash 294A3451
+ sample 23:
+ time = 767433
+ flags = 0
+ data = length 544, hash FCCE2DE6
+ sample 24:
+ time = 834166
+ flags = 0
+ data = length 329, hash A654FFA1
+ sample 25:
+ time = 1001000
+ flags = 0
+ data = length 1517, hash 5F7EBF8B
+ sample 26:
+ time = 934266
+ flags = 0
+ data = length 803, hash 7A5C4C1D
+ sample 27:
+ time = 900900
+ flags = 0
+ data = length 415, hash B31BBC3B
+ sample 28:
+ time = 967633
+ flags = 0
+ data = length 415, hash 850DFEA3
+ sample 29:
+ time = 1034366
+ flags = 0
+ data = length 619, hash AB5E56CA
+track 1:
+ total output bytes = 18257
+ sample count = 46
+ format 0:
+ averageBitrate = 2147483647
+ peakBitrate = 2147483647
+ id = 2
+ sampleMimeType = audio/mp4a-latm
+ codecs = mp4a.40.2
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ initializationData:
+ data = length 5, hash 2B7623A
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 18, hash 96519432
+ sample 1:
+ time = 23219
+ flags = 1
+ data = length 4, hash EE9DF
+ sample 2:
+ time = 46439
+ flags = 1
+ data = length 4, hash EEDBF
+ sample 3:
+ time = 69659
+ flags = 1
+ data = length 157, hash E2F078F4
+ sample 4:
+ time = 92879
+ flags = 1
+ data = length 371, hash B9471F94
+ sample 5:
+ time = 116099
+ flags = 1
+ data = length 373, hash 2AB265CB
+ sample 6:
+ time = 139319
+ flags = 1
+ data = length 402, hash 1295477C
+ sample 7:
+ time = 162539
+ flags = 1
+ data = length 455, hash 2D8146C8
+ sample 8:
+ time = 185759
+ flags = 1
+ data = length 434, hash F2C5D287
+ sample 9:
+ time = 208979
+ flags = 1
+ data = length 450, hash 84143FCD
+ sample 10:
+ time = 232199
+ flags = 1
+ data = length 429, hash EF769D50
+ sample 11:
+ time = 255419
+ flags = 1
+ data = length 450, hash EC3DE692
+ sample 12:
+ time = 278639
+ flags = 1
+ data = length 447, hash 3E519E13
+ sample 13:
+ time = 301859
+ flags = 1
+ data = length 457, hash 1E4F23A0
+ sample 14:
+ time = 325079
+ flags = 1
+ data = length 447, hash A439EA97
+ sample 15:
+ time = 348299
+ flags = 1
+ data = length 456, hash 1E9034C6
+ sample 16:
+ time = 371519
+ flags = 1
+ data = length 398, hash 99DB7345
+ sample 17:
+ time = 394739
+ flags = 1
+ data = length 474, hash 3F05F10A
+ sample 18:
+ time = 417959
+ flags = 1
+ data = length 416, hash C105EE09
+ sample 19:
+ time = 441179
+ flags = 1
+ data = length 454, hash 5FDBE458
+ sample 20:
+ time = 464399
+ flags = 1
+ data = length 438, hash 41A93AC3
+ sample 21:
+ time = 487619
+ flags = 1
+ data = length 443, hash 10FDA652
+ sample 22:
+ time = 510839
+ flags = 1
+ data = length 412, hash 1F791E25
+ sample 23:
+ time = 534058
+ flags = 1
+ data = length 482, hash A6D983D
+ sample 24:
+ time = 557278
+ flags = 1
+ data = length 386, hash BED7392F
+ sample 25:
+ time = 580498
+ flags = 1
+ data = length 463, hash 5309F8C9
+ sample 26:
+ time = 603718
+ flags = 1
+ data = length 394, hash 21C7321F
+ sample 27:
+ time = 626938
+ flags = 1
+ data = length 489, hash 71B4730D
+ sample 28:
+ time = 650158
+ flags = 1
+ data = length 403, hash D9C6DE89
+ sample 29:
+ time = 673378
+ flags = 1
+ data = length 447, hash 9B14B73B
+ sample 30:
+ time = 696598
+ flags = 1
+ data = length 439, hash 4760D35B
+ sample 31:
+ time = 719818
+ flags = 1
+ data = length 463, hash 1601F88D
+ sample 32:
+ time = 743038
+ flags = 1
+ data = length 423, hash D4AE6773
+ sample 33:
+ time = 766258
+ flags = 1
+ data = length 497, hash A3C674D3
+ sample 34:
+ time = 789478
+ flags = 1
+ data = length 419, hash D3734A1F
+ sample 35:
+ time = 812698
+ flags = 1
+ data = length 474, hash DFB41F9
+ sample 36:
+ time = 835918
+ flags = 1
+ data = length 413, hash 53E7CB9F
+ sample 37:
+ time = 859138
+ flags = 1
+ data = length 445, hash D15B0E39
+ sample 38:
+ time = 882358
+ flags = 1
+ data = length 453, hash 77ED81E4
+ sample 39:
+ time = 905578
+ flags = 1
+ data = length 545, hash 3321AEB9
+ sample 40:
+ time = 928798
+ flags = 1
+ data = length 317, hash F557D0E
+ sample 41:
+ time = 952018
+ flags = 1
+ data = length 537, hash ED58CF7B
+ sample 42:
+ time = 975238
+ flags = 1
+ data = length 458, hash 51CDAA10
+ sample 43:
+ time = 998458
+ flags = 1
+ data = length 465, hash CBA1EFD7
+ sample 44:
+ time = 1021678
+ flags = 1
+ data = length 446, hash D6735B8A
+ sample 45:
+ time = 1044897
+ flags = 1
+ data = length 10, hash A453EEBE
+tracksEnded = true
diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump
new file mode 100644
index 0000000000..53cb776780
--- /dev/null
+++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump
@@ -0,0 +1,279 @@
+seekMap:
+ isSeekable = true
+ duration = 1067733
+ getPosition(0) = [[timeUs=66733, position=1325]]
+ getPosition(1) = [[timeUs=66733, position=1325]]
+ getPosition(533866) = [[timeUs=66733, position=1325]]
+ getPosition(1067733) = [[timeUs=66733, position=1325]]
+numberOfTracks = 2
+track 0:
+ total output bytes = 85933
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ codecs = avc1.64001F
+ width = 1080
+ height = 720
+ initializationData:
+ data = length 29, hash 4746B5D9
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 38070, hash B58E1AEE
+ sample 1:
+ time = 200200
+ flags = 0
+ data = length 8340, hash 8AC449FF
+ sample 2:
+ time = 133466
+ flags = 0
+ data = length 1295, hash C0DA5090
+ sample 3:
+ time = 100100
+ flags = 0
+ data = length 469, hash D6E0A200
+ sample 4:
+ time = 166833
+ flags = 0
+ data = length 564, hash E5F56C5B
+ sample 5:
+ time = 333666
+ flags = 0
+ data = length 6075, hash 8756E49E
+ sample 6:
+ time = 266933
+ flags = 0
+ data = length 847, hash DCC2B618
+ sample 7:
+ time = 233566
+ flags = 0
+ data = length 455, hash B9CCE047
+ sample 8:
+ time = 300300
+ flags = 0
+ data = length 467, hash 69806D94
+ sample 9:
+ time = 467133
+ flags = 0
+ data = length 4549, hash 3944F501
+ sample 10:
+ time = 400400
+ flags = 0
+ data = length 1087, hash 491BF106
+ sample 11:
+ time = 367033
+ flags = 0
+ data = length 380, hash 5FED016A
+ sample 12:
+ time = 433766
+ flags = 0
+ data = length 455, hash 8A0610
+ sample 13:
+ time = 600600
+ flags = 0
+ data = length 5190, hash B9031D8
+ sample 14:
+ time = 533866
+ flags = 0
+ data = length 1071, hash 684E7DC8
+ sample 15:
+ time = 500500
+ flags = 0
+ data = length 653, hash 8494F326
+ sample 16:
+ time = 567233
+ flags = 0
+ data = length 485, hash 2CCC85F4
+ sample 17:
+ time = 734066
+ flags = 0
+ data = length 4884, hash D16B6A96
+ sample 18:
+ time = 667333
+ flags = 0
+ data = length 997, hash 164FF210
+ sample 19:
+ time = 633966
+ flags = 0
+ data = length 640, hash F664125B
+ sample 20:
+ time = 700700
+ flags = 0
+ data = length 491, hash B5930C7C
+ sample 21:
+ time = 867533
+ flags = 0
+ data = length 2989, hash 92CF4FCF
+ sample 22:
+ time = 800800
+ flags = 0
+ data = length 838, hash 294A3451
+ sample 23:
+ time = 767433
+ flags = 0
+ data = length 544, hash FCCE2DE6
+ sample 24:
+ time = 834166
+ flags = 0
+ data = length 329, hash A654FFA1
+ sample 25:
+ time = 1001000
+ flags = 0
+ data = length 1517, hash 5F7EBF8B
+ sample 26:
+ time = 934266
+ flags = 0
+ data = length 803, hash 7A5C4C1D
+ sample 27:
+ time = 900900
+ flags = 0
+ data = length 415, hash B31BBC3B
+ sample 28:
+ time = 967633
+ flags = 0
+ data = length 415, hash 850DFEA3
+ sample 29:
+ time = 1034366
+ flags = 0
+ data = length 619, hash AB5E56CA
+track 1:
+ total output bytes = 13359
+ sample count = 31
+ format 0:
+ averageBitrate = 2147483647
+ peakBitrate = 2147483647
+ id = 2
+ sampleMimeType = audio/mp4a-latm
+ codecs = mp4a.40.2
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ initializationData:
+ data = length 5, hash 2B7623A
+ sample 0:
+ time = 348299
+ flags = 1
+ data = length 456, hash 1E9034C6
+ sample 1:
+ time = 371519
+ flags = 1
+ data = length 398, hash 99DB7345
+ sample 2:
+ time = 394739
+ flags = 1
+ data = length 474, hash 3F05F10A
+ sample 3:
+ time = 417959
+ flags = 1
+ data = length 416, hash C105EE09
+ sample 4:
+ time = 441179
+ flags = 1
+ data = length 454, hash 5FDBE458
+ sample 5:
+ time = 464399
+ flags = 1
+ data = length 438, hash 41A93AC3
+ sample 6:
+ time = 487619
+ flags = 1
+ data = length 443, hash 10FDA652
+ sample 7:
+ time = 510839
+ flags = 1
+ data = length 412, hash 1F791E25
+ sample 8:
+ time = 534058
+ flags = 1
+ data = length 482, hash A6D983D
+ sample 9:
+ time = 557278
+ flags = 1
+ data = length 386, hash BED7392F
+ sample 10:
+ time = 580498
+ flags = 1
+ data = length 463, hash 5309F8C9
+ sample 11:
+ time = 603718
+ flags = 1
+ data = length 394, hash 21C7321F
+ sample 12:
+ time = 626938
+ flags = 1
+ data = length 489, hash 71B4730D
+ sample 13:
+ time = 650158
+ flags = 1
+ data = length 403, hash D9C6DE89
+ sample 14:
+ time = 673378
+ flags = 1
+ data = length 447, hash 9B14B73B
+ sample 15:
+ time = 696598
+ flags = 1
+ data = length 439, hash 4760D35B
+ sample 16:
+ time = 719818
+ flags = 1
+ data = length 463, hash 1601F88D
+ sample 17:
+ time = 743038
+ flags = 1
+ data = length 423, hash D4AE6773
+ sample 18:
+ time = 766258
+ flags = 1
+ data = length 497, hash A3C674D3
+ sample 19:
+ time = 789478
+ flags = 1
+ data = length 419, hash D3734A1F
+ sample 20:
+ time = 812698
+ flags = 1
+ data = length 474, hash DFB41F9
+ sample 21:
+ time = 835918
+ flags = 1
+ data = length 413, hash 53E7CB9F
+ sample 22:
+ time = 859138
+ flags = 1
+ data = length 445, hash D15B0E39
+ sample 23:
+ time = 882358
+ flags = 1
+ data = length 453, hash 77ED81E4
+ sample 24:
+ time = 905578
+ flags = 1
+ data = length 545, hash 3321AEB9
+ sample 25:
+ time = 928798
+ flags = 1
+ data = length 317, hash F557D0E
+ sample 26:
+ time = 952018
+ flags = 1
+ data = length 537, hash ED58CF7B
+ sample 27:
+ time = 975238
+ flags = 1
+ data = length 458, hash 51CDAA10
+ sample 28:
+ time = 998458
+ flags = 1
+ data = length 465, hash CBA1EFD7
+ sample 29:
+ time = 1021678
+ flags = 1
+ data = length 446, hash D6735B8A
+ sample 30:
+ time = 1044897
+ flags = 1
+ data = length 10, hash A453EEBE
+tracksEnded = true
diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump
new file mode 100644
index 0000000000..ecb83ddeea
--- /dev/null
+++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump
@@ -0,0 +1,219 @@
+seekMap:
+ isSeekable = true
+ duration = 1067733
+ getPosition(0) = [[timeUs=66733, position=1325]]
+ getPosition(1) = [[timeUs=66733, position=1325]]
+ getPosition(533866) = [[timeUs=66733, position=1325]]
+ getPosition(1067733) = [[timeUs=66733, position=1325]]
+numberOfTracks = 2
+track 0:
+ total output bytes = 85933
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ codecs = avc1.64001F
+ width = 1080
+ height = 720
+ initializationData:
+ data = length 29, hash 4746B5D9
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 38070, hash B58E1AEE
+ sample 1:
+ time = 200200
+ flags = 0
+ data = length 8340, hash 8AC449FF
+ sample 2:
+ time = 133466
+ flags = 0
+ data = length 1295, hash C0DA5090
+ sample 3:
+ time = 100100
+ flags = 0
+ data = length 469, hash D6E0A200
+ sample 4:
+ time = 166833
+ flags = 0
+ data = length 564, hash E5F56C5B
+ sample 5:
+ time = 333666
+ flags = 0
+ data = length 6075, hash 8756E49E
+ sample 6:
+ time = 266933
+ flags = 0
+ data = length 847, hash DCC2B618
+ sample 7:
+ time = 233566
+ flags = 0
+ data = length 455, hash B9CCE047
+ sample 8:
+ time = 300300
+ flags = 0
+ data = length 467, hash 69806D94
+ sample 9:
+ time = 467133
+ flags = 0
+ data = length 4549, hash 3944F501
+ sample 10:
+ time = 400400
+ flags = 0
+ data = length 1087, hash 491BF106
+ sample 11:
+ time = 367033
+ flags = 0
+ data = length 380, hash 5FED016A
+ sample 12:
+ time = 433766
+ flags = 0
+ data = length 455, hash 8A0610
+ sample 13:
+ time = 600600
+ flags = 0
+ data = length 5190, hash B9031D8
+ sample 14:
+ time = 533866
+ flags = 0
+ data = length 1071, hash 684E7DC8
+ sample 15:
+ time = 500500
+ flags = 0
+ data = length 653, hash 8494F326
+ sample 16:
+ time = 567233
+ flags = 0
+ data = length 485, hash 2CCC85F4
+ sample 17:
+ time = 734066
+ flags = 0
+ data = length 4884, hash D16B6A96
+ sample 18:
+ time = 667333
+ flags = 0
+ data = length 997, hash 164FF210
+ sample 19:
+ time = 633966
+ flags = 0
+ data = length 640, hash F664125B
+ sample 20:
+ time = 700700
+ flags = 0
+ data = length 491, hash B5930C7C
+ sample 21:
+ time = 867533
+ flags = 0
+ data = length 2989, hash 92CF4FCF
+ sample 22:
+ time = 800800
+ flags = 0
+ data = length 838, hash 294A3451
+ sample 23:
+ time = 767433
+ flags = 0
+ data = length 544, hash FCCE2DE6
+ sample 24:
+ time = 834166
+ flags = 0
+ data = length 329, hash A654FFA1
+ sample 25:
+ time = 1001000
+ flags = 0
+ data = length 1517, hash 5F7EBF8B
+ sample 26:
+ time = 934266
+ flags = 0
+ data = length 803, hash 7A5C4C1D
+ sample 27:
+ time = 900900
+ flags = 0
+ data = length 415, hash B31BBC3B
+ sample 28:
+ time = 967633
+ flags = 0
+ data = length 415, hash 850DFEA3
+ sample 29:
+ time = 1034366
+ flags = 0
+ data = length 619, hash AB5E56CA
+track 1:
+ total output bytes = 6804
+ sample count = 16
+ format 0:
+ averageBitrate = 2147483647
+ peakBitrate = 2147483647
+ id = 2
+ sampleMimeType = audio/mp4a-latm
+ codecs = mp4a.40.2
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ initializationData:
+ data = length 5, hash 2B7623A
+ sample 0:
+ time = 696598
+ flags = 1
+ data = length 439, hash 4760D35B
+ sample 1:
+ time = 719818
+ flags = 1
+ data = length 463, hash 1601F88D
+ sample 2:
+ time = 743038
+ flags = 1
+ data = length 423, hash D4AE6773
+ sample 3:
+ time = 766258
+ flags = 1
+ data = length 497, hash A3C674D3
+ sample 4:
+ time = 789478
+ flags = 1
+ data = length 419, hash D3734A1F
+ sample 5:
+ time = 812698
+ flags = 1
+ data = length 474, hash DFB41F9
+ sample 6:
+ time = 835918
+ flags = 1
+ data = length 413, hash 53E7CB9F
+ sample 7:
+ time = 859138
+ flags = 1
+ data = length 445, hash D15B0E39
+ sample 8:
+ time = 882358
+ flags = 1
+ data = length 453, hash 77ED81E4
+ sample 9:
+ time = 905578
+ flags = 1
+ data = length 545, hash 3321AEB9
+ sample 10:
+ time = 928798
+ flags = 1
+ data = length 317, hash F557D0E
+ sample 11:
+ time = 952018
+ flags = 1
+ data = length 537, hash ED58CF7B
+ sample 12:
+ time = 975238
+ flags = 1
+ data = length 458, hash 51CDAA10
+ sample 13:
+ time = 998458
+ flags = 1
+ data = length 465, hash CBA1EFD7
+ sample 14:
+ time = 1021678
+ flags = 1
+ data = length 446, hash D6735B8A
+ sample 15:
+ time = 1044897
+ flags = 1
+ data = length 10, hash A453EEBE
+tracksEnded = true
diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump
new file mode 100644
index 0000000000..c049809940
--- /dev/null
+++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump
@@ -0,0 +1,159 @@
+seekMap:
+ isSeekable = true
+ duration = 1067733
+ getPosition(0) = [[timeUs=66733, position=1325]]
+ getPosition(1) = [[timeUs=66733, position=1325]]
+ getPosition(533866) = [[timeUs=66733, position=1325]]
+ getPosition(1067733) = [[timeUs=66733, position=1325]]
+numberOfTracks = 2
+track 0:
+ total output bytes = 85933
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ codecs = avc1.64001F
+ width = 1080
+ height = 720
+ initializationData:
+ data = length 29, hash 4746B5D9
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 38070, hash B58E1AEE
+ sample 1:
+ time = 200200
+ flags = 0
+ data = length 8340, hash 8AC449FF
+ sample 2:
+ time = 133466
+ flags = 0
+ data = length 1295, hash C0DA5090
+ sample 3:
+ time = 100100
+ flags = 0
+ data = length 469, hash D6E0A200
+ sample 4:
+ time = 166833
+ flags = 0
+ data = length 564, hash E5F56C5B
+ sample 5:
+ time = 333666
+ flags = 0
+ data = length 6075, hash 8756E49E
+ sample 6:
+ time = 266933
+ flags = 0
+ data = length 847, hash DCC2B618
+ sample 7:
+ time = 233566
+ flags = 0
+ data = length 455, hash B9CCE047
+ sample 8:
+ time = 300300
+ flags = 0
+ data = length 467, hash 69806D94
+ sample 9:
+ time = 467133
+ flags = 0
+ data = length 4549, hash 3944F501
+ sample 10:
+ time = 400400
+ flags = 0
+ data = length 1087, hash 491BF106
+ sample 11:
+ time = 367033
+ flags = 0
+ data = length 380, hash 5FED016A
+ sample 12:
+ time = 433766
+ flags = 0
+ data = length 455, hash 8A0610
+ sample 13:
+ time = 600600
+ flags = 0
+ data = length 5190, hash B9031D8
+ sample 14:
+ time = 533866
+ flags = 0
+ data = length 1071, hash 684E7DC8
+ sample 15:
+ time = 500500
+ flags = 0
+ data = length 653, hash 8494F326
+ sample 16:
+ time = 567233
+ flags = 0
+ data = length 485, hash 2CCC85F4
+ sample 17:
+ time = 734066
+ flags = 0
+ data = length 4884, hash D16B6A96
+ sample 18:
+ time = 667333
+ flags = 0
+ data = length 997, hash 164FF210
+ sample 19:
+ time = 633966
+ flags = 0
+ data = length 640, hash F664125B
+ sample 20:
+ time = 700700
+ flags = 0
+ data = length 491, hash B5930C7C
+ sample 21:
+ time = 867533
+ flags = 0
+ data = length 2989, hash 92CF4FCF
+ sample 22:
+ time = 800800
+ flags = 0
+ data = length 838, hash 294A3451
+ sample 23:
+ time = 767433
+ flags = 0
+ data = length 544, hash FCCE2DE6
+ sample 24:
+ time = 834166
+ flags = 0
+ data = length 329, hash A654FFA1
+ sample 25:
+ time = 1001000
+ flags = 0
+ data = length 1517, hash 5F7EBF8B
+ sample 26:
+ time = 934266
+ flags = 0
+ data = length 803, hash 7A5C4C1D
+ sample 27:
+ time = 900900
+ flags = 0
+ data = length 415, hash B31BBC3B
+ sample 28:
+ time = 967633
+ flags = 0
+ data = length 415, hash 850DFEA3
+ sample 29:
+ time = 1034366
+ flags = 0
+ data = length 619, hash AB5E56CA
+track 1:
+ total output bytes = 10
+ sample count = 1
+ format 0:
+ averageBitrate = 2147483647
+ peakBitrate = 2147483647
+ id = 2
+ sampleMimeType = audio/mp4a-latm
+ codecs = mp4a.40.2
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ initializationData:
+ data = length 5, hash 2B7623A
+ sample 0:
+ time = 1044897
+ flags = 1
+ data = length 10, hash A453EEBE
+tracksEnded = true
diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump
new file mode 100644
index 0000000000..5b9a721cb6
--- /dev/null
+++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump
@@ -0,0 +1,339 @@
+seekMap:
+ isSeekable = true
+ duration = 1067733
+ getPosition(0) = [[timeUs=66733, position=1325]]
+ getPosition(1) = [[timeUs=66733, position=1325]]
+ getPosition(533866) = [[timeUs=66733, position=1325]]
+ getPosition(1067733) = [[timeUs=66733, position=1325]]
+numberOfTracks = 2
+track 0:
+ total output bytes = 85933
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ codecs = avc1.64001F
+ width = 1080
+ height = 720
+ initializationData:
+ data = length 29, hash 4746B5D9
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 38070, hash B58E1AEE
+ sample 1:
+ time = 200200
+ flags = 0
+ data = length 8340, hash 8AC449FF
+ sample 2:
+ time = 133466
+ flags = 0
+ data = length 1295, hash C0DA5090
+ sample 3:
+ time = 100100
+ flags = 0
+ data = length 469, hash D6E0A200
+ sample 4:
+ time = 166833
+ flags = 0
+ data = length 564, hash E5F56C5B
+ sample 5:
+ time = 333666
+ flags = 0
+ data = length 6075, hash 8756E49E
+ sample 6:
+ time = 266933
+ flags = 0
+ data = length 847, hash DCC2B618
+ sample 7:
+ time = 233566
+ flags = 0
+ data = length 455, hash B9CCE047
+ sample 8:
+ time = 300300
+ flags = 0
+ data = length 467, hash 69806D94
+ sample 9:
+ time = 467133
+ flags = 0
+ data = length 4549, hash 3944F501
+ sample 10:
+ time = 400400
+ flags = 0
+ data = length 1087, hash 491BF106
+ sample 11:
+ time = 367033
+ flags = 0
+ data = length 380, hash 5FED016A
+ sample 12:
+ time = 433766
+ flags = 0
+ data = length 455, hash 8A0610
+ sample 13:
+ time = 600600
+ flags = 0
+ data = length 5190, hash B9031D8
+ sample 14:
+ time = 533866
+ flags = 0
+ data = length 1071, hash 684E7DC8
+ sample 15:
+ time = 500500
+ flags = 0
+ data = length 653, hash 8494F326
+ sample 16:
+ time = 567233
+ flags = 0
+ data = length 485, hash 2CCC85F4
+ sample 17:
+ time = 734066
+ flags = 0
+ data = length 4884, hash D16B6A96
+ sample 18:
+ time = 667333
+ flags = 0
+ data = length 997, hash 164FF210
+ sample 19:
+ time = 633966
+ flags = 0
+ data = length 640, hash F664125B
+ sample 20:
+ time = 700700
+ flags = 0
+ data = length 491, hash B5930C7C
+ sample 21:
+ time = 867533
+ flags = 0
+ data = length 2989, hash 92CF4FCF
+ sample 22:
+ time = 800800
+ flags = 0
+ data = length 838, hash 294A3451
+ sample 23:
+ time = 767433
+ flags = 0
+ data = length 544, hash FCCE2DE6
+ sample 24:
+ time = 834166
+ flags = 0
+ data = length 329, hash A654FFA1
+ sample 25:
+ time = 1001000
+ flags = 0
+ data = length 1517, hash 5F7EBF8B
+ sample 26:
+ time = 934266
+ flags = 0
+ data = length 803, hash 7A5C4C1D
+ sample 27:
+ time = 900900
+ flags = 0
+ data = length 415, hash B31BBC3B
+ sample 28:
+ time = 967633
+ flags = 0
+ data = length 415, hash 850DFEA3
+ sample 29:
+ time = 1034366
+ flags = 0
+ data = length 619, hash AB5E56CA
+track 1:
+ total output bytes = 18257
+ sample count = 46
+ format 0:
+ averageBitrate = 2147483647
+ peakBitrate = 2147483647
+ id = 2
+ sampleMimeType = audio/mp4a-latm
+ codecs = mp4a.40.2
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ initializationData:
+ data = length 5, hash 2B7623A
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 18, hash 96519432
+ sample 1:
+ time = 23219
+ flags = 1
+ data = length 4, hash EE9DF
+ sample 2:
+ time = 46439
+ flags = 1
+ data = length 4, hash EEDBF
+ sample 3:
+ time = 69659
+ flags = 1
+ data = length 157, hash E2F078F4
+ sample 4:
+ time = 92879
+ flags = 1
+ data = length 371, hash B9471F94
+ sample 5:
+ time = 116099
+ flags = 1
+ data = length 373, hash 2AB265CB
+ sample 6:
+ time = 139319
+ flags = 1
+ data = length 402, hash 1295477C
+ sample 7:
+ time = 162539
+ flags = 1
+ data = length 455, hash 2D8146C8
+ sample 8:
+ time = 185759
+ flags = 1
+ data = length 434, hash F2C5D287
+ sample 9:
+ time = 208979
+ flags = 1
+ data = length 450, hash 84143FCD
+ sample 10:
+ time = 232199
+ flags = 1
+ data = length 429, hash EF769D50
+ sample 11:
+ time = 255419
+ flags = 1
+ data = length 450, hash EC3DE692
+ sample 12:
+ time = 278639
+ flags = 1
+ data = length 447, hash 3E519E13
+ sample 13:
+ time = 301859
+ flags = 1
+ data = length 457, hash 1E4F23A0
+ sample 14:
+ time = 325079
+ flags = 1
+ data = length 447, hash A439EA97
+ sample 15:
+ time = 348299
+ flags = 1
+ data = length 456, hash 1E9034C6
+ sample 16:
+ time = 371519
+ flags = 1
+ data = length 398, hash 99DB7345
+ sample 17:
+ time = 394739
+ flags = 1
+ data = length 474, hash 3F05F10A
+ sample 18:
+ time = 417959
+ flags = 1
+ data = length 416, hash C105EE09
+ sample 19:
+ time = 441179
+ flags = 1
+ data = length 454, hash 5FDBE458
+ sample 20:
+ time = 464399
+ flags = 1
+ data = length 438, hash 41A93AC3
+ sample 21:
+ time = 487619
+ flags = 1
+ data = length 443, hash 10FDA652
+ sample 22:
+ time = 510839
+ flags = 1
+ data = length 412, hash 1F791E25
+ sample 23:
+ time = 534058
+ flags = 1
+ data = length 482, hash A6D983D
+ sample 24:
+ time = 557278
+ flags = 1
+ data = length 386, hash BED7392F
+ sample 25:
+ time = 580498
+ flags = 1
+ data = length 463, hash 5309F8C9
+ sample 26:
+ time = 603718
+ flags = 1
+ data = length 394, hash 21C7321F
+ sample 27:
+ time = 626938
+ flags = 1
+ data = length 489, hash 71B4730D
+ sample 28:
+ time = 650158
+ flags = 1
+ data = length 403, hash D9C6DE89
+ sample 29:
+ time = 673378
+ flags = 1
+ data = length 447, hash 9B14B73B
+ sample 30:
+ time = 696598
+ flags = 1
+ data = length 439, hash 4760D35B
+ sample 31:
+ time = 719818
+ flags = 1
+ data = length 463, hash 1601F88D
+ sample 32:
+ time = 743038
+ flags = 1
+ data = length 423, hash D4AE6773
+ sample 33:
+ time = 766258
+ flags = 1
+ data = length 497, hash A3C674D3
+ sample 34:
+ time = 789478
+ flags = 1
+ data = length 419, hash D3734A1F
+ sample 35:
+ time = 812698
+ flags = 1
+ data = length 474, hash DFB41F9
+ sample 36:
+ time = 835918
+ flags = 1
+ data = length 413, hash 53E7CB9F
+ sample 37:
+ time = 859138
+ flags = 1
+ data = length 445, hash D15B0E39
+ sample 38:
+ time = 882358
+ flags = 1
+ data = length 453, hash 77ED81E4
+ sample 39:
+ time = 905578
+ flags = 1
+ data = length 545, hash 3321AEB9
+ sample 40:
+ time = 928798
+ flags = 1
+ data = length 317, hash F557D0E
+ sample 41:
+ time = 952018
+ flags = 1
+ data = length 537, hash ED58CF7B
+ sample 42:
+ time = 975238
+ flags = 1
+ data = length 458, hash 51CDAA10
+ sample 43:
+ time = 998458
+ flags = 1
+ data = length 465, hash CBA1EFD7
+ sample 44:
+ time = 1021678
+ flags = 1
+ data = length 446, hash D6735B8A
+ sample 45:
+ time = 1044897
+ flags = 1
+ data = length 10, hash A453EEBE
+tracksEnded = true
diff --git a/libraries/test_data/src/test/assets/media/mp4/sample_fragmented_large_bitrates.mp4 b/libraries/test_data/src/test/assets/media/mp4/sample_fragmented_large_bitrates.mp4
new file mode 100644
index 0000000000..39fd4c18cf
Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mp4/sample_fragmented_large_bitrates.mp4 differ
diff --git a/libraries/test_data/src/test/assets/media/mpd/sample_mpd_clear_key_license_url b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_clear_key_license_url
new file mode 100644
index 0000000000..ed362b729a
--- /dev/null
+++ b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_clear_key_license_url
@@ -0,0 +1,30 @@
+
+
+ If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
- *
- * @param player The {@link Player}.
- * @return The new offloadSchedulingEnabled state.
- * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
- * exceeded.
- */
- public static boolean runUntilReceiveOffloadSchedulingEnabledNewState(ExoPlayer player)
- throws TimeoutException {
- verifyMainTestThread(player);
- AtomicReference<@NullableType Boolean> offloadSchedulingEnabledReceiver =
- new AtomicReference<>();
- ExoPlayer.AudioOffloadListener listener =
- new ExoPlayer.AudioOffloadListener() {
- @Override
- public void onExperimentalOffloadSchedulingEnabledChanged(
- boolean offloadSchedulingEnabled) {
- offloadSchedulingEnabledReceiver.set(offloadSchedulingEnabled);
- }
- };
- player.addAudioOffloadListener(listener);
- runMainLooperUntil(
- () -> offloadSchedulingEnabledReceiver.get() != null || player.getPlayerError() != null);
- player.removeAudioOffloadListener(listener);
- if (player.getPlayerError() != null) {
- throw new IllegalStateException(player.getPlayerError());
- }
- return checkNotNull(offloadSchedulingEnabledReceiver.get());
- }
-
/**
* Runs tasks of the main {@link Looper} until {@link
* ExoPlayer.AudioOffloadListener#onExperimentalSleepingForOffloadChanged(boolean)} is called or a
diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java
index 7ab349bd1d..8ec6d27498 100644
--- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java
+++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java
@@ -1811,7 +1811,13 @@ public class PlayerControlView extends FrameLayout {
if (position < playbackSpeedTexts.length) {
holder.textView.setText(playbackSpeedTexts[position]);
}
- holder.checkView.setVisibility(position == selectedIndex ? VISIBLE : INVISIBLE);
+ if (position == selectedIndex) {
+ holder.itemView.setSelected(true);
+ holder.checkView.setVisibility(VISIBLE);
+ } else {
+ holder.itemView.setSelected(false);
+ holder.checkView.setVisibility(INVISIBLE);
+ }
holder.itemView.setOnClickListener(
v -> {
if (position != selectedIndex) {
diff --git a/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java b/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java
index 77d25ce9dc..84a8c9eb75 100644
--- a/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java
+++ b/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java
@@ -236,11 +236,6 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
// Player.Listener implementation.
- @Override
- public void onPlaybackStateChanged(@Player.State int playbackState) {
- notifyStateChanged();
- }
-
@Override
public void onPlayerError(PlaybackException error) {
Callback callback = getCallback();
@@ -285,5 +280,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
int scaledWidth = Math.round(videoSize.width * videoSize.pixelWidthHeightRatio);
getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, scaledWidth, videoSize.height);
}
+
+ @Override
+ public void onEvents(Player player, Player.Events events) {
+ if (events.containsAny(
+ Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED)) {
+ notifyStateChanged();
+ }
+ }
}
}
Custom commands
+ *
+ * Custom actions are sent to the session under the hood. You can receive them by overriding the
+ * session callback method {@link MediaSession.Callback#onCustomCommand(MediaSession,
+ * MediaSession.ControllerInfo, SessionCommand, Bundle)}. This is useful because starting with
+ * Android 13, the System UI notification sends commands directly to the session. So handling the
+ * custom commands on the session level allows you to handle them at the same callback for all API
+ * levels.
+ *
* Drawables
*
* The drawables used can be overridden by drawables with the same names defined the application.
@@ -219,6 +228,14 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
* customized by defining the index of the command in compact view of up to 3 commands in their
* extras with key {@link DefaultMediaNotificationProvider#COMMAND_KEY_COMPACT_VIEW_INDEX}.
*
+ *
+
+ *
*
* @param actualTimelines A list of actual {@link Timeline timelines}.
* @param expectedTimelines A list of expected {@link Timeline timelines}.
*/
public static void assertTimelinesSame(
List