Update ExoPlayer version to ToT am: 5196d1b109 am: a526f72309

Change-Id: I193d24c7fa83448a1d54662e862f839fc89f65a3
diff --git a/METADATA b/METADATA
index 998c606..62bb7a0 100644
--- a/METADATA
+++ b/METADATA
@@ -16,7 +16,7 @@
     type: GIT
     value: "https://github.com/google/ExoPlayer.git"
   }
-  version: "d33c5ac0b3248a94211d844696652742854f96c0"
-  last_upgrade_date { year: 2020 month: 4 day: 9 }
+  version: "abadc768725929df0f4eb1ef0aacf53893e45d6d"
+  last_upgrade_date { year: 2020 month: 4 day: 19 }
   license_type: NOTICE
 }
\ No newline at end of file
diff --git a/tree/RELEASENOTES.md b/tree/RELEASENOTES.md
index 4c2284d..9daedd6 100644
--- a/tree/RELEASENOTES.md
+++ b/tree/RELEASENOTES.md
@@ -65,6 +65,11 @@
     *   Remove deprecated members in `DefaultTrackSelector`.
     *   Add `Player.DeviceComponent` and implement it for `SimpleExoPlayer` so
         that the device volume can be controlled by player.
+    *   Avoid throwing an exception while parsing fragmented MP4 default sample
+        values where the most-significant bit is set
+        ([#7207](https://github.com/google/ExoPlayer/issues/7207)).
+    *   Add `SilenceMediaSource.Factory` to support tags
+        ([PR #7245](https://github.com/google/ExoPlayer/pull/7245)).
 *   Text:
     *   Parse `<ruby>` and `<rt>` tags in WebVTT subtitles (rendering is coming
         later).
diff --git a/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
index be092e8..2b79071 100644
--- a/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
+++ b/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java
@@ -24,6 +24,7 @@
 import androidx.fragment.app.FragmentManager;
 import com.google.android.exoplayer2.C;
 import com.google.android.exoplayer2.RenderersFactory;
+import com.google.android.exoplayer2.demo.Sample.UriSample;
 import com.google.android.exoplayer2.offline.Download;
 import com.google.android.exoplayer2.offline.DownloadCursor;
 import com.google.android.exoplayer2.offline.DownloadHelper;
@@ -92,12 +93,8 @@
   }
 
   public void toggleDownload(
-      FragmentManager fragmentManager,
-      String name,
-      Uri uri,
-      String extension,
-      RenderersFactory renderersFactory) {
-    Download download = downloads.get(uri);
+      FragmentManager fragmentManager, UriSample sample, RenderersFactory renderersFactory) {
+    Download download = downloads.get(sample.uri);
     if (download != null) {
       DownloadService.sendRemoveDownload(
           context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
@@ -107,7 +104,9 @@
       }
       startDownloadDialogHelper =
           new StartDownloadDialogHelper(
-              fragmentManager, getDownloadHelper(uri, extension, renderersFactory), name);
+              fragmentManager,
+              getDownloadHelper(sample.uri, sample.extension, renderersFactory),
+              sample);
     }
   }
 
@@ -167,16 +166,16 @@
 
     private final FragmentManager fragmentManager;
     private final DownloadHelper downloadHelper;
-    private final String name;
+    private final UriSample sample;
 
     private TrackSelectionDialog trackSelectionDialog;
     private MappedTrackInfo mappedTrackInfo;
 
     public StartDownloadDialogHelper(
-        FragmentManager fragmentManager, DownloadHelper downloadHelper, String name) {
+        FragmentManager fragmentManager, DownloadHelper downloadHelper, UriSample sample) {
       this.fragmentManager = fragmentManager;
       this.downloadHelper = downloadHelper;
-      this.name = name;
+      this.sample = sample;
       downloadHelper.prepare(this);
     }
 
@@ -271,7 +270,7 @@
     }
 
     private DownloadRequest buildDownloadRequest() {
-      return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(name));
+      return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(sample.name));
     }
   }
 }
diff --git a/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
index 44d471b..f941234 100644
--- a/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
+++ b/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java
@@ -42,7 +42,6 @@
 import com.google.android.exoplayer2.demo.Sample.UriSample;
 import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
 import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
-import com.google.android.exoplayer2.offline.DownloadHelper;
 import com.google.android.exoplayer2.offline.DownloadRequest;
 import com.google.android.exoplayer2.source.BehindLiveWindowException;
 import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
@@ -473,7 +472,12 @@
             .getDownloadTracker()
             .getDownloadRequest(mediaItem.playbackProperties.sourceUri);
     if (downloadRequest != null) {
-      return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
+      mediaItem =
+          mediaItem
+              .buildUpon()
+              .setStreamKeys(downloadRequest.streamKeys)
+              .setCustomCacheKey(downloadRequest.customCacheKey)
+              .build();
     }
     return mediaSourceFactory
         .setDrmHttpDataSourceFactory(drmDataSourceFactory)
diff --git a/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
index ebfaa76..740f016 100644
--- a/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
+++ b/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
@@ -249,16 +249,11 @@
       Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
           .show();
     } else {
-      UriSample uriSample = (UriSample) sample;
       RenderersFactory renderersFactory =
           ((DemoApplication) getApplication())
               .buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
       downloadTracker.toggleDownload(
-          getSupportFragmentManager(),
-          sample.name,
-          uriSample.uri,
-          uriSample.extension,
-          renderersFactory);
+          getSupportFragmentManager(), (UriSample) sample, renderersFactory);
     }
   }
 
diff --git a/tree/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java b/tree/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
index 7d549be..2c25c32 100644
--- a/tree/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
+++ b/tree/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetEngineWrapper.java
@@ -166,7 +166,8 @@
     private final boolean preferGMSCoreCronet;
 
     // Multi-catch can only be used for API 19+ in this case.
-    @SuppressWarnings("UseMultiCatch")
+    // incompatible types in argument.
+    @SuppressWarnings({"UseMultiCatch", "nullness:argument.type.incompatible"})
     public CronetProviderComparator(boolean preferGMSCoreCronet) {
       // GMSCore CronetProvider classes are only available in some configurations.
       // Thus, we use reflection to copy static name.
diff --git a/tree/library/common/src/main/java/com/google/android/exoplayer2/C.java b/tree/library/common/src/main/java/com/google/android/exoplayer2/C.java
index 780d825..9f4e8be 100644
--- a/tree/library/common/src/main/java/com/google/android/exoplayer2/C.java
+++ b/tree/library/common/src/main/java/com/google/android/exoplayer2/C.java
@@ -998,7 +998,8 @@
    * #ROLE_FLAG_DUB}, {@link #ROLE_FLAG_EMERGENCY}, {@link #ROLE_FLAG_CAPTION}, {@link
    * #ROLE_FLAG_SUBTITLE}, {@link #ROLE_FLAG_SIGN}, {@link #ROLE_FLAG_DESCRIBES_VIDEO}, {@link
    * #ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND}, {@link #ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY},
-   * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG} and {@link #ROLE_FLAG_EASY_TO_READ}.
+   * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG}, {@link #ROLE_FLAG_EASY_TO_READ} and {@link
+   * #ROLE_FLAG_TRICK_PLAY}.
    */
   @Documented
   @Retention(RetentionPolicy.SOURCE)
@@ -1018,7 +1019,8 @@
         ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND,
         ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY,
         ROLE_FLAG_TRANSCRIBES_DIALOG,
-        ROLE_FLAG_EASY_TO_READ
+        ROLE_FLAG_EASY_TO_READ,
+        ROLE_FLAG_TRICK_PLAY
       })
   public @interface RoleFlags {}
   /** Indicates a main track. */
@@ -1064,6 +1066,8 @@
   public static final int ROLE_FLAG_TRANSCRIBES_DIALOG = 1 << 12;
   /** Indicates the track contains a text that has been edited for ease of reading. */
   public static final int ROLE_FLAG_EASY_TO_READ = 1 << 13;
+  /** Indicates the track is intended for trick play. */
+  public static final int ROLE_FLAG_TRICK_PLAY = 1 << 14;
 
   /**
    * Converts a time in microseconds to the corresponding time in milliseconds, preserving
diff --git a/tree/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java b/tree/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java
index a78a2d0..f484d3f 100644
--- a/tree/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java
+++ b/tree/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java
@@ -69,17 +69,47 @@
     private boolean drmPlayClearContentWithoutKey;
     private List<Integer> drmSessionForClearTypes;
     private List<StreamKey> streamKeys;
+    @Nullable private String customCacheKey;
     private List<Subtitle> subtitles;
     @Nullable private Object tag;
     @Nullable private MediaMetadata mediaMetadata;
 
     /** Creates a builder. */
     public Builder() {
-      streamKeys = Collections.emptyList();
-      subtitles = Collections.emptyList();
+      clipEndPositionMs = C.TIME_END_OF_SOURCE;
       drmSessionForClearTypes = Collections.emptyList();
       drmLicenseRequestHeaders = Collections.emptyMap();
-      clipEndPositionMs = C.TIME_END_OF_SOURCE;
+      streamKeys = Collections.emptyList();
+      subtitles = Collections.emptyList();
+    }
+
+    private Builder(MediaItem mediaItem) {
+      this();
+      clipEndPositionMs = mediaItem.clippingProperties.endPositionMs;
+      clipRelativeToLiveWindow = mediaItem.clippingProperties.relativeToLiveWindow;
+      clipRelativeToDefaultPosition = mediaItem.clippingProperties.relativeToDefaultPosition;
+      clipStartsAtKeyFrame = mediaItem.clippingProperties.startsAtKeyFrame;
+      clipStartPositionMs = mediaItem.clippingProperties.startPositionMs;
+      mediaId = mediaItem.mediaId;
+      mediaMetadata = mediaItem.mediaMetadata;
+      @Nullable PlaybackProperties playbackProperties = mediaItem.playbackProperties;
+      if (playbackProperties != null) {
+        customCacheKey = playbackProperties.customCacheKey;
+        mimeType = playbackProperties.mimeType;
+        sourceUri = playbackProperties.sourceUri;
+        streamKeys = playbackProperties.streamKeys;
+        subtitles = playbackProperties.subtitles;
+        tag = playbackProperties.tag;
+        @Nullable DrmConfiguration drmConfiguration = playbackProperties.drmConfiguration;
+        if (drmConfiguration != null) {
+          drmLicenseUri = drmConfiguration.licenseUri;
+          drmLicenseRequestHeaders = drmConfiguration.requestHeaders;
+          drmMultiSession = drmConfiguration.multiSession;
+          drmPlayClearContentWithoutKey = drmConfiguration.playClearContentWithoutKey;
+          drmSessionForClearTypes = drmConfiguration.sessionForClearTypes;
+          drmUuid = drmConfiguration.uuid;
+        }
+      }
     }
 
     /**
@@ -297,6 +327,17 @@
     }
 
     /**
+     * Sets the optional custom cache key (only used for progressive streams).
+     *
+     * <p>If a {@link PlaybackProperties#sourceUri} is set, the custom cache key is used to create a
+     * {@link PlaybackProperties} object. Otherwise it will be ignored.
+     */
+    public Builder setCustomCacheKey(@Nullable String customCacheKey) {
+      this.customCacheKey = customCacheKey;
+      return this;
+    }
+
+    /**
      * Sets the optional subtitles.
      *
      * <p>{@code null} or an empty {@link List} can be used for a reset.
@@ -352,6 +393,7 @@
                         drmSessionForClearTypes)
                     : null,
                 streamKeys,
+                customCacheKey,
                 subtitles,
                 tag);
         mediaId = mediaId != null ? mediaId : sourceUri.toString();
@@ -461,6 +503,9 @@
     /** Optional stream keys by which the manifest is filtered. */
     public final List<StreamKey> streamKeys;
 
+    /** Optional custom cache key (only used for progressive streams). */
+    @Nullable public final String customCacheKey;
+
     /** Optional subtitles to be sideloaded. */
     public final List<Subtitle> subtitles;
 
@@ -476,12 +521,14 @@
         @Nullable String mimeType,
         @Nullable DrmConfiguration drmConfiguration,
         List<StreamKey> streamKeys,
+        @Nullable String customCacheKey,
         List<Subtitle> subtitles,
         @Nullable Object tag) {
       this.sourceUri = sourceUri;
       this.mimeType = mimeType;
       this.drmConfiguration = drmConfiguration;
       this.streamKeys = streamKeys;
+      this.customCacheKey = customCacheKey;
       this.subtitles = subtitles;
       this.tag = tag;
     }
@@ -500,6 +547,7 @@
           && Util.areEqual(mimeType, other.mimeType)
           && Util.areEqual(drmConfiguration, other.drmConfiguration)
           && streamKeys.equals(other.streamKeys)
+          && Util.areEqual(customCacheKey, other.customCacheKey)
           && subtitles.equals(other.subtitles)
           && Util.areEqual(tag, other.tag);
     }
@@ -510,6 +558,7 @@
       result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
       result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode());
       result = 31 * result + streamKeys.hashCode();
+      result = 31 * result + (customCacheKey == null ? 0 : customCacheKey.hashCode());
       result = 31 * result + subtitles.hashCode();
       result = 31 * result + (tag == null ? 0 : tag.hashCode());
       return result;
@@ -674,6 +723,11 @@
     this.clippingProperties = clippingProperties;
   }
 
+  /** Returns a {@link Builder} initialized with the values of this instance. */
+  public Builder buildUpon() {
+    return new Builder(this);
+  }
+
   @Override
   public boolean equals(@Nullable Object obj) {
     if (this == obj) {
diff --git a/tree/library/common/src/main/java/com/google/android/exoplayer2/util/UnknownNull.java b/tree/library/common/src/main/java/com/google/android/exoplayer2/util/UnknownNull.java
index 8ca2e8f..0ccad43 100644
--- a/tree/library/common/src/main/java/com/google/android/exoplayer2/util/UnknownNull.java
+++ b/tree/library/common/src/main/java/com/google/android/exoplayer2/util/UnknownNull.java
@@ -23,7 +23,7 @@
 import javax.annotation.meta.When;
 
 /**
- * Annotation for specifiying unknown nullness. Useful for clearing the effects of an automatically
+ * Annotation for specifying unknown nullness. Useful for clearing the effects of an automatically
  * propagated {@link Nonnull} annotation.
  */
 @Nonnull(when = When.UNKNOWN)
diff --git a/tree/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java b/tree/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java
index bb47011..adfbc60 100644
--- a/tree/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java
+++ b/tree/library/common/src/test/java/com/google/android/exoplayer2/MediaItemTest.java
@@ -142,6 +142,14 @@
   }
 
   @Test
+  public void builderSetCustomCacheKey_setsCustomCacheKey() {
+    MediaItem mediaItem =
+        new MediaItem.Builder().setSourceUri(URI_STRING).setCustomCacheKey("key").build();
+
+    assertThat(mediaItem.playbackProperties.customCacheKey).isEqualTo("key");
+  }
+
+  @Test
   public void builderSetStreamKeys_setsStreamKeys() {
     List<StreamKey> streamKeys = new ArrayList<>();
     streamKeys.add(new StreamKey(1, 0, 0));
@@ -267,4 +275,40 @@
 
     assertThat(mediaItem.mediaMetadata).isEqualTo(mediaMetadata);
   }
+
+  @Test
+  public void buildUpon_equalsToOriginal() {
+    MediaItem mediaItem =
+        new MediaItem.Builder()
+            .setClipEndPositionMs(1000)
+            .setClipRelativeToDefaultPosition(true)
+            .setClipRelativeToLiveWindow(true)
+            .setClipStartPositionMs(100)
+            .setClipStartsAtKeyFrame(true)
+            .setCustomCacheKey("key")
+            .setDrmUuid(C.WIDEVINE_UUID)
+            .setDrmLicenseUri(URI_STRING + "/license")
+            .setDrmLicenseRequestHeaders(
+                Collections.singletonMap("Referer", "http://www.google.com"))
+            .setDrmMultiSession(true)
+            .setDrmPlayClearContentWithoutKey(true)
+            .setDrmSessionForClearTypes(Collections.singletonList(C.TRACK_TYPE_AUDIO))
+            .setMediaId("mediaId")
+            .setMediaMetadata(new MediaMetadata.Builder().setTitle("title").build())
+            .setMimeType(MimeTypes.APPLICATION_MP4)
+            .setSourceUri(URI_STRING)
+            .setStreamKeys(Collections.singletonList(new StreamKey(1, 0, 0)))
+            .setSubtitles(
+                Collections.singletonList(
+                    new MediaItem.Subtitle(
+                        Uri.parse(URI_STRING + "/en"),
+                        MimeTypes.APPLICATION_TTML,
+                        /* language= */ "en")))
+            .setTag(new Object())
+            .build();
+
+    MediaItem copy = mediaItem.buildUpon().build();
+
+    assertThat(copy).isEqualTo(mediaItem);
+  }
 }
diff --git a/tree/library/core/src/androidTest/java/com/google/android/exoplayer2/StreamVolumeManagerTest.java b/tree/library/core/src/androidTest/java/com/google/android/exoplayer2/StreamVolumeManagerTest.java
index 220f331..792492d 100644
--- a/tree/library/core/src/androidTest/java/com/google/android/exoplayer2/StreamVolumeManagerTest.java
+++ b/tree/library/core/src/androidTest/java/com/google/android/exoplayer2/StreamVolumeManagerTest.java
@@ -198,6 +198,30 @@
   }
 
   @Test
+  public void setVolumeMuted_changesMuteState() {
+    testThread.runOnMainThread(
+        () -> {
+          int minVolume = streamVolumeManager.getMinVolume();
+          int maxVolume = streamVolumeManager.getMaxVolume();
+          if (minVolume == maxVolume || minVolume > 0) {
+            return;
+          }
+
+          streamVolumeManager.setVolume(maxVolume);
+          assertThat(streamVolumeManager.isMuted()).isFalse();
+
+          streamVolumeManager.setMuted(true);
+          assertThat(streamVolumeManager.isMuted()).isTrue();
+          assertThat(testListener.lastStreamVolumeMuted).isTrue();
+
+          streamVolumeManager.setMuted(false);
+          assertThat(streamVolumeManager.isMuted()).isFalse();
+          assertThat(testListener.lastStreamVolumeMuted).isFalse();
+          assertThat(testListener.lastStreamVolume).isEqualTo(maxVolume);
+        });
+  }
+
+  @Test
   public void setStreamType_notifiesStreamTypeAndVolume() {
     testThread.runOnMainThread(
         () -> {
@@ -250,6 +274,7 @@
 
     @C.StreamType private int lastStreamType;
     private int lastStreamVolume;
+    private boolean lastStreamVolumeMuted;
     public final CountDownLatch onStreamVolumeChangedLatch;
 
     public TestListener() {
@@ -262,8 +287,9 @@
     }
 
     @Override
-    public void onStreamVolumeChanged(int streamVolume) {
+    public void onStreamVolumeChanged(int streamVolume, boolean streamMuted) {
       lastStreamVolume = streamVolume;
+      lastStreamVolumeMuted = streamMuted;
       onStreamVolumeChangedLatch.countDown();
     }
   }
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index 2242ebf..0344b09 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -397,6 +397,9 @@
   @Override
   public void addMediaSources(int index, List<MediaSource> mediaSources) {
     Assertions.checkArgument(index >= 0);
+    for (int i = 0; i < mediaSources.size(); i++) {
+      Assertions.checkArgument(mediaSources.get(i) != null);
+    }
     int currentWindowIndex = getCurrentWindowIndex();
     long currentPositionMs = getCurrentPosition();
     Timeline oldTimeline = getCurrentTimeline();
@@ -966,10 +969,13 @@
   }
 
   private void setMediaSourcesInternal(
-      List<MediaSource> mediaItems,
+      List<MediaSource> mediaSources,
       int startWindowIndex,
       long startPositionMs,
       boolean resetToDefaultPosition) {
+    for (int i = 0; i < mediaSources.size(); i++) {
+      Assertions.checkArgument(mediaSources.get(i) != null);
+    }
     int currentWindowIndex = getCurrentWindowIndexInternal();
     long currentPositionMs = getCurrentPosition();
     pendingOperationAcks++;
@@ -978,7 +984,7 @@
           /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size());
     }
     List<MediaSourceList.MediaSourceHolder> holders =
-        addMediaSourceHolders(/* index= */ 0, mediaItems);
+        addMediaSourceHolders(/* index= */ 0, mediaSources);
     PlaybackInfo playbackInfo = maskTimeline();
     Timeline timeline = playbackInfo.timeline;
     if (!timeline.isEmpty() && startWindowIndex >= timeline.getWindowCount()) {
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/Player.java
index 518e331..f692629 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/Player.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/Player.java
@@ -397,6 +397,9 @@
      */
     int getDeviceVolume();
 
+    /** Gets whether the device is muted or not. */
+    boolean isDeviceMuted();
+
     /**
      * Sets the volume of the device.
      *
@@ -409,6 +412,9 @@
 
     /** Decreases the volume of the device. */
     void decreaseDeviceVolume();
+
+    /** Sets the mute state of the device. */
+    void setDeviceMuted(boolean muted);
   }
 
   /**
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
index 5d98e59..67c4b88 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -1776,6 +1776,12 @@
   }
 
   @Override
+  public boolean isDeviceMuted() {
+    verifyApplicationThread();
+    return streamVolumeManager.isMuted();
+  }
+
+  @Override
   public void setDeviceVolume(int volume) {
     verifyApplicationThread();
     streamVolumeManager.setVolume(volume);
@@ -1793,6 +1799,12 @@
     streamVolumeManager.decreaseVolume();
   }
 
+  @Override
+  public void setDeviceMuted(boolean muted) {
+    verifyApplicationThread();
+    streamVolumeManager.setMuted(muted);
+  }
+
   // Internal methods.
 
   private void removeSurfaceCallbacks() {
@@ -2217,9 +2229,9 @@
     }
 
     @Override
-    public void onStreamVolumeChanged(int streamVolume) {
+    public void onStreamVolumeChanged(int streamVolume, boolean streamMuted) {
       for (DeviceListener deviceListener : deviceListeners) {
-        deviceListener.onDeviceVolumeChanged(streamVolume);
+        deviceListener.onDeviceVolumeChanged(streamVolume, streamMuted);
       }
     }
 
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java
index 28f439d..59ab3f1 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java
@@ -33,8 +33,8 @@
     /** Called when the audio stream type is changed. */
     void onStreamTypeChanged(@C.StreamType int streamType);
 
-    /** Called when the audio stream volume is changed. */
-    void onStreamVolumeChanged(int streamVolume);
+    /** Called when the audio stream volume or mute state is changed. */
+    void onStreamVolumeChanged(int streamVolume, boolean streamMuted);
   }
 
   // TODO(b/151280453): Replace the hidden intent action with an official one.
@@ -52,6 +52,7 @@
 
   @C.StreamType private int streamType;
   private int volume;
+  private boolean muted;
 
   /** Creates a manager. */
   public StreamVolumeManager(Context context, Handler eventHandler, Listener listener) {
@@ -63,7 +64,8 @@
             (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE));
 
     streamType = C.STREAM_TYPE_DEFAULT;
-    volume = audioManager.getStreamVolume(streamType);
+    volume = getVolumeFromManager(audioManager, streamType);
+    muted = getMutedFromManager(audioManager, streamType);
 
     receiver = new VolumeChangeReceiver();
     IntentFilter filter = new IntentFilter(VOLUME_CHANGED_ACTION);
@@ -102,6 +104,11 @@
     return volume;
   }
 
+  /** Gets whether the current audio stream is muted or not. */
+  public boolean isMuted() {
+    return muted;
+  }
+
   /**
    * Sets the volume with the given value for the current audio stream. The value should be between
    * {@link #getMinVolume()} and {@link #getMaxVolume()}, otherwise it will be ignored.
@@ -138,16 +145,42 @@
     updateVolumeAndNotifyIfChanged();
   }
 
+  /** Sets the mute state of the current audio stream. */
+  public void setMuted(boolean muted) {
+    if (Util.SDK_INT >= 23) {
+      audioManager.adjustStreamVolume(
+          streamType, muted ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE, VOLUME_FLAGS);
+    } else {
+      audioManager.setStreamMute(streamType, muted);
+    }
+    updateVolumeAndNotifyIfChanged();
+  }
+
   /** Releases the manager. It must be called when the manager is no longer required. */
   public void release() {
     applicationContext.unregisterReceiver(receiver);
   }
 
   private void updateVolumeAndNotifyIfChanged() {
-    int newVolume = audioManager.getStreamVolume(streamType);
-    if (volume != newVolume) {
+    int newVolume = getVolumeFromManager(audioManager, streamType);
+    boolean newMuted = getMutedFromManager(audioManager, streamType);
+    if (volume != newVolume || muted != newMuted) {
       volume = newVolume;
-      listener.onStreamVolumeChanged(newVolume);
+      muted = newMuted;
+      listener.onStreamVolumeChanged(newVolume, newMuted);
+    }
+  }
+
+  private static int getVolumeFromManager(AudioManager audioManager, @C.StreamType int streamType) {
+    return audioManager.getStreamVolume(streamType);
+  }
+
+  private static boolean getMutedFromManager(
+      AudioManager audioManager, @C.StreamType int streamType) {
+    if (Util.SDK_INT >= 23) {
+      return audioManager.isStreamMute(streamType);
+    } else {
+      return audioManager.getStreamVolume(streamType) == 0;
     }
   }
 
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java
index cede01e..e9baef3 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java
@@ -24,6 +24,7 @@
 import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
 import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
 import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Supplier;
 import com.google.android.exoplayer2.util.Util;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -34,23 +35,41 @@
  * Default {@link PlaybackSessionManager} which instantiates a new session for each window in the
  * timeline and also for each ad within the windows.
  *
- * <p>Sessions are identified by Base64-encoded, URL-safe, random strings.
+ * <p>By default, sessions are identified by Base64-encoded, URL-safe, random strings.
  */
 public final class DefaultPlaybackSessionManager implements PlaybackSessionManager {
 
+  /** Default generator for unique session ids that are random, Based64-encoded and URL-safe. */
+  public static final Supplier<String> DEFAULT_SESSION_ID_GENERATOR =
+      DefaultPlaybackSessionManager::generateDefaultSessionId;
+
   private static final Random RANDOM = new Random();
   private static final int SESSION_ID_LENGTH = 12;
 
   private final Timeline.Window window;
   private final Timeline.Period period;
   private final HashMap<String, SessionDescriptor> sessions;
+  private final Supplier<String> sessionIdGenerator;
 
   private @MonotonicNonNull Listener listener;
   private Timeline currentTimeline;
   @Nullable private String currentSessionId;
 
-  /** Creates session manager. */
+  /**
+   * Creates session manager with a {@link #DEFAULT_SESSION_ID_GENERATOR} to generate session ids.
+   */
   public DefaultPlaybackSessionManager() {
+    this(DEFAULT_SESSION_ID_GENERATOR);
+  }
+
+  /**
+   * Creates session manager.
+   *
+   * @param sessionIdGenerator A generator for new session ids. All generated session ids must be
+   *     unique.
+   */
+  public DefaultPlaybackSessionManager(Supplier<String> sessionIdGenerator) {
+    this.sessionIdGenerator = sessionIdGenerator;
     window = new Timeline.Window();
     period = new Timeline.Period();
     sessions = new HashMap<>();
@@ -207,14 +226,14 @@
       }
     }
     if (bestMatch == null) {
-      String sessionId = generateSessionId();
+      String sessionId = sessionIdGenerator.get();
       bestMatch = new SessionDescriptor(sessionId, windowIndex, mediaPeriodId);
       sessions.put(sessionId, bestMatch);
     }
     return bestMatch;
   }
 
-  private static String generateSessionId() {
+  private static String generateDefaultSessionId() {
     byte[] randomBytes = new byte[SESSION_ID_LENGTH];
     RANDOM.nextBytes(randomBytes);
     return Base64.encodeToString(randomBytes, Base64.URL_SAFE | Base64.NO_WRAP);
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java
index b6a063b..a9afa47 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java
@@ -80,6 +80,11 @@
   }
 
   @Override
+  protected void onFlush() {
+    flushSinkIfActive();
+  }
+
+  @Override
   protected void onQueueEndOfStream() {
     flushSinkIfActive();
   }
@@ -201,7 +206,7 @@
     }
 
     private void reset() throws IOException {
-      RandomAccessFile randomAccessFile = this.randomAccessFile;
+      @Nullable RandomAccessFile randomAccessFile = this.randomAccessFile;
       if (randomAccessFile == null) {
         return;
       }
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java
index f310b6d..3d35c6a 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/device/DeviceListener.java
@@ -23,6 +23,6 @@
   /** Called when the device information changes. */
   default void onDeviceInfoChanged(DeviceInfo deviceInfo) {}
 
-  /** Called when the device volume changes. */
-  default void onDeviceVolumeChanged(int volume) {}
+  /** Called when the device volume or mute state changes. */
+  default void onDeviceVolumeChanged(int volume, boolean muted) {}
 }
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
index 4094f4b..736f941 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
@@ -547,7 +547,9 @@
     width = alignedSize.x;
     height = alignedSize.y;
 
-    if (frameRate == Format.NO_VALUE || frameRate <= 0) {
+    // VideoCapabilities.areSizeAndRateSupported incorrectly returns false if frameRate < 1 on some
+    // versions of Android, so we only check the size in this case [Internal ref: b/153940404].
+    if (frameRate == Format.NO_VALUE || frameRate < 1) {
       return capabilities.isSizeSupported(width, height);
     } else {
       // The signaled frame rate may be slightly higher than the actual frame rate, so we take the
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
index c2e0c47..2bba84a 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
@@ -96,6 +96,8 @@
     return Assertions.checkNotNull(trackGroups);
   }
 
+  // unboxing a possibly-null reference streamPeriodIndices.get(streams[i])
+  @SuppressWarnings("nullness:unboxing.of.nullable")
   @Override
   public long selectTracks(
       @NullableType TrackSelection[] selections,
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java
index b2404db..ef9a965 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java
@@ -95,13 +95,10 @@
     }
 
     /**
-     * Sets the custom key that uniquely identifies the original stream. Used for cache indexing.
-     * The default value is {@code null}.
-     *
-     * @param customCacheKey A custom key that uniquely identifies the original stream. Used for
-     *     cache indexing.
-     * @return This factory, for convenience.
+     * @deprecated Use {@link MediaItem.Builder#setCustomCacheKey(String)} and {@link
+     *     #createMediaSource(MediaItem)} instead.
      */
+    @Deprecated
     public Factory setCustomCacheKey(@Nullable String customCacheKey) {
       this.customCacheKey = customCacheKey;
       return this;
@@ -188,7 +185,9 @@
           extractorsFactory,
           drmSessionManager,
           loadErrorHandlingPolicy,
-          customCacheKey,
+          mediaItem.playbackProperties.customCacheKey != null
+              ? mediaItem.playbackProperties.customCacheKey
+              : customCacheKey,
           continueLoadingCheckIntervalBytes,
           mediaItem.playbackProperties.tag != null ? mediaItem.playbackProperties.tag : tag);
     }
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java
index 839f683..f4fb376 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java
@@ -33,6 +33,42 @@
 /** Media source with a single period consisting of silent raw audio of a given duration. */
 public final class SilenceMediaSource extends BaseMediaSource {
 
+  /** Factory for {@link SilenceMediaSource SilenceMediaSources}. */
+  public static final class Factory {
+
+    private long durationUs;
+    @Nullable private Object tag;
+
+    /**
+     * Sets the duration of the silent audio.
+     *
+     * @param durationUs The duration of silent audio to output, in microseconds.
+     * @return This factory, for convenience.
+     */
+    public Factory setDurationUs(long durationUs) {
+      this.durationUs = durationUs;
+      return this;
+    }
+
+    /**
+     * Sets a tag for the media source which will be published in the {@link
+     * com.google.android.exoplayer2.Timeline} of the source as {@link
+     * com.google.android.exoplayer2.Timeline.Window#tag}.
+     *
+     * @param tag A tag for the media source.
+     * @return This factory, for convenience.
+     */
+    public Factory setTag(@Nullable Object tag) {
+      this.tag = tag;
+      return this;
+    }
+
+    /** Creates a new {@link SilenceMediaSource}. */
+    public SilenceMediaSource createMediaSource() {
+      return new SilenceMediaSource(durationUs, tag);
+    }
+  }
+
   private static final int SAMPLE_RATE_HZ = 44100;
   @C.PcmEncoding private static final int PCM_ENCODING = C.ENCODING_PCM_16BIT;
   private static final int CHANNEL_COUNT = 2;
@@ -47,6 +83,7 @@
       new byte[Util.getPcmFrameSize(PCM_ENCODING, CHANNEL_COUNT) * 1024];
 
   private final long durationUs;
+  @Nullable private final Object tag;
 
   /**
    * Creates a new media source providing silent audio of the given duration.
@@ -54,15 +91,25 @@
    * @param durationUs The duration of silent audio to output, in microseconds.
    */
   public SilenceMediaSource(long durationUs) {
+    this(durationUs, /* tag= */ null);
+  }
+
+  private SilenceMediaSource(long durationUs, @Nullable Object tag) {
     Assertions.checkArgument(durationUs >= 0);
     this.durationUs = durationUs;
+    this.tag = tag;
   }
 
   @Override
   protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
     refreshSourceInfo(
         new SinglePeriodTimeline(
-            durationUs, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false));
+            durationUs,
+            /* isSeekable= */ true,
+            /* isDynamic= */ false,
+            /* isLive= */ false,
+            /* manifest= */ null,
+            tag));
   }
 
   @Override
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
index 5596031..6682029 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
@@ -1888,6 +1888,10 @@
       int maxVideoHeight,
       int maxVideoFrameRate,
       int maxVideoBitrate) {
+    if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) {
+      // Ignore trick-play tracks for now.
+      return false;
+    }
     return isSupported(formatSupport, false)
         && ((formatSupport & requiredAdaptiveSupport) != 0)
         && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType))
@@ -1911,9 +1915,13 @@
           params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange);
       @Capabilities int[] trackFormatSupport = formatSupports[groupIndex];
       for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+        Format format = trackGroup.getFormat(trackIndex);
+        if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) {
+          // Ignore trick-play tracks for now.
+          continue;
+        }
         if (isSupported(trackFormatSupport[trackIndex],
             params.exceedRendererCapabilitiesIfNecessary)) {
-          Format format = trackGroup.getFormat(trackIndex);
           boolean isWithinConstraints =
               selectedTrackIndices.contains(trackIndex)
                   && (format.width == Format.NO_VALUE || format.width <= params.maxVideoWidth)
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java
index 7a87d7d..ffb8236 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java
@@ -30,6 +30,13 @@
    */
   Clock DEFAULT = new SystemClock();
 
+  /**
+   * Returns the current time in milliseconds since the Unix Epoch.
+   *
+   * @see System#currentTimeMillis()
+   */
+  long currentTimeMillis();
+
   /** @see android.os.SystemClock#elapsedRealtime() */
   long elapsedRealtime();
 
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java
index be52659..a094e81 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java
@@ -26,6 +26,11 @@
 /* package */ final class SystemClock implements Clock {
 
   @Override
+  public long currentTimeMillis() {
+    return System.currentTimeMillis();
+  }
+
+  @Override
   public long elapsedRealtime() {
     return android.os.SystemClock.elapsedRealtime();
   }
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/audio/TeeAudioProcessorTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/audio/TeeAudioProcessorTest.java
new file mode 100644
index 0000000..6f0a87e
--- /dev/null
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/audio/TeeAudioProcessorTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2020 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.audio;
+
+import static org.mockito.Mockito.verify;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat;
+import com.google.android.exoplayer2.audio.TeeAudioProcessor.AudioBufferSink;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Unit tests for {@link TeeAudioProcessorTest}. */
+@RunWith(AndroidJUnit4.class)
+public final class TeeAudioProcessorTest {
+
+  private static final AudioFormat AUDIO_FORMAT =
+      new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_16BIT);
+
+  @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+  private TeeAudioProcessor teeAudioProcessor;
+
+  @Mock private AudioBufferSink mockAudioBufferSink;
+
+  @Before
+  public void setUp() {
+    teeAudioProcessor = new TeeAudioProcessor(mockAudioBufferSink);
+  }
+
+  @Test
+  public void initialFlush_flushesSink() throws Exception {
+    teeAudioProcessor.configure(AUDIO_FORMAT);
+    teeAudioProcessor.flush();
+
+    verify(mockAudioBufferSink)
+        .flush(AUDIO_FORMAT.sampleRate, AUDIO_FORMAT.channelCount, AUDIO_FORMAT.encoding);
+  }
+}
diff --git a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
index d962374..b0689ee 100644
--- a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
+++ b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
@@ -50,9 +50,10 @@
    */
   public final List<Descriptor> accessibilityDescriptors;
 
-  /**
-   * Supplemental properties in the adaptation set.
-   */
+  /** Essential properties in the adaptation set. */
+  public final List<Descriptor> essentialProperties;
+
+  /** Supplemental properties in the adaptation set. */
   public final List<Descriptor> supplementalProperties;
 
   /**
@@ -62,21 +63,21 @@
    *     {@code TRACK_TYPE_*} constants.
    * @param representations {@link Representation}s in the adaptation set.
    * @param accessibilityDescriptors Accessibility descriptors in the adaptation set.
+   * @param essentialProperties Essential properties in the adaptation set.
    * @param supplementalProperties Supplemental properties in the adaptation set.
    */
-  public AdaptationSet(int id, int type, List<Representation> representations,
-      List<Descriptor> accessibilityDescriptors, List<Descriptor> supplementalProperties) {
+  public AdaptationSet(
+      int id,
+      int type,
+      List<Representation> representations,
+      List<Descriptor> accessibilityDescriptors,
+      List<Descriptor> essentialProperties,
+      List<Descriptor> supplementalProperties) {
     this.id = id;
     this.type = type;
     this.representations = Collections.unmodifiableList(representations);
-    this.accessibilityDescriptors =
-        accessibilityDescriptors == null
-            ? Collections.emptyList()
-            : Collections.unmodifiableList(accessibilityDescriptors);
-    this.supplementalProperties =
-        supplementalProperties == null
-            ? Collections.emptyList()
-            : Collections.unmodifiableList(supplementalProperties);
+    this.accessibilityDescriptors = Collections.unmodifiableList(accessibilityDescriptors);
+    this.essentialProperties = Collections.unmodifiableList(essentialProperties);
+    this.supplementalProperties = Collections.unmodifiableList(supplementalProperties);
   }
-
 }
diff --git a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
index 2d8909f..c21af45 100644
--- a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
+++ b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
@@ -224,9 +224,14 @@
         key = keys.poll();
       } while (key.periodIndex == periodIndex && key.groupIndex == adaptationSetIndex);
 
-      copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type,
-          copyRepresentations, adaptationSet.accessibilityDescriptors,
-          adaptationSet.supplementalProperties));
+      copyAdaptationSets.add(
+          new AdaptationSet(
+              adaptationSet.id,
+              adaptationSet.type,
+              copyRepresentations,
+              adaptationSet.accessibilityDescriptors,
+              adaptationSet.essentialProperties,
+              adaptationSet.supplementalProperties));
     } while(key.periodIndex == periodIndex);
     // Add back the last key which doesn't belong to the period being processed
     keys.addFirst(key);
diff --git a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
index 1ceeb31..23f264e 100644
--- a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
+++ b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
@@ -289,6 +289,7 @@
     ArrayList<Descriptor> inbandEventStreams = new ArrayList<>();
     ArrayList<Descriptor> accessibilityDescriptors = new ArrayList<>();
     ArrayList<Descriptor> roleDescriptors = new ArrayList<>();
+    ArrayList<Descriptor> essentialProperties = new ArrayList<>();
     ArrayList<Descriptor> supplementalProperties = new ArrayList<>();
     List<RepresentationInfo> representationInfos = new ArrayList<>();
 
@@ -317,6 +318,8 @@
         audioChannels = parseAudioChannelConfiguration(xpp);
       } else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) {
         accessibilityDescriptors.add(parseDescriptor(xpp, "Accessibility"));
+      } else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) {
+        essentialProperties.add(parseDescriptor(xpp, "EssentialProperty"));
       } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) {
         supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty"));
       } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) {
@@ -334,6 +337,7 @@
                 language,
                 roleDescriptors,
                 accessibilityDescriptors,
+                essentialProperties,
                 supplementalProperties,
                 segmentBase,
                 periodDurationMs);
@@ -370,14 +374,28 @@
               inbandEventStreams));
     }
 
-    return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors,
+    return buildAdaptationSet(
+        id,
+        contentType,
+        representations,
+        accessibilityDescriptors,
+        essentialProperties,
         supplementalProperties);
   }
 
-  protected AdaptationSet buildAdaptationSet(int id, int contentType,
-      List<Representation> representations, List<Descriptor> accessibilityDescriptors,
+  protected AdaptationSet buildAdaptationSet(
+      int id,
+      int contentType,
+      List<Representation> representations,
+      List<Descriptor> accessibilityDescriptors,
+      List<Descriptor> essentialProperties,
       List<Descriptor> supplementalProperties) {
-    return new AdaptationSet(id, contentType, representations, accessibilityDescriptors,
+    return new AdaptationSet(
+        id,
+        contentType,
+        representations,
+        accessibilityDescriptors,
+        essentialProperties,
         supplementalProperties);
   }
 
@@ -492,6 +510,7 @@
       @Nullable String adaptationSetLanguage,
       List<Descriptor> adaptationSetRoleDescriptors,
       List<Descriptor> adaptationSetAccessibilityDescriptors,
+      List<Descriptor> adaptationSetEssentialProperties,
       List<Descriptor> adaptationSetSupplementalProperties,
       @Nullable SegmentBase segmentBase,
       long periodDurationMs)
@@ -509,7 +528,9 @@
     String drmSchemeType = null;
     ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();
     ArrayList<Descriptor> inbandEventStreams = new ArrayList<>();
-    ArrayList<Descriptor> supplementalProperties = new ArrayList<>();
+    ArrayList<Descriptor> essentialProperties = new ArrayList<>(adaptationSetEssentialProperties);
+    ArrayList<Descriptor> supplementalProperties =
+        new ArrayList<>(adaptationSetSupplementalProperties);
 
     boolean seenFirstBaseUrl = false;
     do {
@@ -542,6 +563,8 @@
         }
       } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
         inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream"));
+      } else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) {
+        essentialProperties.add(parseDescriptor(xpp, "EssentialProperty"));
       } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) {
         supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty"));
       } else {
@@ -563,6 +586,7 @@
             adaptationSetRoleDescriptors,
             adaptationSetAccessibilityDescriptors,
             codecs,
+            essentialProperties,
             supplementalProperties);
     segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase();
 
@@ -583,6 +607,7 @@
       List<Descriptor> roleDescriptors,
       List<Descriptor> accessibilityDescriptors,
       @Nullable String codecs,
+      List<Descriptor> essentialProperties,
       List<Descriptor> supplementalProperties) {
     @Nullable String sampleMimeType = getSampleMimeType(containerMimeType, codecs);
     if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) {
@@ -591,6 +616,8 @@
     @C.SelectionFlags int selectionFlags = parseSelectionFlagsFromRoleDescriptors(roleDescriptors);
     @C.RoleFlags int roleFlags = parseRoleFlagsFromRoleDescriptors(roleDescriptors);
     roleFlags |= parseRoleFlagsFromAccessibilityDescriptors(accessibilityDescriptors);
+    roleFlags |= parseRoleFlagsFromProperties(essentialProperties);
+    roleFlags |= parseRoleFlagsFromProperties(supplementalProperties);
 
     Format.Builder formatBuilder =
         new Format.Builder()
@@ -1186,6 +1213,18 @@
   }
 
   @C.RoleFlags
+  protected int parseRoleFlagsFromProperties(List<Descriptor> accessibilityDescriptors) {
+    @C.RoleFlags int result = 0;
+    for (int i = 0; i < accessibilityDescriptors.size(); i++) {
+      Descriptor descriptor = accessibilityDescriptors.get(i);
+      if ("http://dashif.org/guidelines/trickmode".equalsIgnoreCase(descriptor.schemeIdUri)) {
+        result |= C.ROLE_FLAG_TRICK_PLAY;
+      }
+    }
+    return result;
+  }
+
+  @C.RoleFlags
   protected int parseDashRoleSchemeValue(@Nullable String value) {
     if (value == null) {
       return 0;
diff --git a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java
index 92aa49d..e9e5f30 100644
--- a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java
+++ b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java
@@ -165,6 +165,7 @@
         trackType,
         Arrays.asList(representations),
         /* accessibilityDescriptors= */ Collections.emptyList(),
+        /* essentialProperties= */ Collections.emptyList(),
         descriptor == null ? Collections.emptyList() : Collections.singletonList(descriptor));
   }
 
diff --git a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java
index 28f15b2..3176b06 100644
--- a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java
+++ b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java
@@ -29,6 +29,7 @@
 import com.google.android.exoplayer2.upstream.DummyDataSource;
 import com.google.android.exoplayer2.util.MimeTypes;
 import java.util.Arrays;
+import java.util.Collections;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -69,7 +70,13 @@
   }
 
   private static AdaptationSet newAdaptationSet(Representation... representations) {
-    return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null, null);
+    return new AdaptationSet(
+        /* id= */ 0,
+        C.TRACK_TYPE_VIDEO,
+        Arrays.asList(representations),
+        /* accessibilityDescriptors= */ Collections.emptyList(),
+        /* essentialProperties= */ Collections.emptyList(),
+        /* supplementalProperties= */ Collections.emptyList());
   }
 
   private static Representation newRepresentation(DrmInitData drmInitData) {
diff --git a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java
index 37b3046..4708747 100644
--- a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java
+++ b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java
@@ -49,6 +49,7 @@
   private static final String SAMPLE_MPD_LABELS = "mpd/sample_mpd_labels";
   private static final String SAMPLE_MPD_ASSET_IDENTIFIER = "mpd/sample_mpd_asset_identifier";
   private static final String SAMPLE_MPD_TEXT = "mpd/sample_mpd_text";
+  private static final String SAMPLE_MPD_TRICK_PLAY = "mpd/sample_mpd_trick_play";
 
   private static final String NEXT_TAG_NAME = "Next";
   private static final String NEXT_TAG = "<" + NEXT_TAG_NAME + "/>";
@@ -173,7 +174,7 @@
     DashManifestParser parser = new DashManifestParser();
     DashManifest manifest =
         parser.parse(
-            Uri.parse("Https://example.com/test.mpd"),
+            Uri.parse("https://example.com/test.mpd"),
             TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD));
     ProgramInformation expectedProgramInformation =
         new ProgramInformation(
@@ -201,7 +202,7 @@
     DashManifestParser parser = new DashManifestParser();
     DashManifest manifest =
         parser.parse(
-            Uri.parse("Https://example.com/test.mpd"),
+            Uri.parse("https://example.com/test.mpd"),
             TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_TEXT));
 
     List<AdaptationSet> adaptationSets = manifest.getPeriod(0).adaptationSets;
@@ -226,6 +227,46 @@
   }
 
   @Test
+  public void parseMediaPresentationDescription_trickPlay() throws IOException {
+    DashManifestParser parser = new DashManifestParser();
+    DashManifest manifest =
+        parser.parse(
+            Uri.parse("https://example.com/test.mpd"),
+            TestUtil.getInputStream(
+                ApplicationProvider.getApplicationContext(), SAMPLE_MPD_TRICK_PLAY));
+
+    List<AdaptationSet> adaptationSets = manifest.getPeriod(0).adaptationSets;
+
+    AdaptationSet adaptationSet = adaptationSets.get(0);
+    assertThat(adaptationSet.essentialProperties).isEmpty();
+    assertThat(adaptationSet.supplementalProperties).isEmpty();
+    assertThat(adaptationSet.representations.get(0).format.roleFlags).isEqualTo(0);
+
+    adaptationSet = adaptationSets.get(1);
+    assertThat(adaptationSet.essentialProperties).isEmpty();
+    assertThat(adaptationSet.supplementalProperties).isEmpty();
+    assertThat(adaptationSet.representations.get(0).format.roleFlags).isEqualTo(0);
+
+    adaptationSet = adaptationSets.get(2);
+    assertThat(adaptationSet.essentialProperties).hasSize(1);
+    assertThat(adaptationSet.essentialProperties.get(0).schemeIdUri)
+        .isEqualTo("http://dashif.org/guidelines/trickmode");
+    assertThat(adaptationSet.essentialProperties.get(0).value).isEqualTo("0");
+    assertThat(adaptationSet.supplementalProperties).isEmpty();
+    assertThat(adaptationSet.representations.get(0).format.roleFlags)
+        .isEqualTo(C.ROLE_FLAG_TRICK_PLAY);
+
+    adaptationSet = adaptationSets.get(3);
+    assertThat(adaptationSet.essentialProperties).isEmpty();
+    assertThat(adaptationSet.supplementalProperties).hasSize(1);
+    assertThat(adaptationSet.supplementalProperties.get(0).schemeIdUri)
+        .isEqualTo("http://dashif.org/guidelines/trickmode");
+    assertThat(adaptationSet.supplementalProperties.get(0).value).isEqualTo("1");
+    assertThat(adaptationSet.representations.get(0).format.roleFlags)
+        .isEqualTo(C.ROLE_FLAG_TRICK_PLAY);
+  }
+
+  @Test
   public void parseSegmentTimeline_repeatCount() throws Exception {
     DashManifestParser parser = new DashManifestParser();
     XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
diff --git a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java
index b063c54..b260bf2 100644
--- a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java
+++ b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java
@@ -239,6 +239,12 @@
   }
 
   private static AdaptationSet newAdaptationSet(int seed, Representation... representations) {
-    return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), null, null);
+    return new AdaptationSet(
+        ++seed,
+        ++seed,
+        Arrays.asList(representations),
+        /* accessibilityDescriptors= */ Collections.emptyList(),
+        /* essentialProperties= */ Collections.emptyList(),
+        /* supplementalProperties= */ Collections.emptyList());
   }
 }
diff --git a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
index 84a92cf..359ccc1 100644
--- a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
+++ b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
@@ -665,9 +665,9 @@
   private static Pair<Integer, DefaultSampleValues> parseTrex(ParsableByteArray trex) {
     trex.setPosition(Atom.FULL_HEADER_SIZE);
     int trackId = trex.readInt();
-    int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;
-    int defaultSampleDuration = trex.readUnsignedIntToInt();
-    int defaultSampleSize = trex.readUnsignedIntToInt();
+    int defaultSampleDescriptionIndex = trex.readInt() - 1;
+    int defaultSampleDuration = trex.readInt();
+    int defaultSampleSize = trex.readInt();
     int defaultSampleFlags = trex.readInt();
 
     return Pair.create(trackId, new DefaultSampleValues(defaultSampleDescriptionIndex,
@@ -753,8 +753,9 @@
     }
   }
 
-  private static void parseTruns(ContainerAtom traf, TrackBundle trackBundle, long decodeTime,
-      @Flags int flags) {
+  private static void parseTruns(
+      ContainerAtom traf, TrackBundle trackBundle, long decodeTime, @Flags int flags)
+      throws ParserException {
     int trunCount = 0;
     int totalSampleCount = 0;
     List<LeafAtom> leafChildren = traf.leafChildren;
@@ -874,13 +875,20 @@
     DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues;
     int defaultSampleDescriptionIndex =
         ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0)
-            ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex;
-    int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0)
-        ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration;
-    int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0)
-        ? tfhd.readUnsignedIntToInt() : defaultSampleValues.size;
-    int defaultSampleFlags = ((atomFlags & 0x20 /* default_sample_flags_present */) != 0)
-        ? tfhd.readUnsignedIntToInt() : defaultSampleValues.flags;
+            ? tfhd.readInt() - 1
+            : defaultSampleValues.sampleDescriptionIndex;
+    int defaultSampleDuration =
+        ((atomFlags & 0x08 /* default_sample_duration_present */) != 0)
+            ? tfhd.readInt()
+            : defaultSampleValues.duration;
+    int defaultSampleSize =
+        ((atomFlags & 0x10 /* default_sample_size_present */) != 0)
+            ? tfhd.readInt()
+            : defaultSampleValues.size;
+    int defaultSampleFlags =
+        ((atomFlags & 0x20 /* default_sample_flags_present */) != 0)
+            ? tfhd.readInt()
+            : defaultSampleValues.flags;
     trackBundle.fragment.header = new DefaultSampleValues(defaultSampleDescriptionIndex,
         defaultSampleDuration, defaultSampleSize, defaultSampleFlags);
     return trackBundle;
@@ -913,16 +921,22 @@
   /**
    * Parses a trun atom (defined in 14496-12).
    *
-   * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into
-   *     which parsed data should be placed.
+   * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into which
+   *     parsed data should be placed.
    * @param index Index of the track run in the fragment.
    * @param decodeTime The decode time of the first sample in the fragment run.
    * @param flags Flags to allow any required workaround to be executed.
    * @param trun The trun atom to decode.
    * @return The starting position of samples for the next run.
    */
-  private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime,
-      @Flags int flags, ParsableByteArray trun, int trackRunStart) {
+  private static int parseTrun(
+      TrackBundle trackBundle,
+      int index,
+      long decodeTime,
+      @Flags int flags,
+      ParsableByteArray trun,
+      int trackRunStart)
+      throws ParserException {
     trun.setPosition(Atom.HEADER_SIZE);
     int fullAtom = trun.readInt();
     int atomFlags = Atom.parseFullAtomFlags(fullAtom);
@@ -940,7 +954,7 @@
     boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0;
     int firstSampleFlags = defaultSampleValues.flags;
     if (firstSampleFlagsPresent) {
-      firstSampleFlags = trun.readUnsignedIntToInt();
+      firstSampleFlags = trun.readInt();
     }
 
     boolean sampleDurationsPresent = (atomFlags & 0x100 /* sample_duration_present */) != 0;
@@ -975,9 +989,10 @@
     long cumulativeTime = index > 0 ? fragment.nextFragmentDecodeTime : decodeTime;
     for (int i = trackRunStart; i < trackRunEnd; i++) {
       // Use trun values if present, otherwise tfhd, otherwise trex.
-      int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
-          : defaultSampleValues.duration;
-      int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size;
+      int sampleDuration =
+          checkNonNegative(sampleDurationsPresent ? trun.readInt() : defaultSampleValues.duration);
+      int sampleSize =
+          checkNonNegative(sampleSizesPresent ? trun.readInt() : defaultSampleValues.size);
       int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
           : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
       if (sampleCompositionTimeOffsetsPresent) {
@@ -1003,6 +1018,13 @@
     return trackRunEnd;
   }
 
+  private static int checkNonNegative(int value) throws ParserException {
+    if (value < 0) {
+      throw new ParserException("Unexpected negtive value: " + value);
+    }
+    return value;
+  }
+
   private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
       byte[] extendedTypeScratch) throws ParserException {
     uuid.setPosition(Atom.HEADER_SIZE);
diff --git a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java
index 4369ff7..dcf64d9 100644
--- a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java
+++ b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java
@@ -172,9 +172,8 @@
 
   @RequiresNonNull("sampleReader")
   private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) {
-    if (hasOutputFormat) {
-      sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs);
-    } else {
+    sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs, hasOutputFormat);
+    if (!hasOutputFormat) {
       vps.startNalUnit(nalUnitType);
       sps.startNalUnit(nalUnitType);
       pps.startNalUnit(nalUnitType);
@@ -185,9 +184,8 @@
 
   @RequiresNonNull("sampleReader")
   private void nalUnitData(byte[] dataArray, int offset, int limit) {
-    if (hasOutputFormat) {
-      sampleReader.readNalUnitData(dataArray, offset, limit);
-    } else {
+    sampleReader.readNalUnitData(dataArray, offset, limit);
+    if (!hasOutputFormat) {
       vps.appendToNalUnit(dataArray, offset, limit);
       sps.appendToNalUnit(dataArray, offset, limit);
       pps.appendToNalUnit(dataArray, offset, limit);
@@ -198,9 +196,8 @@
 
   @RequiresNonNull({"output", "sampleReader"})
   private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) {
-    if (hasOutputFormat) {
-      sampleReader.endNalUnit(position, offset);
-    } else {
+    sampleReader.endNalUnit(position, offset, hasOutputFormat);
+    if (!hasOutputFormat) {
       vps.endNalUnit(discardPadding);
       sps.endNalUnit(discardPadding);
       pps.endNalUnit(discardPadding);
@@ -454,7 +451,8 @@
       writingParameterSets = false;
     }
 
-    public void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) {
+    public void startNalUnit(
+        long position, int offset, int nalUnitType, long pesTimeUs, boolean hasOutputFormat) {
       isFirstSlice = false;
       isFirstParameterSet = false;
       nalUnitTimeUs = pesTimeUs;
@@ -464,7 +462,9 @@
       if (nalUnitType >= VPS_NUT) {
         if (!writingParameterSets && readingSample) {
           // This is a non-VCL NAL unit, so flush the previous sample.
-          outputSample(offset);
+          if (hasOutputFormat) {
+            outputSample(offset);
+          }
           readingSample = false;
         }
         if (nalUnitType <= PPS_NUT) {
@@ -491,14 +491,14 @@
       }
     }
 
-    public void endNalUnit(long position, int offset) {
+    public void endNalUnit(long position, int offset, boolean hasOutputFormat) {
       if (writingParameterSets && isFirstSlice) {
         // This sample has parameter sets. Reset the key-frame flag based on the first slice.
         sampleIsKeyframe = nalUnitHasKeyframeData;
         writingParameterSets = false;
       } else if (isFirstParameterSet || isFirstSlice) {
         // This NAL unit is at the start of a new sample (access unit).
-        if (readingSample) {
+        if (hasOutputFormat && readingSample) {
           // Output the sample ending before this NAL unit.
           int nalUnitLength = (int) (position - nalUnitStartPosition);
           outputSample(offset + nalUnitLength);
diff --git a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PassthroughSectionPayloadReader.java b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PassthroughSectionPayloadReader.java
index af374f6..72667e2 100644
--- a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PassthroughSectionPayloadReader.java
+++ b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PassthroughSectionPayloadReader.java
@@ -33,10 +33,9 @@
  */
 public final class PassthroughSectionPayloadReader implements SectionPayloadReader {
 
-  private final String mimeType;
+  private Format format;
   private @MonotonicNonNull TimestampAdjuster timestampAdjuster;
   private @MonotonicNonNull TrackOutput output;
-  private boolean formatDeclared;
 
   /**
    * Create a new PassthroughSectionPayloadReader.
@@ -44,7 +43,7 @@
    * @param mimeType The MIME type set as {@link Format#sampleMimeType} on the created output track.
    */
   public PassthroughSectionPayloadReader(String mimeType) {
-    this.mimeType = mimeType;
+    this.format = new Format.Builder().setSampleMimeType(mimeType).build();
   }
 
   @Override
@@ -55,22 +54,22 @@
     this.timestampAdjuster = timestampAdjuster;
     idGenerator.generateNewId();
     output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA);
+    // Eagerly output an incomplete format (missing timestamp offset) to ensure source preparation
+    // is not blocked waiting for potentially sparse metadata.
+    output.format(format);
   }
 
   @Override
   public void consume(ParsableByteArray sectionData) {
     assertInitialized();
-    if (!formatDeclared) {
-      if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) {
-        // There is not enough information to initialize the timestamp adjuster.
-        return;
-      }
-      output.format(
-          new Format.Builder()
-              .setSampleMimeType(mimeType)
-              .setSubsampleOffsetUs(timestampAdjuster.getTimestampOffsetUs())
-              .build());
-      formatDeclared = true;
+    long subsampleOffsetUs = timestampAdjuster.getTimestampOffsetUs();
+    if (subsampleOffsetUs == C.TIME_UNSET) {
+      // Don't output samples without a known subsample offset.
+      return;
+    }
+    if (subsampleOffsetUs != format.subsampleOffsetUs) {
+      format = format.buildUpon().setSubsampleOffsetUs(subsampleOffsetUs).build();
+      output.format(format);
     }
     int sampleSize = sectionData.bytesLeft();
     output.sampleData(sectionData, sampleSize);
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java
index 554c032..5622137 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java
@@ -15,6 +15,7 @@
  */
 package com.google.android.exoplayer2.extractor.mp3;
 
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import com.google.android.exoplayer2.testutil.ExtractorAsserts;
 import org.junit.Test;
@@ -46,4 +47,22 @@
   public void trimmedMp3Sample() throws Exception {
     ExtractorAsserts.assertBehavior(Mp3Extractor::new, "mp3/play-trimmed.mp3");
   }
+
+  @Test
+  public void mp3SampleWithId3Enabled() throws Exception {
+    ExtractorAsserts.assertBehavior(
+        Mp3Extractor::new,
+        /* file= */ "mp3/bear-id3.mp3",
+        ApplicationProvider.getApplicationContext(),
+        /* dumpFilesPrefix= */ "mp3/bear-id3-enabled");
+  }
+
+  @Test
+  public void mp3SampleWithId3Disabled() throws Exception {
+    ExtractorAsserts.assertBehavior(
+        () -> new Mp3Extractor(Mp3Extractor.FLAG_DISABLE_ID3_METADATA),
+        /* file= */ "mp3/bear-id3.mp3",
+        ApplicationProvider.getApplicationContext(),
+        /* dumpFilesPrefix= */ "mp3/bear-id3-disabled");
+  }
 }
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java
index 794df91..d040c22 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java
@@ -36,6 +36,7 @@
 import com.google.android.exoplayer2.testutil.TestUtil;
 import com.google.android.exoplayer2.util.ParsableByteArray;
 import com.google.android.exoplayer2.util.TimestampAdjuster;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -54,11 +55,22 @@
   }
 
   @Test
+  public void sampleWithH265() throws Exception {
+    ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_h265.ts");
+  }
+
+  @Test
+  @Ignore
+  // TODO(internal: b/153539929) Re-enable when ExtractorAsserts is less strict around repeated
+  // formats and seeking.
   public void sampleWithScte35() throws Exception {
     ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_scte35.ts");
   }
 
   @Test
+  @Ignore
+  // TODO(internal: b/153539929) Re-enable when ExtractorAsserts is less strict around repeated
+  // formats and seeking.
   public void sampleWithAit() throws Exception {
     ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_ait.ts");
   }
diff --git a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
index 0b587fd..c269691 100644
--- a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
+++ b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
@@ -647,7 +647,7 @@
       checkInBounds();
       Segment segment = playlist.segments.get((int) getCurrentIndex());
       Uri chunkUri = UriUtil.resolveToUri(playlist.baseUri, segment.url);
-      return new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength);
+      return new DataSpec(chunkUri, segment.byteRangeOffset, segment.byteRangeLength);
     }
 
     @Override
diff --git a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
index d397dc5..3a2285a 100644
--- a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
+++ b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
@@ -94,8 +94,8 @@
     DataSpec dataSpec =
         new DataSpec(
             UriUtil.resolveToUri(mediaPlaylist.baseUri, mediaSegment.url),
-            mediaSegment.byterangeOffset,
-            mediaSegment.byterangeLength);
+            mediaSegment.byteRangeOffset,
+            mediaSegment.byteRangeLength);
     boolean mediaSegmentEncrypted = mediaSegmentKey != null;
     @Nullable
     byte[] mediaSegmentIv =
@@ -118,7 +118,7 @@
               : null;
       Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url);
       initDataSpec =
-          new DataSpec(initSegmentUri, initSegment.byterangeOffset, initSegment.byterangeLength);
+          new DataSpec(initSegmentUri, initSegment.byteRangeOffset, initSegment.byteRangeLength);
       initDataSource = buildDataSource(dataSource, initSegmentKey, initSegmentIv);
     }
 
diff --git a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java
index 7bf982b..d172aa2 100644
--- a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java
+++ b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java
@@ -141,7 +141,7 @@
       }
     }
     Uri segmentUri = UriUtil.resolveToUri(baseUri, segment.url);
-    DataSpec dataSpec = new DataSpec(segmentUri, segment.byterangeOffset, segment.byterangeLength);
+    DataSpec dataSpec = new DataSpec(segmentUri, segment.byteRangeOffset, segment.byteRangeLength);
     out.add(new Segment(startTimeUs, dataSpec));
   }
 }
diff --git a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
index 58f500c..be771b9 100644
--- a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
+++ b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
@@ -70,30 +70,28 @@
      * encrypted.
      */
     @Nullable public final String encryptionIV;
-    /**
-     * The segment's byte range offset, as defined by #EXT-X-BYTERANGE.
-     */
-    public final long byterangeOffset;
+    /** The segment's byte range offset, as defined by #EXT-X-BYTERANGE. */
+    public final long byteRangeOffset;
     /**
      * The segment's byte range length, as defined by #EXT-X-BYTERANGE, or {@link C#LENGTH_UNSET} if
      * no byte range is specified.
      */
-    public final long byterangeLength;
+    public final long byteRangeLength;
 
     /** Whether the segment is tagged with #EXT-X-GAP. */
     public final boolean hasGapTag;
 
     /**
      * @param uri See {@link #url}.
-     * @param byterangeOffset See {@link #byterangeOffset}.
-     * @param byterangeLength See {@link #byterangeLength}.
+     * @param byteRangeOffset See {@link #byteRangeOffset}.
+     * @param byteRangeLength See {@link #byteRangeLength}.
      * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}.
      * @param encryptionIV See {@link #encryptionIV}.
      */
     public Segment(
         String uri,
-        long byterangeOffset,
-        long byterangeLength,
+        long byteRangeOffset,
+        long byteRangeLength,
         @Nullable String fullSegmentEncryptionKeyUri,
         @Nullable String encryptionIV) {
       this(
@@ -106,8 +104,8 @@
           /* drmInitData= */ null,
           fullSegmentEncryptionKeyUri,
           encryptionIV,
-          byterangeOffset,
-          byterangeLength,
+          byteRangeOffset,
+          byteRangeLength,
           /* hasGapTag= */ false);
     }
 
@@ -121,8 +119,8 @@
      * @param drmInitData See {@link #drmInitData}.
      * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}.
      * @param encryptionIV See {@link #encryptionIV}.
-     * @param byterangeOffset See {@link #byterangeOffset}.
-     * @param byterangeLength See {@link #byterangeLength}.
+     * @param byteRangeOffset See {@link #byteRangeOffset}.
+     * @param byteRangeLength See {@link #byteRangeLength}.
      * @param hasGapTag See {@link #hasGapTag}.
      */
     public Segment(
@@ -135,8 +133,8 @@
         @Nullable DrmInitData drmInitData,
         @Nullable String fullSegmentEncryptionKeyUri,
         @Nullable String encryptionIV,
-        long byterangeOffset,
-        long byterangeLength,
+        long byteRangeOffset,
+        long byteRangeLength,
         boolean hasGapTag) {
       this.url = url;
       this.initializationSegment = initializationSegment;
@@ -147,8 +145,8 @@
       this.drmInitData = drmInitData;
       this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri;
       this.encryptionIV = encryptionIV;
-      this.byterangeOffset = byterangeOffset;
-      this.byterangeLength = byterangeLength;
+      this.byteRangeOffset = byteRangeOffset;
+      this.byteRangeLength = byteRangeLength;
       this.hasGapTag = hasGapTag;
     }
 
diff --git a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
index 8a0d190..77e541f 100644
--- a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
+++ b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
@@ -69,6 +69,8 @@
   private static final String TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE";
   private static final String TAG_DEFINE = "#EXT-X-DEFINE";
   private static final String TAG_STREAM_INF = "#EXT-X-STREAM-INF";
+  private static final String TAG_I_FRAME_STREAM_INF = "#EXT-X-I-FRAME-STREAM-INF";
+  private static final String TAG_IFRAME = "#EXT-X-I-FRAMES-ONLY";
   private static final String TAG_MEDIA = "#EXT-X-MEDIA";
   private static final String TAG_TARGET_DURATION = "#EXT-X-TARGETDURATION";
   private static final String TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
@@ -281,6 +283,7 @@
         // We expose all tags through the playlist.
         tags.add(line);
       }
+      boolean isIFrameOnlyVariant = line.startsWith(TAG_I_FRAME_STREAM_INF);
 
       if (line.startsWith(TAG_DEFINE)) {
         variableDefinitions.put(
@@ -301,8 +304,9 @@
           String scheme = parseEncryptionScheme(method);
           sessionKeyDrmInitData.add(new DrmInitData(scheme, schemeData));
         }
-      } else if (line.startsWith(TAG_STREAM_INF)) {
+      } else if (line.startsWith(TAG_STREAM_INF) || isIFrameOnlyVariant) {
         noClosedCaptions |= line.contains(ATTR_CLOSED_CAPTIONS_NONE);
+        int roleFlags = isIFrameOnlyVariant ? C.ROLE_FLAG_TRICK_PLAY : 0;
         int peakBitrate = parseIntAttr(line, REGEX_BANDWIDTH);
         int averageBitrate = parseOptionalIntAttr(line, REGEX_AVERAGE_BANDWIDTH, -1);
         String codecs = parseOptionalStringAttr(line, REGEX_CODECS, variableDefinitions);
@@ -335,13 +339,18 @@
             parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions);
         String closedCaptionsGroupId =
             parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions);
-        if (!iterator.hasNext()) {
-          throw new ParserException("#EXT-X-STREAM-INF tag must be followed by another line");
+        Uri uri;
+        if (isIFrameOnlyVariant) {
+          uri =
+              UriUtil.resolveToUri(baseUri, parseStringAttr(line, REGEX_URI, variableDefinitions));
+        } else if (!iterator.hasNext()) {
+          throw new ParserException("#EXT-X-STREAM-INF must be followed by another line");
+        } else {
+          // The following line contains #EXT-X-STREAM-INF's URI.
+          line = replaceVariableReferences(iterator.next(), variableDefinitions);
+          uri = UriUtil.resolveToUri(baseUri, line);
         }
-        line =
-            replaceVariableReferences(
-                iterator.next(), variableDefinitions); // #EXT-X-STREAM-INF's URI.
-        Uri uri = UriUtil.resolveToUri(baseUri, line);
+
         Format format =
             new Format.Builder()
                 .setId(variants.size())
@@ -352,6 +361,7 @@
                 .setWidth(width)
                 .setHeight(height)
                 .setFrameRate(frameRate)
+                .setRoleFlags(roleFlags)
                 .build();
         Variant variant =
             new Variant(
@@ -558,8 +568,9 @@
     long targetDurationUs = C.TIME_UNSET;
     boolean hasIndependentSegmentsTag = masterPlaylist.hasIndependentSegments;
     boolean hasEndTag = false;
-    Segment initializationSegment = null;
+    @Nullable Segment initializationSegment = null;
     HashMap<String, String> variableDefinitions = new HashMap<>();
+    HashMap<String, Segment> urlToInferredInitSegment = new HashMap<>();
     List<Segment> segments = new ArrayList<>();
     List<String> tags = new ArrayList<>();
 
@@ -572,6 +583,7 @@
     long segmentStartTimeUs = 0;
     long segmentByteRangeOffset = 0;
     long segmentByteRangeLength = C.LENGTH_UNSET;
+    boolean isIFrameOnly = false;
     long segmentMediaSequence = 0;
     boolean hasGapTag = false;
 
@@ -598,6 +610,8 @@
         } else if ("EVENT".equals(playlistTypeString)) {
           playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT;
         }
+      } else if (line.equals(TAG_IFRAME)) {
+        isIFrameOnly = true;
       } else if (line.startsWith(TAG_START)) {
         startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
       } else if (line.startsWith(TAG_INIT_SEGMENT)) {
@@ -715,8 +729,25 @@
         }
 
         segmentMediaSequence++;
+        String segmentUri = replaceVariableReferences(line, variableDefinitions);
+        @Nullable Segment inferredInitSegment = urlToInferredInitSegment.get(segmentUri);
         if (segmentByteRangeLength == C.LENGTH_UNSET) {
+          // The segment is not byte range defined.
           segmentByteRangeOffset = 0;
+        } else if (isIFrameOnly && initializationSegment == null && inferredInitSegment == null) {
+          // The segment is a resource byte range without an initialization segment.
+          // As per RFC 8216, Section 4.3.3.6, we assume the initialization section exists in the
+          // bytes preceding the first segment in this segment's URL.
+          // We assume the implicit initialization segment is unencrypted, since there's no way for
+          // the playlist to provide an initialization vector for it.
+          inferredInitSegment =
+              new Segment(
+                  segmentUri,
+                  /* byteRangeOffset= */ 0,
+                  segmentByteRangeOffset,
+                  /* fullSegmentEncryptionKeyUri= */ null,
+                  /* encryptionIV= */ null);
+          urlToInferredInitSegment.put(segmentUri, inferredInitSegment);
         }
 
         if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
@@ -733,8 +764,8 @@
 
         segments.add(
             new Segment(
-                replaceVariableReferences(line, variableDefinitions),
-                initializationSegment,
+                segmentUri,
+                initializationSegment != null ? initializationSegment : inferredInitSegment,
                 segmentTitle,
                 segmentDurationUs,
                 relativeDiscontinuitySequence,
diff --git a/tree/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/tree/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java
index 55aa4b3..05bc3ba 100644
--- a/tree/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java
+++ b/tree/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java
@@ -25,6 +25,7 @@
 import com.google.android.exoplayer2.ParserException;
 import com.google.android.exoplayer2.metadata.Metadata;
 import com.google.android.exoplayer2.source.hls.HlsTrackMetadataEntry;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
 import com.google.android.exoplayer2.util.MimeTypes;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
@@ -207,6 +208,22 @@
           + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",NAME=\"English\",URI=\"a1/index.m3u8\"\n"
           + "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"sub1\",NAME=\"English\",AUTOSELECT=YES,DEFAULT=YES,URI=\"s1/en/prog_index.m3u8\"\n";
 
+  private static final String PLAYLIST_WITH_IFRAME_VARIANTS =
+      "#EXTM3U\n"
+          + "#EXT-X-VERSION:5\n"
+          + "#EXT-X-MEDIA:URI=\"AUDIO_English/index.m3u8\",TYPE=AUDIO,GROUP-ID=\"audio-aac\",LANGUAGE=\"en\",NAME=\"English\",AUTOSELECT=YES\n"
+          + "#EXT-X-MEDIA:URI=\"AUDIO_Spanish/index.m3u8\",TYPE=AUDIO,GROUP-ID=\"audio-aac\",LANGUAGE=\"es\",NAME=\"Spanish\",AUTOSELECT=YES\n"
+          + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"cc1\",LANGUAGE=\"en\",NAME=\"English\",AUTOSELECT=YES,DEFAULT=YES,INSTREAM-ID=\"CC1\"\n"
+          + "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=400000,RESOLUTION=480x320,CODECS=\"mp4a.40.2,avc1.640015\",AUDIO=\"audio-aac\",CLOSED-CAPTIONS=\"cc1\"\n"
+          + "400000/index.m3u8\n"
+          + "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000,RESOLUTION=848x480,CODECS=\"mp4a.40.2,avc1.64001f\",AUDIO=\"audio-aac\",CLOSED-CAPTIONS=\"cc1\"\n"
+          + "1000000/index.m3u8\n"
+          + "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3220000,RESOLUTION=1280x720,CODECS=\"mp4a.40.2,avc1.64001f\",AUDIO=\"audio-aac\",CLOSED-CAPTIONS=\"cc1\"\n"
+          + "3220000/index.m3u8\n"
+          + "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=8940000,RESOLUTION=1920x1080,CODECS=\"mp4a.40.2,avc1.640028\",AUDIO=\"audio-aac\",CLOSED-CAPTIONS=\"cc1\"\n"
+          + "8940000/index.m3u8\n"
+          + "#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1313400,RESOLUTION=1920x1080,CODECS=\"avc1.640028\",URI=\"iframe_1313400/index.m3u8\"\n";
+
   @Test
   public void parseMasterPlaylist_withSimple_success() throws IOException {
     HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
@@ -407,6 +424,19 @@
         .isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud3", /* name= */ "English"));
   }
 
+  @Test
+  public void testIFrameVariant() throws IOException {
+    HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_IFRAME_VARIANTS);
+    assertThat(playlist.variants).hasSize(5);
+    for (int i = 0; i < 4; i++) {
+      assertThat(playlist.variants.get(i).format.roleFlags).isEqualTo(0);
+    }
+    Variant iFramesOnlyVariant = playlist.variants.get(4);
+    assertThat(iFramesOnlyVariant.format.bitrate).isEqualTo(1313400);
+    assertThat(iFramesOnlyVariant.format.roleFlags & C.ROLE_FLAG_TRICK_PLAY)
+        .isEqualTo(C.ROLE_FLAG_TRICK_PLAY);
+  }
+
   private static Metadata createExtXStreamInfMetadata(HlsTrackMetadataEntry.VariantInfo... infos) {
     return new Metadata(
         new HlsTrackMetadataEntry(/* groupId= */ null, /* name= */ null, Arrays.asList(infos)));
diff --git a/tree/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/tree/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java
index ce5ff84..dd8a32b 100644
--- a/tree/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java
+++ b/tree/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.fail;
 
 import android.net.Uri;
+import androidx.annotation.Nullable;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import com.google.android.exoplayer2.C;
 import com.google.android.exoplayer2.ParserException;
@@ -96,8 +97,8 @@
     assertThat(segment.title).isEqualTo("");
     assertThat(segment.fullSegmentEncryptionKeyUri).isNull();
     assertThat(segment.encryptionIV).isNull();
-    assertThat(segment.byterangeLength).isEqualTo(51370);
-    assertThat(segment.byterangeOffset).isEqualTo(0);
+    assertThat(segment.byteRangeLength).isEqualTo(51370);
+    assertThat(segment.byteRangeOffset).isEqualTo(0);
     assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2679.ts");
 
     segment = segments.get(1);
@@ -107,8 +108,8 @@
     assertThat(segment.fullSegmentEncryptionKeyUri)
         .isEqualTo("https://priv.example.com/key.php?r=2680");
     assertThat(segment.encryptionIV).isEqualTo("0x1566B");
-    assertThat(segment.byterangeLength).isEqualTo(51501);
-    assertThat(segment.byterangeOffset).isEqualTo(2147483648L);
+    assertThat(segment.byteRangeLength).isEqualTo(51501);
+    assertThat(segment.byteRangeOffset).isEqualTo(2147483648L);
     assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2680.ts");
 
     segment = segments.get(2);
@@ -117,8 +118,8 @@
     assertThat(segment.title).isEqualTo("segment title .,:/# with interesting chars");
     assertThat(segment.fullSegmentEncryptionKeyUri).isNull();
     assertThat(segment.encryptionIV).isEqualTo(null);
-    assertThat(segment.byterangeLength).isEqualTo(51501);
-    assertThat(segment.byterangeOffset).isEqualTo(2147535149L);
+    assertThat(segment.byteRangeLength).isEqualTo(51501);
+    assertThat(segment.byteRangeOffset).isEqualTo(2147535149L);
     assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2681.ts");
 
     segment = segments.get(3);
@@ -130,8 +131,8 @@
     // 0xA7A == 2682.
     assertThat(segment.encryptionIV).isNotNull();
     assertThat(Util.toUpperInvariant(segment.encryptionIV)).isEqualTo("A7A");
-    assertThat(segment.byterangeLength).isEqualTo(51740);
-    assertThat(segment.byterangeOffset).isEqualTo(2147586650L);
+    assertThat(segment.byteRangeLength).isEqualTo(51740);
+    assertThat(segment.byteRangeOffset).isEqualTo(2147586650L);
     assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2682.ts");
 
     segment = segments.get(4);
@@ -143,8 +144,8 @@
     // 0xA7B == 2683.
     assertThat(segment.encryptionIV).isNotNull();
     assertThat(Util.toUpperInvariant(segment.encryptionIV)).isEqualTo("A7B");
-    assertThat(segment.byterangeLength).isEqualTo(C.LENGTH_UNSET);
-    assertThat(segment.byterangeOffset).isEqualTo(0);
+    assertThat(segment.byteRangeLength).isEqualTo(C.LENGTH_UNSET);
+    assertThat(segment.byteRangeOffset).isEqualTo(0);
     assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts");
   }
 
@@ -368,6 +369,40 @@
   }
 
   @Test
+  public void noExplicitInitSegmentInIFrameOnly_infersInitSegment() throws IOException {
+    Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");
+    String playlistString =
+        "#EXTM3U\n"
+            + "#EXT-X-TARGETDURATION:5\n"
+            + "#EXT-X-I-FRAMES-ONLY\n"
+            + "#EXTINF:5.005,\n"
+            + "#EXT-X-BYTERANGE:100@300\n"
+            + "segment1.ts\n"
+            + "#EXTINF:5.005,\n"
+            + "#EXT-X-BYTERANGE:100@400\n"
+            + "segment2.ts\n"
+            + "#EXTINF:5.005,\n"
+            + "#EXT-X-BYTERANGE:100@400\n"
+            + "segment1.ts\n";
+    InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
+    HlsMediaPlaylist playlist =
+        (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
+    List<Segment> segments = playlist.segments;
+    @Nullable Segment initializationSegment = segments.get(0).initializationSegment;
+    assertThat(initializationSegment.url).isEqualTo("segment1.ts");
+    assertThat(initializationSegment.byteRangeOffset).isEqualTo(0);
+    assertThat(initializationSegment.byteRangeLength).isEqualTo(300);
+    initializationSegment = segments.get(1).initializationSegment;
+    assertThat(initializationSegment.url).isEqualTo("segment2.ts");
+    assertThat(initializationSegment.byteRangeOffset).isEqualTo(0);
+    assertThat(initializationSegment.byteRangeLength).isEqualTo(400);
+    initializationSegment = segments.get(2).initializationSegment;
+    assertThat(initializationSegment.url).isEqualTo("segment1.ts");
+    assertThat(initializationSegment.byteRangeOffset).isEqualTo(0);
+    assertThat(initializationSegment.byteRangeLength).isEqualTo(300);
+  }
+
+  @Test
   public void encryptedMapTag() throws IOException {
     Uri playlistUri = Uri.parse("https://example.com/test3.m3u8");
     String playlistString =
diff --git a/tree/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/tree/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java
index 775045c..ee9f3f9 100644
--- a/tree/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java
+++ b/tree/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java
@@ -23,8 +23,8 @@
 import android.util.AttributeSet;
 import android.util.TypedValue;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.accessibility.CaptioningManager;
+import android.widget.FrameLayout;
 import androidx.annotation.Dimension;
 import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
@@ -39,7 +39,7 @@
 import java.util.List;
 
 /** A view for displaying subtitle {@link Cue}s. */
-public final class SubtitleView extends ViewGroup implements TextOutput {
+public final class SubtitleView extends FrameLayout implements TextOutput {
 
   /**
    * The default fractional text size.
@@ -116,13 +116,6 @@
     output.onCues(cues != null ? cues : Collections.emptyList());
   }
 
-  @Override
-  protected void onLayout(boolean changed, int l, int t, int r, int b) {
-    if (changed) {
-      innerSubtitleView.layout(l, t, r, b);
-    }
-  }
-
   /**
    * Set the type of {@link View} used to display subtitles.
    *
diff --git a/tree/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleWebView.java b/tree/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleWebView.java
index 670314e..3a5560b 100644
--- a/tree/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleWebView.java
+++ b/tree/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleWebView.java
@@ -25,8 +25,8 @@
 import android.util.AttributeSet;
 import android.util.Base64;
 import android.view.MotionEvent;
-import android.view.ViewGroup;
 import android.webkit.WebView;
+import android.widget.FrameLayout;
 import androidx.annotation.Nullable;
 import com.google.android.exoplayer2.C;
 import com.google.android.exoplayer2.text.CaptionStyleCompat;
@@ -45,7 +45,7 @@
  * <p>NOTE: This is currently extremely experimental and doesn't support most {@link Cue} styling
  * properties.
  */
-/* package */ final class SubtitleWebView extends ViewGroup implements SubtitleView.Output {
+/* package */ final class SubtitleWebView extends FrameLayout implements SubtitleView.Output {
 
   private final WebView webView;
 
@@ -145,13 +145,6 @@
     updateWebView();
   }
 
-  @Override
-  protected void onLayout(boolean changed, int l, int t, int r, int b) {
-    if (changed) {
-      webView.layout(l, t, r, b);
-    }
-  }
-
   private void updateWebView() {
     StringBuilder html = new StringBuilder();
     html.append("<html><body>")
diff --git a/tree/testdata/src/test/assets/mp3/bear-id3-disabled.0.dump b/tree/testdata/src/test/assets/mp3/bear-id3-disabled.0.dump
new file mode 100644
index 0000000..a80bf5e
--- /dev/null
+++ b/tree/testdata/src/test/assets/mp3/bear-id3-disabled.0.dump
@@ -0,0 +1,487 @@
+seekMap:
+  isSeekable = true
+  duration = 2808000
+  getPosition(0) = [[timeUs=0, position=39740]]
+  getPosition(1) = [[timeUs=0, position=39740]]
+  getPosition(1404000) = [[timeUs=1404000, position=58820]]
+  getPosition(2808000) = [[timeUs=2808000, position=77900]]
+numberOfTracks = 1
+track 0:
+  total output bytes = 38160
+  sample count = 117
+  format 0:
+    sampleMimeType = audio/mpeg
+    maxInputSize = 4096
+    channelCount = 2
+    sampleRate = 48000
+    encoderDelay = 576
+    encoderPadding = 576
+  sample 0:
+    time = 0
+    flags = 1
+    data = length 96, hash 1F161542
+  sample 1:
+    time = 24000
+    flags = 1
+    data = length 768, hash CD1DC50F
+  sample 2:
+    time = 48000
+    flags = 1
+    data = length 336, hash 3F64124B
+  sample 3:
+    time = 72000
+    flags = 1
+    data = length 336, hash 8FFED94E
+  sample 4:
+    time = 96000
+    flags = 1
+    data = length 288, hash 9CD77D47
+  sample 5:
+    time = 120000
+    flags = 1
+    data = length 384, hash 24607BB5
+  sample 6:
+    time = 144000
+    flags = 1
+    data = length 480, hash 4937EBAB
+  sample 7:
+    time = 168000
+    flags = 1
+    data = length 336, hash 546342B1
+  sample 8:
+    time = 192000
+    flags = 1
+    data = length 336, hash 79E0923F
+  sample 9:
+    time = 216000
+    flags = 1
+    data = length 336, hash AB1F3948
+  sample 10:
+    time = 240000
+    flags = 1
+    data = length 336, hash C3A4D888
+  sample 11:
+    time = 264000
+    flags = 1
+    data = length 288, hash 7867DA45
+  sample 12:
+    time = 288000
+    flags = 1
+    data = length 336, hash B1240B73
+  sample 13:
+    time = 312000
+    flags = 1
+    data = length 336, hash 94CFCD35
+  sample 14:
+    time = 336000
+    flags = 1
+    data = length 288, hash 94F412C
+  sample 15:
+    time = 360000
+    flags = 1
+    data = length 336, hash A1D9FF41
+  sample 16:
+    time = 384000
+    flags = 1
+    data = length 288, hash 2A8DA21B
+  sample 17:
+    time = 408000
+    flags = 1
+    data = length 336, hash 6A429CE
+  sample 18:
+    time = 432000
+    flags = 1
+    data = length 336, hash 68853982
+  sample 19:
+    time = 456000
+    flags = 1
+    data = length 384, hash 1D6F779C
+  sample 20:
+    time = 480000
+    flags = 1
+    data = length 480, hash 6B31EBEE
+  sample 21:
+    time = 504000
+    flags = 1
+    data = length 336, hash 888335BE
+  sample 22:
+    time = 528000
+    flags = 1
+    data = length 336, hash 6072AC8B
+  sample 23:
+    time = 552000
+    flags = 1
+    data = length 336, hash C9D24234
+  sample 24:
+    time = 576000
+    flags = 1
+    data = length 288, hash 52BF4D1E
+  sample 25:
+    time = 600000
+    flags = 1
+    data = length 336, hash F93F4F0
+  sample 26:
+    time = 624000
+    flags = 1
+    data = length 336, hash 8617688A
+  sample 27:
+    time = 648000
+    flags = 1
+    data = length 480, hash FAB0D31B
+  sample 28:
+    time = 672000
+    flags = 1
+    data = length 384, hash FA4B53E2
+  sample 29:
+    time = 696000
+    flags = 1
+    data = length 336, hash 8C435F6A
+  sample 30:
+    time = 720000
+    flags = 1
+    data = length 336, hash 60D3F80C
+  sample 31:
+    time = 744000
+    flags = 1
+    data = length 336, hash DC15B68B
+  sample 32:
+    time = 768000
+    flags = 1
+    data = length 288, hash FF3DF141
+  sample 33:
+    time = 792000
+    flags = 1
+    data = length 336, hash A64B3042
+  sample 34:
+    time = 816000
+    flags = 1
+    data = length 336, hash ACA622A1
+  sample 35:
+    time = 840000
+    flags = 1
+    data = length 288, hash 3E34B8D4
+  sample 36:
+    time = 864000
+    flags = 1
+    data = length 288, hash 9B96F72A
+  sample 37:
+    time = 888000
+    flags = 1
+    data = length 336, hash E917C122
+  sample 38:
+    time = 912000
+    flags = 1
+    data = length 336, hash 10ED1470
+  sample 39:
+    time = 936000
+    flags = 1
+    data = length 288, hash 706B8A7C
+  sample 40:
+    time = 960000
+    flags = 1
+    data = length 336, hash 71FFE4A0
+  sample 41:
+    time = 984000
+    flags = 1
+    data = length 336, hash D4160463
+  sample 42:
+    time = 1008000
+    flags = 1
+    data = length 336, hash EC557B14
+  sample 43:
+    time = 1032000
+    flags = 1
+    data = length 288, hash 5598CF8B
+  sample 44:
+    time = 1056000
+    flags = 1
+    data = length 336, hash 7E0AB41
+  sample 45:
+    time = 1080000
+    flags = 1
+    data = length 336, hash 1C585FEF
+  sample 46:
+    time = 1104000
+    flags = 1
+    data = length 336, hash A4A4855E
+  sample 47:
+    time = 1128000
+    flags = 1
+    data = length 336, hash CECA51D3
+  sample 48:
+    time = 1152000
+    flags = 1
+    data = length 288, hash 2D362DC5
+  sample 49:
+    time = 1176000
+    flags = 1
+    data = length 336, hash 9EB2609D
+  sample 50:
+    time = 1200000
+    flags = 1
+    data = length 336, hash 28FFB3FE
+  sample 51:
+    time = 1224000
+    flags = 1
+    data = length 288, hash 2AA2D216
+  sample 52:
+    time = 1248000
+    flags = 1
+    data = length 336, hash CDBC7032
+  sample 53:
+    time = 1272000
+    flags = 1
+    data = length 336, hash 25B13FE7
+  sample 54:
+    time = 1296000
+    flags = 1
+    data = length 336, hash DB6BB1E
+  sample 55:
+    time = 1320000
+    flags = 1
+    data = length 336, hash EBE951F4
+  sample 56:
+    time = 1344000
+    flags = 1
+    data = length 288, hash 9E2EBFF7
+  sample 57:
+    time = 1368000
+    flags = 1
+    data = length 336, hash 36A7D455
+  sample 58:
+    time = 1392000
+    flags = 1
+    data = length 336, hash 84545F8C
+  sample 59:
+    time = 1416000
+    flags = 1
+    data = length 336, hash F66F3045
+  sample 60:
+    time = 1440000
+    flags = 1
+    data = length 576, hash 5AB089EA
+  sample 61:
+    time = 1464000
+    flags = 1
+    data = length 336, hash 8868086
+  sample 62:
+    time = 1488000
+    flags = 1
+    data = length 336, hash D5EB6D63
+  sample 63:
+    time = 1512000
+    flags = 1
+    data = length 288, hash 7A5374B7
+  sample 64:
+    time = 1536000
+    flags = 1
+    data = length 336, hash BEB27A75
+  sample 65:
+    time = 1560000
+    flags = 1
+    data = length 336, hash E251E0FD
+  sample 66:
+    time = 1584000
+    flags = 1
+    data = length 288, hash D54C970
+  sample 67:
+    time = 1608000
+    flags = 1
+    data = length 336, hash 52C473B9
+  sample 68:
+    time = 1632000
+    flags = 1
+    data = length 336, hash F5F13334
+  sample 69:
+    time = 1656000
+    flags = 1
+    data = length 480, hash A5F1E987
+  sample 70:
+    time = 1680000
+    flags = 1
+    data = length 288, hash 453A1267
+  sample 71:
+    time = 1704000
+    flags = 1
+    data = length 288, hash 7C6C2EA9
+  sample 72:
+    time = 1728000
+    flags = 1
+    data = length 336, hash F4BFECA4
+  sample 73:
+    time = 1752000
+    flags = 1
+    data = length 336, hash 751A395A
+  sample 74:
+    time = 1776000
+    flags = 1
+    data = length 336, hash EE38DB02
+  sample 75:
+    time = 1800000
+    flags = 1
+    data = length 336, hash F18837E2
+  sample 76:
+    time = 1824000
+    flags = 1
+    data = length 336, hash ED36B78E
+  sample 77:
+    time = 1848000
+    flags = 1
+    data = length 336, hash B3D28289
+  sample 78:
+    time = 1872000
+    flags = 1
+    data = length 288, hash 8BDE28E1
+  sample 79:
+    time = 1896000
+    flags = 1
+    data = length 336, hash CFD5E966
+  sample 80:
+    time = 1920000
+    flags = 1
+    data = length 288, hash DC08E267
+  sample 81:
+    time = 1944000
+    flags = 1
+    data = length 336, hash 6530CB78
+  sample 82:
+    time = 1968000
+    flags = 1
+    data = length 336, hash 6CC6636E
+  sample 83:
+    time = 1992000
+    flags = 1
+    data = length 336, hash 613047C1
+  sample 84:
+    time = 2016000
+    flags = 1
+    data = length 288, hash CDC747BF
+  sample 85:
+    time = 2040000
+    flags = 1
+    data = length 336, hash AF22AA74
+  sample 86:
+    time = 2064000
+    flags = 1
+    data = length 384, hash 82F326AA
+  sample 87:
+    time = 2088000
+    flags = 1
+    data = length 384, hash EDA26C4D
+  sample 88:
+    time = 2112000
+    flags = 1
+    data = length 336, hash 94C643DC
+  sample 89:
+    time = 2136000
+    flags = 1
+    data = length 288, hash CB5D9C40
+  sample 90:
+    time = 2160000
+    flags = 1
+    data = length 336, hash 1E69DE3F
+  sample 91:
+    time = 2184000
+    flags = 1
+    data = length 336, hash 7E472219
+  sample 92:
+    time = 2208000
+    flags = 1
+    data = length 336, hash DA47B9FA
+  sample 93:
+    time = 2232000
+    flags = 1
+    data = length 336, hash DD0ABB7C
+  sample 94:
+    time = 2256000
+    flags = 1
+    data = length 288, hash DBF93FAC
+  sample 95:
+    time = 2280000
+    flags = 1
+    data = length 336, hash 243F4B2
+  sample 96:
+    time = 2304000
+    flags = 1
+    data = length 336, hash 2E881490
+  sample 97:
+    time = 2328000
+    flags = 1
+    data = length 288, hash 1C28C8BE
+  sample 98:
+    time = 2352000
+    flags = 1
+    data = length 336, hash C73E5D30
+  sample 99:
+    time = 2376000
+    flags = 1
+    data = length 288, hash 98B5BFF6
+  sample 100:
+    time = 2400000
+    flags = 1
+    data = length 336, hash E0135533
+  sample 101:
+    time = 2424000
+    flags = 1
+    data = length 336, hash D13C9DBC
+  sample 102:
+    time = 2448000
+    flags = 1
+    data = length 336, hash 63D524CA
+  sample 103:
+    time = 2472000
+    flags = 1
+    data = length 288, hash A28514C3
+  sample 104:
+    time = 2496000
+    flags = 1
+    data = length 336, hash 72B647FF
+  sample 105:
+    time = 2520000
+    flags = 1
+    data = length 336, hash 8F740AB1
+  sample 106:
+    time = 2544000
+    flags = 1
+    data = length 336, hash 5E3C7E93
+  sample 107:
+    time = 2568000
+    flags = 1
+    data = length 336, hash 121B913B
+  sample 108:
+    time = 2592000
+    flags = 1
+    data = length 336, hash 578FCCF2
+  sample 109:
+    time = 2616000
+    flags = 1
+    data = length 336, hash 5B5823DE
+  sample 110:
+    time = 2640000
+    flags = 1
+    data = length 384, hash D8B83F78
+  sample 111:
+    time = 2664000
+    flags = 1
+    data = length 240, hash E649682F
+  sample 112:
+    time = 2688000
+    flags = 1
+    data = length 96, hash C559A6F4
+  sample 113:
+    time = 2712000
+    flags = 1
+    data = length 96, hash 792796BC
+  sample 114:
+    time = 2736000
+    flags = 1
+    data = length 120, hash 8172CD0E
+  sample 115:
+    time = 2760000
+    flags = 1
+    data = length 120, hash F562B52F
+  sample 116:
+    time = 2784000
+    flags = 1
+    data = length 96, hash FF8D5B98
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp3/bear-id3-disabled.1.dump b/tree/testdata/src/test/assets/mp3/bear-id3-disabled.1.dump
new file mode 100644
index 0000000..27e36eb
--- /dev/null
+++ b/tree/testdata/src/test/assets/mp3/bear-id3-disabled.1.dump
@@ -0,0 +1,339 @@
+seekMap:
+  isSeekable = true
+  duration = 2808000
+  getPosition(0) = [[timeUs=0, position=39740]]
+  getPosition(1) = [[timeUs=0, position=39740]]
+  getPosition(1404000) = [[timeUs=1404000, position=58820]]
+  getPosition(2808000) = [[timeUs=2808000, position=77900]]
+numberOfTracks = 1
+track 0:
+  total output bytes = 25344
+  sample count = 80
+  format 0:
+    sampleMimeType = audio/mpeg
+    maxInputSize = 4096
+    channelCount = 2
+    sampleRate = 48000
+    encoderDelay = 576
+    encoderPadding = 576
+  sample 0:
+    time = 943000
+    flags = 1
+    data = length 336, hash E917C122
+  sample 1:
+    time = 967000
+    flags = 1
+    data = length 336, hash 10ED1470
+  sample 2:
+    time = 991000
+    flags = 1
+    data = length 288, hash 706B8A7C
+  sample 3:
+    time = 1015000
+    flags = 1
+    data = length 336, hash 71FFE4A0
+  sample 4:
+    time = 1039000
+    flags = 1
+    data = length 336, hash D4160463
+  sample 5:
+    time = 1063000
+    flags = 1
+    data = length 336, hash EC557B14
+  sample 6:
+    time = 1087000
+    flags = 1
+    data = length 288, hash 5598CF8B
+  sample 7:
+    time = 1111000
+    flags = 1
+    data = length 336, hash 7E0AB41
+  sample 8:
+    time = 1135000
+    flags = 1
+    data = length 336, hash 1C585FEF
+  sample 9:
+    time = 1159000
+    flags = 1
+    data = length 336, hash A4A4855E
+  sample 10:
+    time = 1183000
+    flags = 1
+    data = length 336, hash CECA51D3
+  sample 11:
+    time = 1207000
+    flags = 1
+    data = length 288, hash 2D362DC5
+  sample 12:
+    time = 1231000
+    flags = 1
+    data = length 336, hash 9EB2609D
+  sample 13:
+    time = 1255000
+    flags = 1
+    data = length 336, hash 28FFB3FE
+  sample 14:
+    time = 1279000
+    flags = 1
+    data = length 288, hash 2AA2D216
+  sample 15:
+    time = 1303000
+    flags = 1
+    data = length 336, hash CDBC7032
+  sample 16:
+    time = 1327000
+    flags = 1
+    data = length 336, hash 25B13FE7
+  sample 17:
+    time = 1351000
+    flags = 1
+    data = length 336, hash DB6BB1E
+  sample 18:
+    time = 1375000
+    flags = 1
+    data = length 336, hash EBE951F4
+  sample 19:
+    time = 1399000
+    flags = 1
+    data = length 288, hash 9E2EBFF7
+  sample 20:
+    time = 1423000
+    flags = 1
+    data = length 336, hash 36A7D455
+  sample 21:
+    time = 1447000
+    flags = 1
+    data = length 336, hash 84545F8C
+  sample 22:
+    time = 1471000
+    flags = 1
+    data = length 336, hash F66F3045
+  sample 23:
+    time = 1495000
+    flags = 1
+    data = length 576, hash 5AB089EA
+  sample 24:
+    time = 1519000
+    flags = 1
+    data = length 336, hash 8868086
+  sample 25:
+    time = 1543000
+    flags = 1
+    data = length 336, hash D5EB6D63
+  sample 26:
+    time = 1567000
+    flags = 1
+    data = length 288, hash 7A5374B7
+  sample 27:
+    time = 1591000
+    flags = 1
+    data = length 336, hash BEB27A75
+  sample 28:
+    time = 1615000
+    flags = 1
+    data = length 336, hash E251E0FD
+  sample 29:
+    time = 1639000
+    flags = 1
+    data = length 288, hash D54C970
+  sample 30:
+    time = 1663000
+    flags = 1
+    data = length 336, hash 52C473B9
+  sample 31:
+    time = 1687000
+    flags = 1
+    data = length 336, hash F5F13334
+  sample 32:
+    time = 1711000
+    flags = 1
+    data = length 480, hash A5F1E987
+  sample 33:
+    time = 1735000
+    flags = 1
+    data = length 288, hash 453A1267
+  sample 34:
+    time = 1759000
+    flags = 1
+    data = length 288, hash 7C6C2EA9
+  sample 35:
+    time = 1783000
+    flags = 1
+    data = length 336, hash F4BFECA4
+  sample 36:
+    time = 1807000
+    flags = 1
+    data = length 336, hash 751A395A
+  sample 37:
+    time = 1831000
+    flags = 1
+    data = length 336, hash EE38DB02
+  sample 38:
+    time = 1855000
+    flags = 1
+    data = length 336, hash F18837E2
+  sample 39:
+    time = 1879000
+    flags = 1
+    data = length 336, hash ED36B78E
+  sample 40:
+    time = 1903000
+    flags = 1
+    data = length 336, hash B3D28289
+  sample 41:
+    time = 1927000
+    flags = 1
+    data = length 288, hash 8BDE28E1
+  sample 42:
+    time = 1951000
+    flags = 1
+    data = length 336, hash CFD5E966
+  sample 43:
+    time = 1975000
+    flags = 1
+    data = length 288, hash DC08E267
+  sample 44:
+    time = 1999000
+    flags = 1
+    data = length 336, hash 6530CB78
+  sample 45:
+    time = 2023000
+    flags = 1
+    data = length 336, hash 6CC6636E
+  sample 46:
+    time = 2047000
+    flags = 1
+    data = length 336, hash 613047C1
+  sample 47:
+    time = 2071000
+    flags = 1
+    data = length 288, hash CDC747BF
+  sample 48:
+    time = 2095000
+    flags = 1
+    data = length 336, hash AF22AA74
+  sample 49:
+    time = 2119000
+    flags = 1
+    data = length 384, hash 82F326AA
+  sample 50:
+    time = 2143000
+    flags = 1
+    data = length 384, hash EDA26C4D
+  sample 51:
+    time = 2167000
+    flags = 1
+    data = length 336, hash 94C643DC
+  sample 52:
+    time = 2191000
+    flags = 1
+    data = length 288, hash CB5D9C40
+  sample 53:
+    time = 2215000
+    flags = 1
+    data = length 336, hash 1E69DE3F
+  sample 54:
+    time = 2239000
+    flags = 1
+    data = length 336, hash 7E472219
+  sample 55:
+    time = 2263000
+    flags = 1
+    data = length 336, hash DA47B9FA
+  sample 56:
+    time = 2287000
+    flags = 1
+    data = length 336, hash DD0ABB7C
+  sample 57:
+    time = 2311000
+    flags = 1
+    data = length 288, hash DBF93FAC
+  sample 58:
+    time = 2335000
+    flags = 1
+    data = length 336, hash 243F4B2
+  sample 59:
+    time = 2359000
+    flags = 1
+    data = length 336, hash 2E881490
+  sample 60:
+    time = 2383000
+    flags = 1
+    data = length 288, hash 1C28C8BE
+  sample 61:
+    time = 2407000
+    flags = 1
+    data = length 336, hash C73E5D30
+  sample 62:
+    time = 2431000
+    flags = 1
+    data = length 288, hash 98B5BFF6
+  sample 63:
+    time = 2455000
+    flags = 1
+    data = length 336, hash E0135533
+  sample 64:
+    time = 2479000
+    flags = 1
+    data = length 336, hash D13C9DBC
+  sample 65:
+    time = 2503000
+    flags = 1
+    data = length 336, hash 63D524CA
+  sample 66:
+    time = 2527000
+    flags = 1
+    data = length 288, hash A28514C3
+  sample 67:
+    time = 2551000
+    flags = 1
+    data = length 336, hash 72B647FF
+  sample 68:
+    time = 2575000
+    flags = 1
+    data = length 336, hash 8F740AB1
+  sample 69:
+    time = 2599000
+    flags = 1
+    data = length 336, hash 5E3C7E93
+  sample 70:
+    time = 2623000
+    flags = 1
+    data = length 336, hash 121B913B
+  sample 71:
+    time = 2647000
+    flags = 1
+    data = length 336, hash 578FCCF2
+  sample 72:
+    time = 2671000
+    flags = 1
+    data = length 336, hash 5B5823DE
+  sample 73:
+    time = 2695000
+    flags = 1
+    data = length 384, hash D8B83F78
+  sample 74:
+    time = 2719000
+    flags = 1
+    data = length 240, hash E649682F
+  sample 75:
+    time = 2743000
+    flags = 1
+    data = length 96, hash C559A6F4
+  sample 76:
+    time = 2767000
+    flags = 1
+    data = length 96, hash 792796BC
+  sample 77:
+    time = 2791000
+    flags = 1
+    data = length 120, hash 8172CD0E
+  sample 78:
+    time = 2815000
+    flags = 1
+    data = length 120, hash F562B52F
+  sample 79:
+    time = 2839000
+    flags = 1
+    data = length 96, hash FF8D5B98
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp3/bear-id3-disabled.2.dump b/tree/testdata/src/test/assets/mp3/bear-id3-disabled.2.dump
new file mode 100644
index 0000000..356e7d9
--- /dev/null
+++ b/tree/testdata/src/test/assets/mp3/bear-id3-disabled.2.dump
@@ -0,0 +1,187 @@
+seekMap:
+  isSeekable = true
+  duration = 2808000
+  getPosition(0) = [[timeUs=0, position=39740]]
+  getPosition(1) = [[timeUs=0, position=39740]]
+  getPosition(1404000) = [[timeUs=1404000, position=58820]]
+  getPosition(2808000) = [[timeUs=2808000, position=77900]]
+numberOfTracks = 1
+track 0:
+  total output bytes = 12624
+  sample count = 42
+  format 0:
+    sampleMimeType = audio/mpeg
+    maxInputSize = 4096
+    channelCount = 2
+    sampleRate = 48000
+    encoderDelay = 576
+    encoderPadding = 576
+  sample 0:
+    time = 1879000
+    flags = 1
+    data = length 336, hash F18837E2
+  sample 1:
+    time = 1903000
+    flags = 1
+    data = length 336, hash ED36B78E
+  sample 2:
+    time = 1927000
+    flags = 1
+    data = length 336, hash B3D28289
+  sample 3:
+    time = 1951000
+    flags = 1
+    data = length 288, hash 8BDE28E1
+  sample 4:
+    time = 1975000
+    flags = 1
+    data = length 336, hash CFD5E966
+  sample 5:
+    time = 1999000
+    flags = 1
+    data = length 288, hash DC08E267
+  sample 6:
+    time = 2023000
+    flags = 1
+    data = length 336, hash 6530CB78
+  sample 7:
+    time = 2047000
+    flags = 1
+    data = length 336, hash 6CC6636E
+  sample 8:
+    time = 2071000
+    flags = 1
+    data = length 336, hash 613047C1
+  sample 9:
+    time = 2095000
+    flags = 1
+    data = length 288, hash CDC747BF
+  sample 10:
+    time = 2119000
+    flags = 1
+    data = length 336, hash AF22AA74
+  sample 11:
+    time = 2143000
+    flags = 1
+    data = length 384, hash 82F326AA
+  sample 12:
+    time = 2167000
+    flags = 1
+    data = length 384, hash EDA26C4D
+  sample 13:
+    time = 2191000
+    flags = 1
+    data = length 336, hash 94C643DC
+  sample 14:
+    time = 2215000
+    flags = 1
+    data = length 288, hash CB5D9C40
+  sample 15:
+    time = 2239000
+    flags = 1
+    data = length 336, hash 1E69DE3F
+  sample 16:
+    time = 2263000
+    flags = 1
+    data = length 336, hash 7E472219
+  sample 17:
+    time = 2287000
+    flags = 1
+    data = length 336, hash DA47B9FA
+  sample 18:
+    time = 2311000
+    flags = 1
+    data = length 336, hash DD0ABB7C
+  sample 19:
+    time = 2335000
+    flags = 1
+    data = length 288, hash DBF93FAC
+  sample 20:
+    time = 2359000
+    flags = 1
+    data = length 336, hash 243F4B2
+  sample 21:
+    time = 2383000
+    flags = 1
+    data = length 336, hash 2E881490
+  sample 22:
+    time = 2407000
+    flags = 1
+    data = length 288, hash 1C28C8BE
+  sample 23:
+    time = 2431000
+    flags = 1
+    data = length 336, hash C73E5D30
+  sample 24:
+    time = 2455000
+    flags = 1
+    data = length 288, hash 98B5BFF6
+  sample 25:
+    time = 2479000
+    flags = 1
+    data = length 336, hash E0135533
+  sample 26:
+    time = 2503000
+    flags = 1
+    data = length 336, hash D13C9DBC
+  sample 27:
+    time = 2527000
+    flags = 1
+    data = length 336, hash 63D524CA
+  sample 28:
+    time = 2551000
+    flags = 1
+    data = length 288, hash A28514C3
+  sample 29:
+    time = 2575000
+    flags = 1
+    data = length 336, hash 72B647FF
+  sample 30:
+    time = 2599000
+    flags = 1
+    data = length 336, hash 8F740AB1
+  sample 31:
+    time = 2623000
+    flags = 1
+    data = length 336, hash 5E3C7E93
+  sample 32:
+    time = 2647000
+    flags = 1
+    data = length 336, hash 121B913B
+  sample 33:
+    time = 2671000
+    flags = 1
+    data = length 336, hash 578FCCF2
+  sample 34:
+    time = 2695000
+    flags = 1
+    data = length 336, hash 5B5823DE
+  sample 35:
+    time = 2719000
+    flags = 1
+    data = length 384, hash D8B83F78
+  sample 36:
+    time = 2743000
+    flags = 1
+    data = length 240, hash E649682F
+  sample 37:
+    time = 2767000
+    flags = 1
+    data = length 96, hash C559A6F4
+  sample 38:
+    time = 2791000
+    flags = 1
+    data = length 96, hash 792796BC
+  sample 39:
+    time = 2815000
+    flags = 1
+    data = length 120, hash 8172CD0E
+  sample 40:
+    time = 2839000
+    flags = 1
+    data = length 120, hash F562B52F
+  sample 41:
+    time = 2863000
+    flags = 1
+    data = length 96, hash FF8D5B98
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp3/bear-id3-disabled.3.dump b/tree/testdata/src/test/assets/mp3/bear-id3-disabled.3.dump
new file mode 100644
index 0000000..44c9375
--- /dev/null
+++ b/tree/testdata/src/test/assets/mp3/bear-id3-disabled.3.dump
@@ -0,0 +1,19 @@
+seekMap:
+  isSeekable = true
+  duration = 2808000
+  getPosition(0) = [[timeUs=0, position=39740]]
+  getPosition(1) = [[timeUs=0, position=39740]]
+  getPosition(1404000) = [[timeUs=1404000, position=58820]]
+  getPosition(2808000) = [[timeUs=2808000, position=77900]]
+numberOfTracks = 1
+track 0:
+  total output bytes = 0
+  sample count = 0
+  format 0:
+    sampleMimeType = audio/mpeg
+    maxInputSize = 4096
+    channelCount = 2
+    sampleRate = 48000
+    encoderDelay = 576
+    encoderPadding = 576
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp3/bear-id3-disabled.unknown_length.dump b/tree/testdata/src/test/assets/mp3/bear-id3-disabled.unknown_length.dump
new file mode 100644
index 0000000..a80bf5e
--- /dev/null
+++ b/tree/testdata/src/test/assets/mp3/bear-id3-disabled.unknown_length.dump
@@ -0,0 +1,487 @@
+seekMap:
+  isSeekable = true
+  duration = 2808000
+  getPosition(0) = [[timeUs=0, position=39740]]
+  getPosition(1) = [[timeUs=0, position=39740]]
+  getPosition(1404000) = [[timeUs=1404000, position=58820]]
+  getPosition(2808000) = [[timeUs=2808000, position=77900]]
+numberOfTracks = 1
+track 0:
+  total output bytes = 38160
+  sample count = 117
+  format 0:
+    sampleMimeType = audio/mpeg
+    maxInputSize = 4096
+    channelCount = 2
+    sampleRate = 48000
+    encoderDelay = 576
+    encoderPadding = 576
+  sample 0:
+    time = 0
+    flags = 1
+    data = length 96, hash 1F161542
+  sample 1:
+    time = 24000
+    flags = 1
+    data = length 768, hash CD1DC50F
+  sample 2:
+    time = 48000
+    flags = 1
+    data = length 336, hash 3F64124B
+  sample 3:
+    time = 72000
+    flags = 1
+    data = length 336, hash 8FFED94E
+  sample 4:
+    time = 96000
+    flags = 1
+    data = length 288, hash 9CD77D47
+  sample 5:
+    time = 120000
+    flags = 1
+    data = length 384, hash 24607BB5
+  sample 6:
+    time = 144000
+    flags = 1
+    data = length 480, hash 4937EBAB
+  sample 7:
+    time = 168000
+    flags = 1
+    data = length 336, hash 546342B1
+  sample 8:
+    time = 192000
+    flags = 1
+    data = length 336, hash 79E0923F
+  sample 9:
+    time = 216000
+    flags = 1
+    data = length 336, hash AB1F3948
+  sample 10:
+    time = 240000
+    flags = 1
+    data = length 336, hash C3A4D888
+  sample 11:
+    time = 264000
+    flags = 1
+    data = length 288, hash 7867DA45
+  sample 12:
+    time = 288000
+    flags = 1
+    data = length 336, hash B1240B73
+  sample 13:
+    time = 312000
+    flags = 1
+    data = length 336, hash 94CFCD35
+  sample 14:
+    time = 336000
+    flags = 1
+    data = length 288, hash 94F412C
+  sample 15:
+    time = 360000
+    flags = 1
+    data = length 336, hash A1D9FF41
+  sample 16:
+    time = 384000
+    flags = 1
+    data = length 288, hash 2A8DA21B
+  sample 17:
+    time = 408000
+    flags = 1
+    data = length 336, hash 6A429CE
+  sample 18:
+    time = 432000
+    flags = 1
+    data = length 336, hash 68853982
+  sample 19:
+    time = 456000
+    flags = 1
+    data = length 384, hash 1D6F779C
+  sample 20:
+    time = 480000
+    flags = 1
+    data = length 480, hash 6B31EBEE
+  sample 21:
+    time = 504000
+    flags = 1
+    data = length 336, hash 888335BE
+  sample 22:
+    time = 528000
+    flags = 1
+    data = length 336, hash 6072AC8B
+  sample 23:
+    time = 552000
+    flags = 1
+    data = length 336, hash C9D24234
+  sample 24:
+    time = 576000
+    flags = 1
+    data = length 288, hash 52BF4D1E
+  sample 25:
+    time = 600000
+    flags = 1
+    data = length 336, hash F93F4F0
+  sample 26:
+    time = 624000
+    flags = 1
+    data = length 336, hash 8617688A
+  sample 27:
+    time = 648000
+    flags = 1
+    data = length 480, hash FAB0D31B
+  sample 28:
+    time = 672000
+    flags = 1
+    data = length 384, hash FA4B53E2
+  sample 29:
+    time = 696000
+    flags = 1
+    data = length 336, hash 8C435F6A
+  sample 30:
+    time = 720000
+    flags = 1
+    data = length 336, hash 60D3F80C
+  sample 31:
+    time = 744000
+    flags = 1
+    data = length 336, hash DC15B68B
+  sample 32:
+    time = 768000
+    flags = 1
+    data = length 288, hash FF3DF141
+  sample 33:
+    time = 792000
+    flags = 1
+    data = length 336, hash A64B3042
+  sample 34:
+    time = 816000
+    flags = 1
+    data = length 336, hash ACA622A1
+  sample 35:
+    time = 840000
+    flags = 1
+    data = length 288, hash 3E34B8D4
+  sample 36:
+    time = 864000
+    flags = 1
+    data = length 288, hash 9B96F72A
+  sample 37:
+    time = 888000
+    flags = 1
+    data = length 336, hash E917C122
+  sample 38:
+    time = 912000
+    flags = 1
+    data = length 336, hash 10ED1470
+  sample 39:
+    time = 936000
+    flags = 1
+    data = length 288, hash 706B8A7C
+  sample 40:
+    time = 960000
+    flags = 1
+    data = length 336, hash 71FFE4A0
+  sample 41:
+    time = 984000
+    flags = 1
+    data = length 336, hash D4160463
+  sample 42:
+    time = 1008000
+    flags = 1
+    data = length 336, hash EC557B14
+  sample 43:
+    time = 1032000
+    flags = 1
+    data = length 288, hash 5598CF8B
+  sample 44:
+    time = 1056000
+    flags = 1
+    data = length 336, hash 7E0AB41
+  sample 45:
+    time = 1080000
+    flags = 1
+    data = length 336, hash 1C585FEF
+  sample 46:
+    time = 1104000
+    flags = 1
+    data = length 336, hash A4A4855E
+  sample 47:
+    time = 1128000
+    flags = 1
+    data = length 336, hash CECA51D3
+  sample 48:
+    time = 1152000
+    flags = 1
+    data = length 288, hash 2D362DC5
+  sample 49:
+    time = 1176000
+    flags = 1
+    data = length 336, hash 9EB2609D
+  sample 50:
+    time = 1200000
+    flags = 1
+    data = length 336, hash 28FFB3FE
+  sample 51:
+    time = 1224000
+    flags = 1
+    data = length 288, hash 2AA2D216
+  sample 52:
+    time = 1248000
+    flags = 1
+    data = length 336, hash CDBC7032
+  sample 53:
+    time = 1272000
+    flags = 1
+    data = length 336, hash 25B13FE7
+  sample 54:
+    time = 1296000
+    flags = 1
+    data = length 336, hash DB6BB1E
+  sample 55:
+    time = 1320000
+    flags = 1
+    data = length 336, hash EBE951F4
+  sample 56:
+    time = 1344000
+    flags = 1
+    data = length 288, hash 9E2EBFF7
+  sample 57:
+    time = 1368000
+    flags = 1
+    data = length 336, hash 36A7D455
+  sample 58:
+    time = 1392000
+    flags = 1
+    data = length 336, hash 84545F8C
+  sample 59:
+    time = 1416000
+    flags = 1
+    data = length 336, hash F66F3045
+  sample 60:
+    time = 1440000
+    flags = 1
+    data = length 576, hash 5AB089EA
+  sample 61:
+    time = 1464000
+    flags = 1
+    data = length 336, hash 8868086
+  sample 62:
+    time = 1488000
+    flags = 1
+    data = length 336, hash D5EB6D63
+  sample 63:
+    time = 1512000
+    flags = 1
+    data = length 288, hash 7A5374B7
+  sample 64:
+    time = 1536000
+    flags = 1
+    data = length 336, hash BEB27A75
+  sample 65:
+    time = 1560000
+    flags = 1
+    data = length 336, hash E251E0FD
+  sample 66:
+    time = 1584000
+    flags = 1
+    data = length 288, hash D54C970
+  sample 67:
+    time = 1608000
+    flags = 1
+    data = length 336, hash 52C473B9
+  sample 68:
+    time = 1632000
+    flags = 1
+    data = length 336, hash F5F13334
+  sample 69:
+    time = 1656000
+    flags = 1
+    data = length 480, hash A5F1E987
+  sample 70:
+    time = 1680000
+    flags = 1
+    data = length 288, hash 453A1267
+  sample 71:
+    time = 1704000
+    flags = 1
+    data = length 288, hash 7C6C2EA9
+  sample 72:
+    time = 1728000
+    flags = 1
+    data = length 336, hash F4BFECA4
+  sample 73:
+    time = 1752000
+    flags = 1
+    data = length 336, hash 751A395A
+  sample 74:
+    time = 1776000
+    flags = 1
+    data = length 336, hash EE38DB02
+  sample 75:
+    time = 1800000
+    flags = 1
+    data = length 336, hash F18837E2
+  sample 76:
+    time = 1824000
+    flags = 1
+    data = length 336, hash ED36B78E
+  sample 77:
+    time = 1848000
+    flags = 1
+    data = length 336, hash B3D28289
+  sample 78:
+    time = 1872000
+    flags = 1
+    data = length 288, hash 8BDE28E1
+  sample 79:
+    time = 1896000
+    flags = 1
+    data = length 336, hash CFD5E966
+  sample 80:
+    time = 1920000
+    flags = 1
+    data = length 288, hash DC08E267
+  sample 81:
+    time = 1944000
+    flags = 1
+    data = length 336, hash 6530CB78
+  sample 82:
+    time = 1968000
+    flags = 1
+    data = length 336, hash 6CC6636E
+  sample 83:
+    time = 1992000
+    flags = 1
+    data = length 336, hash 613047C1
+  sample 84:
+    time = 2016000
+    flags = 1
+    data = length 288, hash CDC747BF
+  sample 85:
+    time = 2040000
+    flags = 1
+    data = length 336, hash AF22AA74
+  sample 86:
+    time = 2064000
+    flags = 1
+    data = length 384, hash 82F326AA
+  sample 87:
+    time = 2088000
+    flags = 1
+    data = length 384, hash EDA26C4D
+  sample 88:
+    time = 2112000
+    flags = 1
+    data = length 336, hash 94C643DC
+  sample 89:
+    time = 2136000
+    flags = 1
+    data = length 288, hash CB5D9C40
+  sample 90:
+    time = 2160000
+    flags = 1
+    data = length 336, hash 1E69DE3F
+  sample 91:
+    time = 2184000
+    flags = 1
+    data = length 336, hash 7E472219
+  sample 92:
+    time = 2208000
+    flags = 1
+    data = length 336, hash DA47B9FA
+  sample 93:
+    time = 2232000
+    flags = 1
+    data = length 336, hash DD0ABB7C
+  sample 94:
+    time = 2256000
+    flags = 1
+    data = length 288, hash DBF93FAC
+  sample 95:
+    time = 2280000
+    flags = 1
+    data = length 336, hash 243F4B2
+  sample 96:
+    time = 2304000
+    flags = 1
+    data = length 336, hash 2E881490
+  sample 97:
+    time = 2328000
+    flags = 1
+    data = length 288, hash 1C28C8BE
+  sample 98:
+    time = 2352000
+    flags = 1
+    data = length 336, hash C73E5D30
+  sample 99:
+    time = 2376000
+    flags = 1
+    data = length 288, hash 98B5BFF6
+  sample 100:
+    time = 2400000
+    flags = 1
+    data = length 336, hash E0135533
+  sample 101:
+    time = 2424000
+    flags = 1
+    data = length 336, hash D13C9DBC
+  sample 102:
+    time = 2448000
+    flags = 1
+    data = length 336, hash 63D524CA
+  sample 103:
+    time = 2472000
+    flags = 1
+    data = length 288, hash A28514C3
+  sample 104:
+    time = 2496000
+    flags = 1
+    data = length 336, hash 72B647FF
+  sample 105:
+    time = 2520000
+    flags = 1
+    data = length 336, hash 8F740AB1
+  sample 106:
+    time = 2544000
+    flags = 1
+    data = length 336, hash 5E3C7E93
+  sample 107:
+    time = 2568000
+    flags = 1
+    data = length 336, hash 121B913B
+  sample 108:
+    time = 2592000
+    flags = 1
+    data = length 336, hash 578FCCF2
+  sample 109:
+    time = 2616000
+    flags = 1
+    data = length 336, hash 5B5823DE
+  sample 110:
+    time = 2640000
+    flags = 1
+    data = length 384, hash D8B83F78
+  sample 111:
+    time = 2664000
+    flags = 1
+    data = length 240, hash E649682F
+  sample 112:
+    time = 2688000
+    flags = 1
+    data = length 96, hash C559A6F4
+  sample 113:
+    time = 2712000
+    flags = 1
+    data = length 96, hash 792796BC
+  sample 114:
+    time = 2736000
+    flags = 1
+    data = length 120, hash 8172CD0E
+  sample 115:
+    time = 2760000
+    flags = 1
+    data = length 120, hash F562B52F
+  sample 116:
+    time = 2784000
+    flags = 1
+    data = length 96, hash FF8D5B98
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp3/bear-id3-enabled.0.dump b/tree/testdata/src/test/assets/mp3/bear-id3-enabled.0.dump
new file mode 100644
index 0000000..c252057
--- /dev/null
+++ b/tree/testdata/src/test/assets/mp3/bear-id3-enabled.0.dump
@@ -0,0 +1,488 @@
+seekMap:
+  isSeekable = true
+  duration = 2808000
+  getPosition(0) = [[timeUs=0, position=39740]]
+  getPosition(1) = [[timeUs=0, position=39740]]
+  getPosition(1404000) = [[timeUs=1404000, position=58820]]
+  getPosition(2808000) = [[timeUs=2808000, position=77900]]
+numberOfTracks = 1
+track 0:
+  total output bytes = 38160
+  sample count = 117
+  format 0:
+    sampleMimeType = audio/mpeg
+    maxInputSize = 4096
+    channelCount = 2
+    sampleRate = 48000
+    encoderDelay = 576
+    encoderPadding = 576
+    metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description]
+  sample 0:
+    time = 0
+    flags = 1
+    data = length 96, hash 1F161542
+  sample 1:
+    time = 24000
+    flags = 1
+    data = length 768, hash CD1DC50F
+  sample 2:
+    time = 48000
+    flags = 1
+    data = length 336, hash 3F64124B
+  sample 3:
+    time = 72000
+    flags = 1
+    data = length 336, hash 8FFED94E
+  sample 4:
+    time = 96000
+    flags = 1
+    data = length 288, hash 9CD77D47
+  sample 5:
+    time = 120000
+    flags = 1
+    data = length 384, hash 24607BB5
+  sample 6:
+    time = 144000
+    flags = 1
+    data = length 480, hash 4937EBAB
+  sample 7:
+    time = 168000
+    flags = 1
+    data = length 336, hash 546342B1
+  sample 8:
+    time = 192000
+    flags = 1
+    data = length 336, hash 79E0923F
+  sample 9:
+    time = 216000
+    flags = 1
+    data = length 336, hash AB1F3948
+  sample 10:
+    time = 240000
+    flags = 1
+    data = length 336, hash C3A4D888
+  sample 11:
+    time = 264000
+    flags = 1
+    data = length 288, hash 7867DA45
+  sample 12:
+    time = 288000
+    flags = 1
+    data = length 336, hash B1240B73
+  sample 13:
+    time = 312000
+    flags = 1
+    data = length 336, hash 94CFCD35
+  sample 14:
+    time = 336000
+    flags = 1
+    data = length 288, hash 94F412C
+  sample 15:
+    time = 360000
+    flags = 1
+    data = length 336, hash A1D9FF41
+  sample 16:
+    time = 384000
+    flags = 1
+    data = length 288, hash 2A8DA21B
+  sample 17:
+    time = 408000
+    flags = 1
+    data = length 336, hash 6A429CE
+  sample 18:
+    time = 432000
+    flags = 1
+    data = length 336, hash 68853982
+  sample 19:
+    time = 456000
+    flags = 1
+    data = length 384, hash 1D6F779C
+  sample 20:
+    time = 480000
+    flags = 1
+    data = length 480, hash 6B31EBEE
+  sample 21:
+    time = 504000
+    flags = 1
+    data = length 336, hash 888335BE
+  sample 22:
+    time = 528000
+    flags = 1
+    data = length 336, hash 6072AC8B
+  sample 23:
+    time = 552000
+    flags = 1
+    data = length 336, hash C9D24234
+  sample 24:
+    time = 576000
+    flags = 1
+    data = length 288, hash 52BF4D1E
+  sample 25:
+    time = 600000
+    flags = 1
+    data = length 336, hash F93F4F0
+  sample 26:
+    time = 624000
+    flags = 1
+    data = length 336, hash 8617688A
+  sample 27:
+    time = 648000
+    flags = 1
+    data = length 480, hash FAB0D31B
+  sample 28:
+    time = 672000
+    flags = 1
+    data = length 384, hash FA4B53E2
+  sample 29:
+    time = 696000
+    flags = 1
+    data = length 336, hash 8C435F6A
+  sample 30:
+    time = 720000
+    flags = 1
+    data = length 336, hash 60D3F80C
+  sample 31:
+    time = 744000
+    flags = 1
+    data = length 336, hash DC15B68B
+  sample 32:
+    time = 768000
+    flags = 1
+    data = length 288, hash FF3DF141
+  sample 33:
+    time = 792000
+    flags = 1
+    data = length 336, hash A64B3042
+  sample 34:
+    time = 816000
+    flags = 1
+    data = length 336, hash ACA622A1
+  sample 35:
+    time = 840000
+    flags = 1
+    data = length 288, hash 3E34B8D4
+  sample 36:
+    time = 864000
+    flags = 1
+    data = length 288, hash 9B96F72A
+  sample 37:
+    time = 888000
+    flags = 1
+    data = length 336, hash E917C122
+  sample 38:
+    time = 912000
+    flags = 1
+    data = length 336, hash 10ED1470
+  sample 39:
+    time = 936000
+    flags = 1
+    data = length 288, hash 706B8A7C
+  sample 40:
+    time = 960000
+    flags = 1
+    data = length 336, hash 71FFE4A0
+  sample 41:
+    time = 984000
+    flags = 1
+    data = length 336, hash D4160463
+  sample 42:
+    time = 1008000
+    flags = 1
+    data = length 336, hash EC557B14
+  sample 43:
+    time = 1032000
+    flags = 1
+    data = length 288, hash 5598CF8B
+  sample 44:
+    time = 1056000
+    flags = 1
+    data = length 336, hash 7E0AB41
+  sample 45:
+    time = 1080000
+    flags = 1
+    data = length 336, hash 1C585FEF
+  sample 46:
+    time = 1104000
+    flags = 1
+    data = length 336, hash A4A4855E
+  sample 47:
+    time = 1128000
+    flags = 1
+    data = length 336, hash CECA51D3
+  sample 48:
+    time = 1152000
+    flags = 1
+    data = length 288, hash 2D362DC5
+  sample 49:
+    time = 1176000
+    flags = 1
+    data = length 336, hash 9EB2609D
+  sample 50:
+    time = 1200000
+    flags = 1
+    data = length 336, hash 28FFB3FE
+  sample 51:
+    time = 1224000
+    flags = 1
+    data = length 288, hash 2AA2D216
+  sample 52:
+    time = 1248000
+    flags = 1
+    data = length 336, hash CDBC7032
+  sample 53:
+    time = 1272000
+    flags = 1
+    data = length 336, hash 25B13FE7
+  sample 54:
+    time = 1296000
+    flags = 1
+    data = length 336, hash DB6BB1E
+  sample 55:
+    time = 1320000
+    flags = 1
+    data = length 336, hash EBE951F4
+  sample 56:
+    time = 1344000
+    flags = 1
+    data = length 288, hash 9E2EBFF7
+  sample 57:
+    time = 1368000
+    flags = 1
+    data = length 336, hash 36A7D455
+  sample 58:
+    time = 1392000
+    flags = 1
+    data = length 336, hash 84545F8C
+  sample 59:
+    time = 1416000
+    flags = 1
+    data = length 336, hash F66F3045
+  sample 60:
+    time = 1440000
+    flags = 1
+    data = length 576, hash 5AB089EA
+  sample 61:
+    time = 1464000
+    flags = 1
+    data = length 336, hash 8868086
+  sample 62:
+    time = 1488000
+    flags = 1
+    data = length 336, hash D5EB6D63
+  sample 63:
+    time = 1512000
+    flags = 1
+    data = length 288, hash 7A5374B7
+  sample 64:
+    time = 1536000
+    flags = 1
+    data = length 336, hash BEB27A75
+  sample 65:
+    time = 1560000
+    flags = 1
+    data = length 336, hash E251E0FD
+  sample 66:
+    time = 1584000
+    flags = 1
+    data = length 288, hash D54C970
+  sample 67:
+    time = 1608000
+    flags = 1
+    data = length 336, hash 52C473B9
+  sample 68:
+    time = 1632000
+    flags = 1
+    data = length 336, hash F5F13334
+  sample 69:
+    time = 1656000
+    flags = 1
+    data = length 480, hash A5F1E987
+  sample 70:
+    time = 1680000
+    flags = 1
+    data = length 288, hash 453A1267
+  sample 71:
+    time = 1704000
+    flags = 1
+    data = length 288, hash 7C6C2EA9
+  sample 72:
+    time = 1728000
+    flags = 1
+    data = length 336, hash F4BFECA4
+  sample 73:
+    time = 1752000
+    flags = 1
+    data = length 336, hash 751A395A
+  sample 74:
+    time = 1776000
+    flags = 1
+    data = length 336, hash EE38DB02
+  sample 75:
+    time = 1800000
+    flags = 1
+    data = length 336, hash F18837E2
+  sample 76:
+    time = 1824000
+    flags = 1
+    data = length 336, hash ED36B78E
+  sample 77:
+    time = 1848000
+    flags = 1
+    data = length 336, hash B3D28289
+  sample 78:
+    time = 1872000
+    flags = 1
+    data = length 288, hash 8BDE28E1
+  sample 79:
+    time = 1896000
+    flags = 1
+    data = length 336, hash CFD5E966
+  sample 80:
+    time = 1920000
+    flags = 1
+    data = length 288, hash DC08E267
+  sample 81:
+    time = 1944000
+    flags = 1
+    data = length 336, hash 6530CB78
+  sample 82:
+    time = 1968000
+    flags = 1
+    data = length 336, hash 6CC6636E
+  sample 83:
+    time = 1992000
+    flags = 1
+    data = length 336, hash 613047C1
+  sample 84:
+    time = 2016000
+    flags = 1
+    data = length 288, hash CDC747BF
+  sample 85:
+    time = 2040000
+    flags = 1
+    data = length 336, hash AF22AA74
+  sample 86:
+    time = 2064000
+    flags = 1
+    data = length 384, hash 82F326AA
+  sample 87:
+    time = 2088000
+    flags = 1
+    data = length 384, hash EDA26C4D
+  sample 88:
+    time = 2112000
+    flags = 1
+    data = length 336, hash 94C643DC
+  sample 89:
+    time = 2136000
+    flags = 1
+    data = length 288, hash CB5D9C40
+  sample 90:
+    time = 2160000
+    flags = 1
+    data = length 336, hash 1E69DE3F
+  sample 91:
+    time = 2184000
+    flags = 1
+    data = length 336, hash 7E472219
+  sample 92:
+    time = 2208000
+    flags = 1
+    data = length 336, hash DA47B9FA
+  sample 93:
+    time = 2232000
+    flags = 1
+    data = length 336, hash DD0ABB7C
+  sample 94:
+    time = 2256000
+    flags = 1
+    data = length 288, hash DBF93FAC
+  sample 95:
+    time = 2280000
+    flags = 1
+    data = length 336, hash 243F4B2
+  sample 96:
+    time = 2304000
+    flags = 1
+    data = length 336, hash 2E881490
+  sample 97:
+    time = 2328000
+    flags = 1
+    data = length 288, hash 1C28C8BE
+  sample 98:
+    time = 2352000
+    flags = 1
+    data = length 336, hash C73E5D30
+  sample 99:
+    time = 2376000
+    flags = 1
+    data = length 288, hash 98B5BFF6
+  sample 100:
+    time = 2400000
+    flags = 1
+    data = length 336, hash E0135533
+  sample 101:
+    time = 2424000
+    flags = 1
+    data = length 336, hash D13C9DBC
+  sample 102:
+    time = 2448000
+    flags = 1
+    data = length 336, hash 63D524CA
+  sample 103:
+    time = 2472000
+    flags = 1
+    data = length 288, hash A28514C3
+  sample 104:
+    time = 2496000
+    flags = 1
+    data = length 336, hash 72B647FF
+  sample 105:
+    time = 2520000
+    flags = 1
+    data = length 336, hash 8F740AB1
+  sample 106:
+    time = 2544000
+    flags = 1
+    data = length 336, hash 5E3C7E93
+  sample 107:
+    time = 2568000
+    flags = 1
+    data = length 336, hash 121B913B
+  sample 108:
+    time = 2592000
+    flags = 1
+    data = length 336, hash 578FCCF2
+  sample 109:
+    time = 2616000
+    flags = 1
+    data = length 336, hash 5B5823DE
+  sample 110:
+    time = 2640000
+    flags = 1
+    data = length 384, hash D8B83F78
+  sample 111:
+    time = 2664000
+    flags = 1
+    data = length 240, hash E649682F
+  sample 112:
+    time = 2688000
+    flags = 1
+    data = length 96, hash C559A6F4
+  sample 113:
+    time = 2712000
+    flags = 1
+    data = length 96, hash 792796BC
+  sample 114:
+    time = 2736000
+    flags = 1
+    data = length 120, hash 8172CD0E
+  sample 115:
+    time = 2760000
+    flags = 1
+    data = length 120, hash F562B52F
+  sample 116:
+    time = 2784000
+    flags = 1
+    data = length 96, hash FF8D5B98
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp3/bear-id3-enabled.1.dump b/tree/testdata/src/test/assets/mp3/bear-id3-enabled.1.dump
new file mode 100644
index 0000000..76fcbc0
--- /dev/null
+++ b/tree/testdata/src/test/assets/mp3/bear-id3-enabled.1.dump
@@ -0,0 +1,340 @@
+seekMap:
+  isSeekable = true
+  duration = 2808000
+  getPosition(0) = [[timeUs=0, position=39740]]
+  getPosition(1) = [[timeUs=0, position=39740]]
+  getPosition(1404000) = [[timeUs=1404000, position=58820]]
+  getPosition(2808000) = [[timeUs=2808000, position=77900]]
+numberOfTracks = 1
+track 0:
+  total output bytes = 25344
+  sample count = 80
+  format 0:
+    sampleMimeType = audio/mpeg
+    maxInputSize = 4096
+    channelCount = 2
+    sampleRate = 48000
+    encoderDelay = 576
+    encoderPadding = 576
+    metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description]
+  sample 0:
+    time = 943000
+    flags = 1
+    data = length 336, hash E917C122
+  sample 1:
+    time = 967000
+    flags = 1
+    data = length 336, hash 10ED1470
+  sample 2:
+    time = 991000
+    flags = 1
+    data = length 288, hash 706B8A7C
+  sample 3:
+    time = 1015000
+    flags = 1
+    data = length 336, hash 71FFE4A0
+  sample 4:
+    time = 1039000
+    flags = 1
+    data = length 336, hash D4160463
+  sample 5:
+    time = 1063000
+    flags = 1
+    data = length 336, hash EC557B14
+  sample 6:
+    time = 1087000
+    flags = 1
+    data = length 288, hash 5598CF8B
+  sample 7:
+    time = 1111000
+    flags = 1
+    data = length 336, hash 7E0AB41
+  sample 8:
+    time = 1135000
+    flags = 1
+    data = length 336, hash 1C585FEF
+  sample 9:
+    time = 1159000
+    flags = 1
+    data = length 336, hash A4A4855E
+  sample 10:
+    time = 1183000
+    flags = 1
+    data = length 336, hash CECA51D3
+  sample 11:
+    time = 1207000
+    flags = 1
+    data = length 288, hash 2D362DC5
+  sample 12:
+    time = 1231000
+    flags = 1
+    data = length 336, hash 9EB2609D
+  sample 13:
+    time = 1255000
+    flags = 1
+    data = length 336, hash 28FFB3FE
+  sample 14:
+    time = 1279000
+    flags = 1
+    data = length 288, hash 2AA2D216
+  sample 15:
+    time = 1303000
+    flags = 1
+    data = length 336, hash CDBC7032
+  sample 16:
+    time = 1327000
+    flags = 1
+    data = length 336, hash 25B13FE7
+  sample 17:
+    time = 1351000
+    flags = 1
+    data = length 336, hash DB6BB1E
+  sample 18:
+    time = 1375000
+    flags = 1
+    data = length 336, hash EBE951F4
+  sample 19:
+    time = 1399000
+    flags = 1
+    data = length 288, hash 9E2EBFF7
+  sample 20:
+    time = 1423000
+    flags = 1
+    data = length 336, hash 36A7D455
+  sample 21:
+    time = 1447000
+    flags = 1
+    data = length 336, hash 84545F8C
+  sample 22:
+    time = 1471000
+    flags = 1
+    data = length 336, hash F66F3045
+  sample 23:
+    time = 1495000
+    flags = 1
+    data = length 576, hash 5AB089EA
+  sample 24:
+    time = 1519000
+    flags = 1
+    data = length 336, hash 8868086
+  sample 25:
+    time = 1543000
+    flags = 1
+    data = length 336, hash D5EB6D63
+  sample 26:
+    time = 1567000
+    flags = 1
+    data = length 288, hash 7A5374B7
+  sample 27:
+    time = 1591000
+    flags = 1
+    data = length 336, hash BEB27A75
+  sample 28:
+    time = 1615000
+    flags = 1
+    data = length 336, hash E251E0FD
+  sample 29:
+    time = 1639000
+    flags = 1
+    data = length 288, hash D54C970
+  sample 30:
+    time = 1663000
+    flags = 1
+    data = length 336, hash 52C473B9
+  sample 31:
+    time = 1687000
+    flags = 1
+    data = length 336, hash F5F13334
+  sample 32:
+    time = 1711000
+    flags = 1
+    data = length 480, hash A5F1E987
+  sample 33:
+    time = 1735000
+    flags = 1
+    data = length 288, hash 453A1267
+  sample 34:
+    time = 1759000
+    flags = 1
+    data = length 288, hash 7C6C2EA9
+  sample 35:
+    time = 1783000
+    flags = 1
+    data = length 336, hash F4BFECA4
+  sample 36:
+    time = 1807000
+    flags = 1
+    data = length 336, hash 751A395A
+  sample 37:
+    time = 1831000
+    flags = 1
+    data = length 336, hash EE38DB02
+  sample 38:
+    time = 1855000
+    flags = 1
+    data = length 336, hash F18837E2
+  sample 39:
+    time = 1879000
+    flags = 1
+    data = length 336, hash ED36B78E
+  sample 40:
+    time = 1903000
+    flags = 1
+    data = length 336, hash B3D28289
+  sample 41:
+    time = 1927000
+    flags = 1
+    data = length 288, hash 8BDE28E1
+  sample 42:
+    time = 1951000
+    flags = 1
+    data = length 336, hash CFD5E966
+  sample 43:
+    time = 1975000
+    flags = 1
+    data = length 288, hash DC08E267
+  sample 44:
+    time = 1999000
+    flags = 1
+    data = length 336, hash 6530CB78
+  sample 45:
+    time = 2023000
+    flags = 1
+    data = length 336, hash 6CC6636E
+  sample 46:
+    time = 2047000
+    flags = 1
+    data = length 336, hash 613047C1
+  sample 47:
+    time = 2071000
+    flags = 1
+    data = length 288, hash CDC747BF
+  sample 48:
+    time = 2095000
+    flags = 1
+    data = length 336, hash AF22AA74
+  sample 49:
+    time = 2119000
+    flags = 1
+    data = length 384, hash 82F326AA
+  sample 50:
+    time = 2143000
+    flags = 1
+    data = length 384, hash EDA26C4D
+  sample 51:
+    time = 2167000
+    flags = 1
+    data = length 336, hash 94C643DC
+  sample 52:
+    time = 2191000
+    flags = 1
+    data = length 288, hash CB5D9C40
+  sample 53:
+    time = 2215000
+    flags = 1
+    data = length 336, hash 1E69DE3F
+  sample 54:
+    time = 2239000
+    flags = 1
+    data = length 336, hash 7E472219
+  sample 55:
+    time = 2263000
+    flags = 1
+    data = length 336, hash DA47B9FA
+  sample 56:
+    time = 2287000
+    flags = 1
+    data = length 336, hash DD0ABB7C
+  sample 57:
+    time = 2311000
+    flags = 1
+    data = length 288, hash DBF93FAC
+  sample 58:
+    time = 2335000
+    flags = 1
+    data = length 336, hash 243F4B2
+  sample 59:
+    time = 2359000
+    flags = 1
+    data = length 336, hash 2E881490
+  sample 60:
+    time = 2383000
+    flags = 1
+    data = length 288, hash 1C28C8BE
+  sample 61:
+    time = 2407000
+    flags = 1
+    data = length 336, hash C73E5D30
+  sample 62:
+    time = 2431000
+    flags = 1
+    data = length 288, hash 98B5BFF6
+  sample 63:
+    time = 2455000
+    flags = 1
+    data = length 336, hash E0135533
+  sample 64:
+    time = 2479000
+    flags = 1
+    data = length 336, hash D13C9DBC
+  sample 65:
+    time = 2503000
+    flags = 1
+    data = length 336, hash 63D524CA
+  sample 66:
+    time = 2527000
+    flags = 1
+    data = length 288, hash A28514C3
+  sample 67:
+    time = 2551000
+    flags = 1
+    data = length 336, hash 72B647FF
+  sample 68:
+    time = 2575000
+    flags = 1
+    data = length 336, hash 8F740AB1
+  sample 69:
+    time = 2599000
+    flags = 1
+    data = length 336, hash 5E3C7E93
+  sample 70:
+    time = 2623000
+    flags = 1
+    data = length 336, hash 121B913B
+  sample 71:
+    time = 2647000
+    flags = 1
+    data = length 336, hash 578FCCF2
+  sample 72:
+    time = 2671000
+    flags = 1
+    data = length 336, hash 5B5823DE
+  sample 73:
+    time = 2695000
+    flags = 1
+    data = length 384, hash D8B83F78
+  sample 74:
+    time = 2719000
+    flags = 1
+    data = length 240, hash E649682F
+  sample 75:
+    time = 2743000
+    flags = 1
+    data = length 96, hash C559A6F4
+  sample 76:
+    time = 2767000
+    flags = 1
+    data = length 96, hash 792796BC
+  sample 77:
+    time = 2791000
+    flags = 1
+    data = length 120, hash 8172CD0E
+  sample 78:
+    time = 2815000
+    flags = 1
+    data = length 120, hash F562B52F
+  sample 79:
+    time = 2839000
+    flags = 1
+    data = length 96, hash FF8D5B98
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp3/bear-id3-enabled.2.dump b/tree/testdata/src/test/assets/mp3/bear-id3-enabled.2.dump
new file mode 100644
index 0000000..4f9b29d
--- /dev/null
+++ b/tree/testdata/src/test/assets/mp3/bear-id3-enabled.2.dump
@@ -0,0 +1,188 @@
+seekMap:
+  isSeekable = true
+  duration = 2808000
+  getPosition(0) = [[timeUs=0, position=39740]]
+  getPosition(1) = [[timeUs=0, position=39740]]
+  getPosition(1404000) = [[timeUs=1404000, position=58820]]
+  getPosition(2808000) = [[timeUs=2808000, position=77900]]
+numberOfTracks = 1
+track 0:
+  total output bytes = 12624
+  sample count = 42
+  format 0:
+    sampleMimeType = audio/mpeg
+    maxInputSize = 4096
+    channelCount = 2
+    sampleRate = 48000
+    encoderDelay = 576
+    encoderPadding = 576
+    metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description]
+  sample 0:
+    time = 1879000
+    flags = 1
+    data = length 336, hash F18837E2
+  sample 1:
+    time = 1903000
+    flags = 1
+    data = length 336, hash ED36B78E
+  sample 2:
+    time = 1927000
+    flags = 1
+    data = length 336, hash B3D28289
+  sample 3:
+    time = 1951000
+    flags = 1
+    data = length 288, hash 8BDE28E1
+  sample 4:
+    time = 1975000
+    flags = 1
+    data = length 336, hash CFD5E966
+  sample 5:
+    time = 1999000
+    flags = 1
+    data = length 288, hash DC08E267
+  sample 6:
+    time = 2023000
+    flags = 1
+    data = length 336, hash 6530CB78
+  sample 7:
+    time = 2047000
+    flags = 1
+    data = length 336, hash 6CC6636E
+  sample 8:
+    time = 2071000
+    flags = 1
+    data = length 336, hash 613047C1
+  sample 9:
+    time = 2095000
+    flags = 1
+    data = length 288, hash CDC747BF
+  sample 10:
+    time = 2119000
+    flags = 1
+    data = length 336, hash AF22AA74
+  sample 11:
+    time = 2143000
+    flags = 1
+    data = length 384, hash 82F326AA
+  sample 12:
+    time = 2167000
+    flags = 1
+    data = length 384, hash EDA26C4D
+  sample 13:
+    time = 2191000
+    flags = 1
+    data = length 336, hash 94C643DC
+  sample 14:
+    time = 2215000
+    flags = 1
+    data = length 288, hash CB5D9C40
+  sample 15:
+    time = 2239000
+    flags = 1
+    data = length 336, hash 1E69DE3F
+  sample 16:
+    time = 2263000
+    flags = 1
+    data = length 336, hash 7E472219
+  sample 17:
+    time = 2287000
+    flags = 1
+    data = length 336, hash DA47B9FA
+  sample 18:
+    time = 2311000
+    flags = 1
+    data = length 336, hash DD0ABB7C
+  sample 19:
+    time = 2335000
+    flags = 1
+    data = length 288, hash DBF93FAC
+  sample 20:
+    time = 2359000
+    flags = 1
+    data = length 336, hash 243F4B2
+  sample 21:
+    time = 2383000
+    flags = 1
+    data = length 336, hash 2E881490
+  sample 22:
+    time = 2407000
+    flags = 1
+    data = length 288, hash 1C28C8BE
+  sample 23:
+    time = 2431000
+    flags = 1
+    data = length 336, hash C73E5D30
+  sample 24:
+    time = 2455000
+    flags = 1
+    data = length 288, hash 98B5BFF6
+  sample 25:
+    time = 2479000
+    flags = 1
+    data = length 336, hash E0135533
+  sample 26:
+    time = 2503000
+    flags = 1
+    data = length 336, hash D13C9DBC
+  sample 27:
+    time = 2527000
+    flags = 1
+    data = length 336, hash 63D524CA
+  sample 28:
+    time = 2551000
+    flags = 1
+    data = length 288, hash A28514C3
+  sample 29:
+    time = 2575000
+    flags = 1
+    data = length 336, hash 72B647FF
+  sample 30:
+    time = 2599000
+    flags = 1
+    data = length 336, hash 8F740AB1
+  sample 31:
+    time = 2623000
+    flags = 1
+    data = length 336, hash 5E3C7E93
+  sample 32:
+    time = 2647000
+    flags = 1
+    data = length 336, hash 121B913B
+  sample 33:
+    time = 2671000
+    flags = 1
+    data = length 336, hash 578FCCF2
+  sample 34:
+    time = 2695000
+    flags = 1
+    data = length 336, hash 5B5823DE
+  sample 35:
+    time = 2719000
+    flags = 1
+    data = length 384, hash D8B83F78
+  sample 36:
+    time = 2743000
+    flags = 1
+    data = length 240, hash E649682F
+  sample 37:
+    time = 2767000
+    flags = 1
+    data = length 96, hash C559A6F4
+  sample 38:
+    time = 2791000
+    flags = 1
+    data = length 96, hash 792796BC
+  sample 39:
+    time = 2815000
+    flags = 1
+    data = length 120, hash 8172CD0E
+  sample 40:
+    time = 2839000
+    flags = 1
+    data = length 120, hash F562B52F
+  sample 41:
+    time = 2863000
+    flags = 1
+    data = length 96, hash FF8D5B98
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp3/bear-id3-enabled.3.dump b/tree/testdata/src/test/assets/mp3/bear-id3-enabled.3.dump
new file mode 100644
index 0000000..2209656
--- /dev/null
+++ b/tree/testdata/src/test/assets/mp3/bear-id3-enabled.3.dump
@@ -0,0 +1,20 @@
+seekMap:
+  isSeekable = true
+  duration = 2808000
+  getPosition(0) = [[timeUs=0, position=39740]]
+  getPosition(1) = [[timeUs=0, position=39740]]
+  getPosition(1404000) = [[timeUs=1404000, position=58820]]
+  getPosition(2808000) = [[timeUs=2808000, position=77900]]
+numberOfTracks = 1
+track 0:
+  total output bytes = 0
+  sample count = 0
+  format 0:
+    sampleMimeType = audio/mpeg
+    maxInputSize = 4096
+    channelCount = 2
+    sampleRate = 48000
+    encoderDelay = 576
+    encoderPadding = 576
+    metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description]
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp3/bear-id3-enabled.unknown_length.dump b/tree/testdata/src/test/assets/mp3/bear-id3-enabled.unknown_length.dump
new file mode 100644
index 0000000..c252057
--- /dev/null
+++ b/tree/testdata/src/test/assets/mp3/bear-id3-enabled.unknown_length.dump
@@ -0,0 +1,488 @@
+seekMap:
+  isSeekable = true
+  duration = 2808000
+  getPosition(0) = [[timeUs=0, position=39740]]
+  getPosition(1) = [[timeUs=0, position=39740]]
+  getPosition(1404000) = [[timeUs=1404000, position=58820]]
+  getPosition(2808000) = [[timeUs=2808000, position=77900]]
+numberOfTracks = 1
+track 0:
+  total output bytes = 38160
+  sample count = 117
+  format 0:
+    sampleMimeType = audio/mpeg
+    maxInputSize = 4096
+    channelCount = 2
+    sampleRate = 48000
+    encoderDelay = 576
+    encoderPadding = 576
+    metadata = entries=[TIT2: description=null: value=Test title, TPE1: description=null: value=Test Artist, TALB: description=null: value=Test Album, TXXX: description=Test description: value=Test user info, COMM: language=eng, description=Test description, WXXX: url=Test URL, TSSE: description=null: value=Lavf58.29.100, MLLT, PRIV: owner=test@gmail.com, UNKN, GEOB: mimeType=test/mime, filename=Testfilename.txt, description=Test description, CHAP, CHAP, CTOC, APIC: mimeType=image/jpeg, description=Test description]
+  sample 0:
+    time = 0
+    flags = 1
+    data = length 96, hash 1F161542
+  sample 1:
+    time = 24000
+    flags = 1
+    data = length 768, hash CD1DC50F
+  sample 2:
+    time = 48000
+    flags = 1
+    data = length 336, hash 3F64124B
+  sample 3:
+    time = 72000
+    flags = 1
+    data = length 336, hash 8FFED94E
+  sample 4:
+    time = 96000
+    flags = 1
+    data = length 288, hash 9CD77D47
+  sample 5:
+    time = 120000
+    flags = 1
+    data = length 384, hash 24607BB5
+  sample 6:
+    time = 144000
+    flags = 1
+    data = length 480, hash 4937EBAB
+  sample 7:
+    time = 168000
+    flags = 1
+    data = length 336, hash 546342B1
+  sample 8:
+    time = 192000
+    flags = 1
+    data = length 336, hash 79E0923F
+  sample 9:
+    time = 216000
+    flags = 1
+    data = length 336, hash AB1F3948
+  sample 10:
+    time = 240000
+    flags = 1
+    data = length 336, hash C3A4D888
+  sample 11:
+    time = 264000
+    flags = 1
+    data = length 288, hash 7867DA45
+  sample 12:
+    time = 288000
+    flags = 1
+    data = length 336, hash B1240B73
+  sample 13:
+    time = 312000
+    flags = 1
+    data = length 336, hash 94CFCD35
+  sample 14:
+    time = 336000
+    flags = 1
+    data = length 288, hash 94F412C
+  sample 15:
+    time = 360000
+    flags = 1
+    data = length 336, hash A1D9FF41
+  sample 16:
+    time = 384000
+    flags = 1
+    data = length 288, hash 2A8DA21B
+  sample 17:
+    time = 408000
+    flags = 1
+    data = length 336, hash 6A429CE
+  sample 18:
+    time = 432000
+    flags = 1
+    data = length 336, hash 68853982
+  sample 19:
+    time = 456000
+    flags = 1
+    data = length 384, hash 1D6F779C
+  sample 20:
+    time = 480000
+    flags = 1
+    data = length 480, hash 6B31EBEE
+  sample 21:
+    time = 504000
+    flags = 1
+    data = length 336, hash 888335BE
+  sample 22:
+    time = 528000
+    flags = 1
+    data = length 336, hash 6072AC8B
+  sample 23:
+    time = 552000
+    flags = 1
+    data = length 336, hash C9D24234
+  sample 24:
+    time = 576000
+    flags = 1
+    data = length 288, hash 52BF4D1E
+  sample 25:
+    time = 600000
+    flags = 1
+    data = length 336, hash F93F4F0
+  sample 26:
+    time = 624000
+    flags = 1
+    data = length 336, hash 8617688A
+  sample 27:
+    time = 648000
+    flags = 1
+    data = length 480, hash FAB0D31B
+  sample 28:
+    time = 672000
+    flags = 1
+    data = length 384, hash FA4B53E2
+  sample 29:
+    time = 696000
+    flags = 1
+    data = length 336, hash 8C435F6A
+  sample 30:
+    time = 720000
+    flags = 1
+    data = length 336, hash 60D3F80C
+  sample 31:
+    time = 744000
+    flags = 1
+    data = length 336, hash DC15B68B
+  sample 32:
+    time = 768000
+    flags = 1
+    data = length 288, hash FF3DF141
+  sample 33:
+    time = 792000
+    flags = 1
+    data = length 336, hash A64B3042
+  sample 34:
+    time = 816000
+    flags = 1
+    data = length 336, hash ACA622A1
+  sample 35:
+    time = 840000
+    flags = 1
+    data = length 288, hash 3E34B8D4
+  sample 36:
+    time = 864000
+    flags = 1
+    data = length 288, hash 9B96F72A
+  sample 37:
+    time = 888000
+    flags = 1
+    data = length 336, hash E917C122
+  sample 38:
+    time = 912000
+    flags = 1
+    data = length 336, hash 10ED1470
+  sample 39:
+    time = 936000
+    flags = 1
+    data = length 288, hash 706B8A7C
+  sample 40:
+    time = 960000
+    flags = 1
+    data = length 336, hash 71FFE4A0
+  sample 41:
+    time = 984000
+    flags = 1
+    data = length 336, hash D4160463
+  sample 42:
+    time = 1008000
+    flags = 1
+    data = length 336, hash EC557B14
+  sample 43:
+    time = 1032000
+    flags = 1
+    data = length 288, hash 5598CF8B
+  sample 44:
+    time = 1056000
+    flags = 1
+    data = length 336, hash 7E0AB41
+  sample 45:
+    time = 1080000
+    flags = 1
+    data = length 336, hash 1C585FEF
+  sample 46:
+    time = 1104000
+    flags = 1
+    data = length 336, hash A4A4855E
+  sample 47:
+    time = 1128000
+    flags = 1
+    data = length 336, hash CECA51D3
+  sample 48:
+    time = 1152000
+    flags = 1
+    data = length 288, hash 2D362DC5
+  sample 49:
+    time = 1176000
+    flags = 1
+    data = length 336, hash 9EB2609D
+  sample 50:
+    time = 1200000
+    flags = 1
+    data = length 336, hash 28FFB3FE
+  sample 51:
+    time = 1224000
+    flags = 1
+    data = length 288, hash 2AA2D216
+  sample 52:
+    time = 1248000
+    flags = 1
+    data = length 336, hash CDBC7032
+  sample 53:
+    time = 1272000
+    flags = 1
+    data = length 336, hash 25B13FE7
+  sample 54:
+    time = 1296000
+    flags = 1
+    data = length 336, hash DB6BB1E
+  sample 55:
+    time = 1320000
+    flags = 1
+    data = length 336, hash EBE951F4
+  sample 56:
+    time = 1344000
+    flags = 1
+    data = length 288, hash 9E2EBFF7
+  sample 57:
+    time = 1368000
+    flags = 1
+    data = length 336, hash 36A7D455
+  sample 58:
+    time = 1392000
+    flags = 1
+    data = length 336, hash 84545F8C
+  sample 59:
+    time = 1416000
+    flags = 1
+    data = length 336, hash F66F3045
+  sample 60:
+    time = 1440000
+    flags = 1
+    data = length 576, hash 5AB089EA
+  sample 61:
+    time = 1464000
+    flags = 1
+    data = length 336, hash 8868086
+  sample 62:
+    time = 1488000
+    flags = 1
+    data = length 336, hash D5EB6D63
+  sample 63:
+    time = 1512000
+    flags = 1
+    data = length 288, hash 7A5374B7
+  sample 64:
+    time = 1536000
+    flags = 1
+    data = length 336, hash BEB27A75
+  sample 65:
+    time = 1560000
+    flags = 1
+    data = length 336, hash E251E0FD
+  sample 66:
+    time = 1584000
+    flags = 1
+    data = length 288, hash D54C970
+  sample 67:
+    time = 1608000
+    flags = 1
+    data = length 336, hash 52C473B9
+  sample 68:
+    time = 1632000
+    flags = 1
+    data = length 336, hash F5F13334
+  sample 69:
+    time = 1656000
+    flags = 1
+    data = length 480, hash A5F1E987
+  sample 70:
+    time = 1680000
+    flags = 1
+    data = length 288, hash 453A1267
+  sample 71:
+    time = 1704000
+    flags = 1
+    data = length 288, hash 7C6C2EA9
+  sample 72:
+    time = 1728000
+    flags = 1
+    data = length 336, hash F4BFECA4
+  sample 73:
+    time = 1752000
+    flags = 1
+    data = length 336, hash 751A395A
+  sample 74:
+    time = 1776000
+    flags = 1
+    data = length 336, hash EE38DB02
+  sample 75:
+    time = 1800000
+    flags = 1
+    data = length 336, hash F18837E2
+  sample 76:
+    time = 1824000
+    flags = 1
+    data = length 336, hash ED36B78E
+  sample 77:
+    time = 1848000
+    flags = 1
+    data = length 336, hash B3D28289
+  sample 78:
+    time = 1872000
+    flags = 1
+    data = length 288, hash 8BDE28E1
+  sample 79:
+    time = 1896000
+    flags = 1
+    data = length 336, hash CFD5E966
+  sample 80:
+    time = 1920000
+    flags = 1
+    data = length 288, hash DC08E267
+  sample 81:
+    time = 1944000
+    flags = 1
+    data = length 336, hash 6530CB78
+  sample 82:
+    time = 1968000
+    flags = 1
+    data = length 336, hash 6CC6636E
+  sample 83:
+    time = 1992000
+    flags = 1
+    data = length 336, hash 613047C1
+  sample 84:
+    time = 2016000
+    flags = 1
+    data = length 288, hash CDC747BF
+  sample 85:
+    time = 2040000
+    flags = 1
+    data = length 336, hash AF22AA74
+  sample 86:
+    time = 2064000
+    flags = 1
+    data = length 384, hash 82F326AA
+  sample 87:
+    time = 2088000
+    flags = 1
+    data = length 384, hash EDA26C4D
+  sample 88:
+    time = 2112000
+    flags = 1
+    data = length 336, hash 94C643DC
+  sample 89:
+    time = 2136000
+    flags = 1
+    data = length 288, hash CB5D9C40
+  sample 90:
+    time = 2160000
+    flags = 1
+    data = length 336, hash 1E69DE3F
+  sample 91:
+    time = 2184000
+    flags = 1
+    data = length 336, hash 7E472219
+  sample 92:
+    time = 2208000
+    flags = 1
+    data = length 336, hash DA47B9FA
+  sample 93:
+    time = 2232000
+    flags = 1
+    data = length 336, hash DD0ABB7C
+  sample 94:
+    time = 2256000
+    flags = 1
+    data = length 288, hash DBF93FAC
+  sample 95:
+    time = 2280000
+    flags = 1
+    data = length 336, hash 243F4B2
+  sample 96:
+    time = 2304000
+    flags = 1
+    data = length 336, hash 2E881490
+  sample 97:
+    time = 2328000
+    flags = 1
+    data = length 288, hash 1C28C8BE
+  sample 98:
+    time = 2352000
+    flags = 1
+    data = length 336, hash C73E5D30
+  sample 99:
+    time = 2376000
+    flags = 1
+    data = length 288, hash 98B5BFF6
+  sample 100:
+    time = 2400000
+    flags = 1
+    data = length 336, hash E0135533
+  sample 101:
+    time = 2424000
+    flags = 1
+    data = length 336, hash D13C9DBC
+  sample 102:
+    time = 2448000
+    flags = 1
+    data = length 336, hash 63D524CA
+  sample 103:
+    time = 2472000
+    flags = 1
+    data = length 288, hash A28514C3
+  sample 104:
+    time = 2496000
+    flags = 1
+    data = length 336, hash 72B647FF
+  sample 105:
+    time = 2520000
+    flags = 1
+    data = length 336, hash 8F740AB1
+  sample 106:
+    time = 2544000
+    flags = 1
+    data = length 336, hash 5E3C7E93
+  sample 107:
+    time = 2568000
+    flags = 1
+    data = length 336, hash 121B913B
+  sample 108:
+    time = 2592000
+    flags = 1
+    data = length 336, hash 578FCCF2
+  sample 109:
+    time = 2616000
+    flags = 1
+    data = length 336, hash 5B5823DE
+  sample 110:
+    time = 2640000
+    flags = 1
+    data = length 384, hash D8B83F78
+  sample 111:
+    time = 2664000
+    flags = 1
+    data = length 240, hash E649682F
+  sample 112:
+    time = 2688000
+    flags = 1
+    data = length 96, hash C559A6F4
+  sample 113:
+    time = 2712000
+    flags = 1
+    data = length 96, hash 792796BC
+  sample 114:
+    time = 2736000
+    flags = 1
+    data = length 120, hash 8172CD0E
+  sample 115:
+    time = 2760000
+    flags = 1
+    data = length 120, hash F562B52F
+  sample 116:
+    time = 2784000
+    flags = 1
+    data = length 96, hash FF8D5B98
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp3/bear-id3.mp3 b/tree/testdata/src/test/assets/mp3/bear-id3.mp3
new file mode 100644
index 0000000..9bd4f72
--- /dev/null
+++ b/tree/testdata/src/test/assets/mp3/bear-id3.mp3
Binary files differ
diff --git a/tree/testdata/src/test/assets/mpd/sample_mpd_trick_play b/tree/testdata/src/test/assets/mpd/sample_mpd_trick_play
new file mode 100644
index 0000000..b35c906
--- /dev/null
+++ b/tree/testdata/src/test/assets/mpd/sample_mpd_trick_play
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<MPD type="static" duration="1s" mediaPresentationDuration="PT1S">
+ <Period>
+  <SegmentTemplate startNumber="0" timescale="1000" media="sq/$Number$">
+   <SegmentTimeline>
+    <S d="1000"/>
+   </SegmentTimeline>
+  </SegmentTemplate>
+  <AdaptationSet id="0" mimeType="video/mp4" subsegmentAlignment="true">
+   <Representation id="0" codecs="avc1.42c01f" bandwidth="128000">
+    <BaseURL>https://test.com/0</BaseURL>
+   </Representation>
+  </AdaptationSet>
+  <AdaptationSet id="1" mimeType="video/mp4" subsegmentAlignment="true">
+   <Representation id="0" codecs="avc1.42c01f" bandwidth="128000">
+    <BaseURL>https://test.com/0</BaseURL>
+   </Representation>
+  </AdaptationSet>
+  <AdaptationSet id="2" mimeType="video/mp4" subsegmentAlignment="true">
+   <EssentialProperty schemeIdUri="http://dashif.org/guidelines/trickmode" value="0"/>
+   <Representation id="0" codecs="avc1.42c01f" bandwidth="128000">
+    <BaseURL>https://test.com/0</BaseURL>
+   </Representation>
+  </AdaptationSet>
+  <AdaptationSet id="3" mimeType="video/mp4" subsegmentAlignment="true">
+   <SupplementalProperty schemeIdUri="http://dashif.org/guidelines/trickmode" value="1"/>
+   <Representation id="0" codecs="avc1.42c01f" bandwidth="128000">
+    <BaseURL>https://test.com/0</BaseURL>
+   </Representation>
+  </AdaptationSet>
+ </Period>
+</MPD>
diff --git a/tree/testdata/src/test/assets/ts/sample_h265.ts b/tree/testdata/src/test/assets/ts/sample_h265.ts
new file mode 100644
index 0000000..483010f
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h265.ts
Binary files differ
diff --git a/tree/testdata/src/test/assets/ts/sample_h265.ts.0.dump b/tree/testdata/src/test/assets/ts/sample_h265.ts.0.dump
new file mode 100644
index 0000000..1c06207
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h265.ts.0.dump
@@ -0,0 +1,141 @@
+seekMap:
+  isSeekable = true
+  duration = 900000
+  getPosition(0) = [[timeUs=0, position=0]]
+  getPosition(1) = [[timeUs=1, position=0]]
+  getPosition(450000) = [[timeUs=450000, position=11421]]
+  getPosition(900000) = [[timeUs=900000, position=23030]]
+numberOfTracks = 2
+track 256:
+  total output bytes = 19364
+  sample count = 29
+  format 0:
+    id = 1/256
+    sampleMimeType = video/hevc
+    width = 854
+    height = 480
+    initializationData:
+      data = length 83, hash 7F428
+  sample 0:
+    time = 66666
+    flags = 1
+    data = length 2510, hash 796A98BE
+  sample 1:
+    time = 100000
+    flags = 0
+    data = length 1219, hash 131AA4E4
+  sample 2:
+    time = 266666
+    flags = 0
+    data = length 7810, hash 3F881DB9
+  sample 3:
+    time = 200000
+    flags = 0
+    data = length 2306, hash 9A77959C
+  sample 4:
+    time = 133333
+    flags = 0
+    data = length 1058, hash B887F7EF
+  sample 5:
+    time = 166666
+    flags = 0
+    data = length 98, hash D95BF6E3
+  sample 6:
+    time = 233333
+    flags = 0
+    data = length 61, hash 574C41C3
+  sample 7:
+    time = 433333
+    flags = 0
+    data = length 296, hash E92DB288
+  sample 8:
+    time = 366666
+    flags = 0
+    data = length 137, hash 586DADD6
+  sample 9:
+    time = 300000
+    flags = 0
+    data = length 218, hash 91E82C9F
+  sample 10:
+    time = 333333
+    flags = 0
+    data = length 177, hash 4A4FEEC0
+  sample 11:
+    time = 400000
+    flags = 0
+    data = length 82, hash 2E2ADD8
+  sample 12:
+    time = 533333
+    flags = 0
+    data = length 290, hash 63CF7D90
+  sample 13:
+    time = 500000
+    flags = 0
+    data = length 268, hash E8CBAC11
+  sample 14:
+    time = 466666
+    flags = 0
+    data = length 178, hash C5B1613E
+  sample 15:
+    time = 566666
+    flags = 0
+    data = length 271, hash 76652FC5
+  sample 16:
+    time = 733333
+    flags = 0
+    data = length 257, hash 960B5DF4
+  sample 17:
+    time = 666666
+    flags = 0
+    data = length 206, hash 87358D00
+  sample 18:
+    time = 600000
+    flags = 0
+    data = length 130, hash 4D7A038D
+  sample 19:
+    time = 633333
+    flags = 0
+    data = length 114, hash 2B3C616E
+  sample 20:
+    time = 700000
+    flags = 0
+    data = length 95, hash 37D79559
+  sample 21:
+    time = 900000
+    flags = 0
+    data = length 233, hash 80308C9E
+  sample 22:
+    time = 833333
+    flags = 0
+    data = length 203, hash E70BA5F2
+  sample 23:
+    time = 766666
+    flags = 0
+    data = length 95, hash BA6FA2D3
+  sample 24:
+    time = 800000
+    flags = 0
+    data = length 103, hash 51291041
+  sample 25:
+    time = 866666
+    flags = 0
+    data = length 111, hash EE9DCFC9
+  sample 26:
+    time = 1033333
+    flags = 0
+    data = length 253, hash D0CEFBA7
+  sample 27:
+    time = 966666
+    flags = 0
+    data = length 134, hash 8802EEF0
+  sample 28:
+    time = 933333
+    flags = 0
+    data = length 80, hash C635D9C2
+track 8448:
+  total output bytes = 0
+  sample count = 0
+  format 0:
+    id = 1/8448
+    sampleMimeType = application/cea-608
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/ts/sample_h265.ts.1.dump b/tree/testdata/src/test/assets/ts/sample_h265.ts.1.dump
new file mode 100644
index 0000000..95d8d9b
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h265.ts.1.dump
@@ -0,0 +1,105 @@
+seekMap:
+  isSeekable = true
+  duration = 900000
+  getPosition(0) = [[timeUs=0, position=0]]
+  getPosition(1) = [[timeUs=1, position=0]]
+  getPosition(450000) = [[timeUs=450000, position=11421]]
+  getPosition(900000) = [[timeUs=900000, position=23030]]
+numberOfTracks = 2
+track 256:
+  total output bytes = 3806
+  sample count = 20
+  format 0:
+    id = 1/256
+    sampleMimeType = video/hevc
+    width = 854
+    height = 480
+    initializationData:
+      data = length 83, hash 7F428
+  sample 0:
+    time = 300000
+    flags = 0
+    data = length 218, hash 91E82C9F
+  sample 1:
+    time = 333333
+    flags = 0
+    data = length 177, hash 4A4FEEC0
+  sample 2:
+    time = 400000
+    flags = 0
+    data = length 82, hash 2E2ADD8
+  sample 3:
+    time = 533333
+    flags = 0
+    data = length 290, hash 63CF7D90
+  sample 4:
+    time = 500000
+    flags = 0
+    data = length 268, hash E8CBAC11
+  sample 5:
+    time = 466666
+    flags = 0
+    data = length 178, hash C5B1613E
+  sample 6:
+    time = 566666
+    flags = 0
+    data = length 271, hash 76652FC5
+  sample 7:
+    time = 733333
+    flags = 0
+    data = length 257, hash 960B5DF4
+  sample 8:
+    time = 666666
+    flags = 0
+    data = length 206, hash 87358D00
+  sample 9:
+    time = 600000
+    flags = 0
+    data = length 130, hash 4D7A038D
+  sample 10:
+    time = 633333
+    flags = 0
+    data = length 114, hash 2B3C616E
+  sample 11:
+    time = 700000
+    flags = 0
+    data = length 95, hash 37D79559
+  sample 12:
+    time = 900000
+    flags = 0
+    data = length 233, hash 80308C9E
+  sample 13:
+    time = 833333
+    flags = 0
+    data = length 203, hash E70BA5F2
+  sample 14:
+    time = 766666
+    flags = 0
+    data = length 95, hash BA6FA2D3
+  sample 15:
+    time = 800000
+    flags = 0
+    data = length 103, hash 51291041
+  sample 16:
+    time = 866666
+    flags = 0
+    data = length 111, hash EE9DCFC9
+  sample 17:
+    time = 1033333
+    flags = 0
+    data = length 253, hash D0CEFBA7
+  sample 18:
+    time = 966666
+    flags = 0
+    data = length 134, hash 8802EEF0
+  sample 19:
+    time = 933333
+    flags = 0
+    data = length 80, hash C635D9C2
+track 8448:
+  total output bytes = 0
+  sample count = 0
+  format 0:
+    id = 1/8448
+    sampleMimeType = application/cea-608
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/ts/sample_h265.ts.2.dump b/tree/testdata/src/test/assets/ts/sample_h265.ts.2.dump
new file mode 100644
index 0000000..9080f5c
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h265.ts.2.dump
@@ -0,0 +1,69 @@
+seekMap:
+  isSeekable = true
+  duration = 900000
+  getPosition(0) = [[timeUs=0, position=0]]
+  getPosition(1) = [[timeUs=1, position=0]]
+  getPosition(450000) = [[timeUs=450000, position=11421]]
+  getPosition(900000) = [[timeUs=900000, position=23030]]
+numberOfTracks = 2
+track 256:
+  total output bytes = 1796
+  sample count = 11
+  format 0:
+    id = 1/256
+    sampleMimeType = video/hevc
+    width = 854
+    height = 480
+    initializationData:
+      data = length 83, hash 7F428
+  sample 0:
+    time = 600000
+    flags = 0
+    data = length 130, hash 4D7A038D
+  sample 1:
+    time = 633333
+    flags = 0
+    data = length 114, hash 2B3C616E
+  sample 2:
+    time = 700000
+    flags = 0
+    data = length 95, hash 37D79559
+  sample 3:
+    time = 900000
+    flags = 0
+    data = length 233, hash 80308C9E
+  sample 4:
+    time = 833333
+    flags = 0
+    data = length 203, hash E70BA5F2
+  sample 5:
+    time = 766666
+    flags = 0
+    data = length 95, hash BA6FA2D3
+  sample 6:
+    time = 800000
+    flags = 0
+    data = length 103, hash 51291041
+  sample 7:
+    time = 866666
+    flags = 0
+    data = length 111, hash EE9DCFC9
+  sample 8:
+    time = 1033333
+    flags = 0
+    data = length 253, hash D0CEFBA7
+  sample 9:
+    time = 966666
+    flags = 0
+    data = length 134, hash 8802EEF0
+  sample 10:
+    time = 933333
+    flags = 0
+    data = length 80, hash C635D9C2
+track 8448:
+  total output bytes = 0
+  sample count = 0
+  format 0:
+    id = 1/8448
+    sampleMimeType = application/cea-608
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/ts/sample_h265.ts.3.dump b/tree/testdata/src/test/assets/ts/sample_h265.ts.3.dump
new file mode 100644
index 0000000..5fe7bce
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h265.ts.3.dump
@@ -0,0 +1,33 @@
+seekMap:
+  isSeekable = true
+  duration = 900000
+  getPosition(0) = [[timeUs=0, position=0]]
+  getPosition(1) = [[timeUs=1, position=0]]
+  getPosition(450000) = [[timeUs=450000, position=11421]]
+  getPosition(900000) = [[timeUs=900000, position=23030]]
+numberOfTracks = 2
+track 256:
+  total output bytes = 396
+  sample count = 2
+  format 0:
+    id = 1/256
+    sampleMimeType = video/hevc
+    width = 854
+    height = 480
+    initializationData:
+      data = length 83, hash 7F428
+  sample 0:
+    time = 966666
+    flags = 0
+    data = length 134, hash 8802EEF0
+  sample 1:
+    time = 933333
+    flags = 0
+    data = length 80, hash C635D9C2
+track 8448:
+  total output bytes = 0
+  sample count = 0
+  format 0:
+    id = 1/8448
+    sampleMimeType = application/cea-608
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/ts/sample_h265.ts.unknown_length.dump b/tree/testdata/src/test/assets/ts/sample_h265.ts.unknown_length.dump
new file mode 100644
index 0000000..733be6f
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h265.ts.unknown_length.dump
@@ -0,0 +1,138 @@
+seekMap:
+  isSeekable = false
+  duration = UNSET TIME
+  getPosition(0) = [[timeUs=0, position=0]]
+numberOfTracks = 2
+track 256:
+  total output bytes = 19364
+  sample count = 29
+  format 0:
+    id = 1/256
+    sampleMimeType = video/hevc
+    width = 854
+    height = 480
+    initializationData:
+      data = length 83, hash 7F428
+  sample 0:
+    time = 66666
+    flags = 1
+    data = length 2510, hash 796A98BE
+  sample 1:
+    time = 100000
+    flags = 0
+    data = length 1219, hash 131AA4E4
+  sample 2:
+    time = 266666
+    flags = 0
+    data = length 7810, hash 3F881DB9
+  sample 3:
+    time = 200000
+    flags = 0
+    data = length 2306, hash 9A77959C
+  sample 4:
+    time = 133333
+    flags = 0
+    data = length 1058, hash B887F7EF
+  sample 5:
+    time = 166666
+    flags = 0
+    data = length 98, hash D95BF6E3
+  sample 6:
+    time = 233333
+    flags = 0
+    data = length 61, hash 574C41C3
+  sample 7:
+    time = 433333
+    flags = 0
+    data = length 296, hash E92DB288
+  sample 8:
+    time = 366666
+    flags = 0
+    data = length 137, hash 586DADD6
+  sample 9:
+    time = 300000
+    flags = 0
+    data = length 218, hash 91E82C9F
+  sample 10:
+    time = 333333
+    flags = 0
+    data = length 177, hash 4A4FEEC0
+  sample 11:
+    time = 400000
+    flags = 0
+    data = length 82, hash 2E2ADD8
+  sample 12:
+    time = 533333
+    flags = 0
+    data = length 290, hash 63CF7D90
+  sample 13:
+    time = 500000
+    flags = 0
+    data = length 268, hash E8CBAC11
+  sample 14:
+    time = 466666
+    flags = 0
+    data = length 178, hash C5B1613E
+  sample 15:
+    time = 566666
+    flags = 0
+    data = length 271, hash 76652FC5
+  sample 16:
+    time = 733333
+    flags = 0
+    data = length 257, hash 960B5DF4
+  sample 17:
+    time = 666666
+    flags = 0
+    data = length 206, hash 87358D00
+  sample 18:
+    time = 600000
+    flags = 0
+    data = length 130, hash 4D7A038D
+  sample 19:
+    time = 633333
+    flags = 0
+    data = length 114, hash 2B3C616E
+  sample 20:
+    time = 700000
+    flags = 0
+    data = length 95, hash 37D79559
+  sample 21:
+    time = 900000
+    flags = 0
+    data = length 233, hash 80308C9E
+  sample 22:
+    time = 833333
+    flags = 0
+    data = length 203, hash E70BA5F2
+  sample 23:
+    time = 766666
+    flags = 0
+    data = length 95, hash BA6FA2D3
+  sample 24:
+    time = 800000
+    flags = 0
+    data = length 103, hash 51291041
+  sample 25:
+    time = 866666
+    flags = 0
+    data = length 111, hash EE9DCFC9
+  sample 26:
+    time = 1033333
+    flags = 0
+    data = length 253, hash D0CEFBA7
+  sample 27:
+    time = 966666
+    flags = 0
+    data = length 134, hash 8802EEF0
+  sample 28:
+    time = 933333
+    flags = 0
+    data = length 80, hash C635D9C2
+track 8448:
+  total output bytes = 0
+  sample count = 0
+  format 0:
+    id = 1/8448
+    sampleMimeType = application/cea-608
+tracksEnded = true
diff --git a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java
index dff69c1..269a992 100644
--- a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java
+++ b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java
@@ -236,86 +236,6 @@
     return extractorOutput;
   }
 
-  /**
-   * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all
-   * possible combinations of "simulate" parameters.
-   *
-   * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
-   *     class which is to be tested.
-   * @param sampleFile The path to the input sample.
-   * @param context To be used to load the sample file.
-   * @param expectedThrowable Expected {@link Throwable} class.
-   * @throws IOException If reading from the input fails.
-   * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)
-   */
-  public static void assertThrows(
-      ExtractorFactory factory,
-      String sampleFile,
-      Context context,
-      Class<? extends Throwable> expectedThrowable)
-      throws IOException {
-    byte[] fileData = TestUtil.getByteArray(context, sampleFile);
-    assertThrows(factory, fileData, expectedThrowable);
-  }
-
-  /**
-   * Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all
-   * possible combinations of "simulate" parameters.
-   *
-   * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
-   *     class which is to be tested.
-   * @param fileData Content of the input file.
-   * @param expectedThrowable Expected {@link Throwable} class.
-   * @throws IOException If reading from the input fails.
-   * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)
-   */
-  private static void assertThrows(
-      ExtractorFactory factory, byte[] fileData, Class<? extends Throwable> expectedThrowable)
-      throws IOException {
-    assertThrows(factory.create(), fileData, expectedThrowable, false, false, false);
-    assertThrows(factory.create(), fileData, expectedThrowable,  true, false, false);
-    assertThrows(factory.create(), fileData, expectedThrowable, false,  true, false);
-    assertThrows(factory.create(), fileData, expectedThrowable,  true,  true, false);
-    assertThrows(factory.create(), fileData, expectedThrowable, false, false,  true);
-    assertThrows(factory.create(), fileData, expectedThrowable,  true, false,  true);
-    assertThrows(factory.create(), fileData, expectedThrowable, false,  true,  true);
-    assertThrows(factory.create(), fileData, expectedThrowable,  true,  true,  true);
-  }
-
-  /**
-   * Asserts {@code extractor} throws {@code expectedThrowable} while consuming {@code fileData}.
-   *
-   * @param extractor The {@link Extractor} to be tested.
-   * @param fileData Content of the input file.
-   * @param expectedThrowable Expected {@link Throwable} class.
-   * @param simulateIOErrors If true simulates IOErrors.
-   * @param simulateUnknownLength If true simulates unknown input length.
-   * @param simulatePartialReads If true simulates partial reads.
-   * @throws IOException If reading from the input fails.
-   */
-  private static void assertThrows(
-      Extractor extractor,
-      byte[] fileData,
-      Class<? extends Throwable> expectedThrowable,
-      boolean simulateIOErrors,
-      boolean simulateUnknownLength,
-      boolean simulatePartialReads)
-      throws IOException {
-    FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData)
-        .setSimulateIOErrors(simulateIOErrors)
-        .setSimulateUnknownLength(simulateUnknownLength)
-        .setSimulatePartialReads(simulatePartialReads).build();
-    try {
-      consumeTestData(extractor, input, 0, true);
-      throw new AssertionError(expectedThrowable.getSimpleName() + " expected but not thrown");
-    } catch (Throwable throwable) {
-      if (expectedThrowable.equals(throwable.getClass())) {
-        return; // Pass!
-      }
-      throw throwable;
-    }
-  }
-
   private ExtractorAsserts() {}
 
   private static FakeExtractorOutput consumeTestData(
diff --git a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java
index c3c4d5c..ee30b90 100644
--- a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java
+++ b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java
@@ -18,6 +18,7 @@
 import android.os.Handler.Callback;
 import android.os.Looper;
 import android.os.Message;
+import androidx.annotation.GuardedBy;
 import androidx.annotation.Nullable;
 import com.google.android.exoplayer2.util.Clock;
 import com.google.android.exoplayer2.util.HandlerWrapper;
@@ -29,16 +30,31 @@
 
   private final List<Long> wakeUpTimes;
   private final List<HandlerMessageData> handlerMessages;
+  private final long bootTimeMs;
 
-  private long currentTimeMs;
+  @GuardedBy("this")
+  private long timeSinceBootMs;
 
   /**
-   * Create {@link FakeClock} with an arbitrary initial timestamp.
+   * Creates a fake clock assuming the system was booted exactly at time {@code 0} (the Unix Epoch)
+   * and {@code initialTimeMs} milliseconds have passed since system boot.
    *
-   * @param initialTimeMs Initial timestamp in milliseconds.
+   * @param initialTimeMs The initial elapsed time since the boot time, in milliseconds.
    */
   public FakeClock(long initialTimeMs) {
-    this.currentTimeMs = initialTimeMs;
+    this(/* bootTimeMs= */ 0, initialTimeMs);
+  }
+
+  /**
+   * Creates a fake clock specifying when the system was booted and how much time has passed since
+   * then.
+   *
+   * @param bootTimeMs The time the system was booted since the Unix Epoch, in milliseconds.
+   * @param initialTimeMs The initial elapsed time since the boot time, in milliseconds.
+   */
+  public FakeClock(long bootTimeMs, long initialTimeMs) {
+    this.bootTimeMs = bootTimeMs;
+    this.timeSinceBootMs = initialTimeMs;
     this.wakeUpTimes = new ArrayList<>();
     this.handlerMessages = new ArrayList<>();
   }
@@ -49,23 +65,28 @@
    * @param timeDiffMs The amount of time to add to the timestamp in milliseconds.
    */
   public synchronized void advanceTime(long timeDiffMs) {
-    currentTimeMs += timeDiffMs;
+    timeSinceBootMs += timeDiffMs;
     for (Long wakeUpTime : wakeUpTimes) {
-      if (wakeUpTime <= currentTimeMs) {
+      if (wakeUpTime <= timeSinceBootMs) {
         notifyAll();
         break;
       }
     }
     for (int i = handlerMessages.size() - 1; i >= 0; i--) {
-      if (handlerMessages.get(i).maybeSendToTarget(currentTimeMs)) {
+      if (handlerMessages.get(i).maybeSendToTarget(timeSinceBootMs)) {
         handlerMessages.remove(i);
       }
     }
   }
 
   @Override
+  public synchronized long currentTimeMillis() {
+    return bootTimeMs + timeSinceBootMs;
+  }
+
+  @Override
   public synchronized long elapsedRealtime() {
-    return currentTimeMs;
+    return timeSinceBootMs;
   }
 
   @Override
@@ -78,9 +99,9 @@
     if (sleepTimeMs <= 0) {
       return;
     }
-    Long wakeUpTimeMs = currentTimeMs + sleepTimeMs;
+    Long wakeUpTimeMs = timeSinceBootMs + sleepTimeMs;
     wakeUpTimes.add(wakeUpTimeMs);
-    while (currentTimeMs < wakeUpTimeMs) {
+    while (timeSinceBootMs < wakeUpTimeMs) {
       try {
         wait();
       } catch (InterruptedException e) {
@@ -98,7 +119,7 @@
   /** Adds a handler post to list of pending messages. */
   protected synchronized boolean addHandlerMessageAtTime(
       HandlerWrapper handler, Runnable runnable, long timeMs) {
-    if (timeMs <= currentTimeMs) {
+    if (timeMs <= timeSinceBootMs) {
       return handler.post(runnable);
     }
     handlerMessages.add(new HandlerMessageData(timeMs, handler, runnable));
@@ -108,7 +129,7 @@
   /** Adds an empty handler message to list of pending messages. */
   protected synchronized boolean addHandlerMessageAtTime(
       HandlerWrapper handler, int message, long timeMs) {
-    if (timeMs <= currentTimeMs) {
+    if (timeMs <= timeSinceBootMs) {
       return handler.sendEmptyMessage(message);
     }
     handlerMessages.add(new HandlerMessageData(timeMs, handler, message));
diff --git a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java
index 46b5b5f..b3c6bf7 100644
--- a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java
+++ b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java
@@ -23,6 +23,7 @@
 import com.google.android.exoplayer2.extractor.TrackOutput;
 import com.google.android.exoplayer2.testutil.Dumper.Dumpable;
 import com.google.android.exoplayer2.upstream.DataReader;
+import com.google.android.exoplayer2.util.Assertions;
 import com.google.android.exoplayer2.util.Function;
 import com.google.android.exoplayer2.util.ParsableByteArray;
 import com.google.android.exoplayer2.util.Util;
@@ -44,6 +45,7 @@
 
   private byte[] sampleData;
   private int formatCount;
+  private boolean receivedSampleInFormat;
 
   @Nullable public Format lastFormat;
 
@@ -52,6 +54,7 @@
     dumpables = new ArrayList<>();
     sampleData = Util.EMPTY_BYTE_ARRAY;
     formatCount = 0;
+    receivedSampleInFormat = true;
   }
 
   public void clear() {
@@ -59,10 +62,12 @@
     dumpables.clear();
     sampleData = Util.EMPTY_BYTE_ARRAY;
     formatCount = 0;
+    receivedSampleInFormat = true;
   }
 
   @Override
   public void format(Format format) {
+    receivedSampleInFormat = false;
     addFormat(format);
   }
 
@@ -95,6 +100,7 @@
       int size,
       int offset,
       @Nullable CryptoData cryptoData) {
+    receivedSampleInFormat = true;
     if (lastFormat == null) {
       throw new IllegalStateException("TrackOutput must receive format before sampleMetadata");
     }
diff --git a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java
index d0dc1df..ad36635 100644
--- a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java
+++ b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java
@@ -41,6 +41,7 @@
 import java.lang.reflect.Method;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
 
 /**
  * Utilities to write unit/integration tests with a SimpleExoPlayer instance that uses fake
@@ -77,7 +78,7 @@
     @Nullable private Renderer[] renderers;
     @Nullable private RenderersFactory renderersFactory;
     private boolean useLazyPreparation;
-    private Looper looper;
+    private @MonotonicNonNull Looper looper;
 
     public Builder(Context context) {
       this.context = context;
@@ -85,7 +86,10 @@
       trackSelector = new DefaultTrackSelector(context);
       loadControl = new DefaultLoadControl();
       bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
-      looper = Assertions.checkNotNull(Looper.myLooper());
+      @Nullable Looper myLooper = Looper.myLooper();
+      if (myLooper != null) {
+        looper = myLooper;
+      }
     }
 
     /**
@@ -234,7 +238,11 @@
       return this;
     }
 
-    /** Returns the {@link Looper} that will be used by the player. */
+    /**
+     * Returns the {@link Looper} that will be used by the player, or null if no {@link Looper} has
+     * been set yet and no default is available.
+     */
+    @Nullable
     public Looper getLooper() {
       return looper;
     }
@@ -245,6 +253,8 @@
      * @return The built {@link ExoPlayerTestRunner}.
      */
     public SimpleExoPlayer build() {
+      Assertions.checkNotNull(
+          looper, "TestExoPlayer builder run on a thread without Looper and no Looper specified.");
       // Do not update renderersFactory and renderers here, otherwise their getters may
       // return different values before and after build() is called, making them confusing.
       RenderersFactory playerRenderersFactory = renderersFactory;
diff --git a/tree/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java b/tree/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java
index 666e45c..41cbf8a 100644
--- a/tree/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java
+++ b/tree/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java
@@ -36,6 +36,25 @@
   private static final long TIMEOUT_MS = 10000;
 
   @Test
+  public void currentTimeMillis_withoutBootTime() {
+    FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 10);
+    assertThat(fakeClock.currentTimeMillis()).isEqualTo(10);
+  }
+
+  @Test
+  public void currentTimeMillis_withBootTime() {
+    FakeClock fakeClock = new FakeClock(/* bootTimeMs= */ 150, /* initialTimeMs= */ 200);
+    assertThat(fakeClock.currentTimeMillis()).isEqualTo(350);
+  }
+
+  @Test
+  public void currentTimeMillis_advanceTime_currentTimeHasAdvanced() {
+    FakeClock fakeClock = new FakeClock(/* bootTimeMs= */ 100, /* initialTimeMs= */ 50);
+    fakeClock.advanceTime(/* timeDiffMs */ 250);
+    assertThat(fakeClock.currentTimeMillis()).isEqualTo(400);
+  }
+
+  @Test
   public void testAdvanceTime() {
     FakeClock fakeClock = new FakeClock(2000);
     assertThat(fakeClock.elapsedRealtime()).isEqualTo(2000);