Update ExoPlayer version to ToT am: 9af07bc62f am: 1ea0fd81d8 am: 728c5496af
Change-Id: I05831620a2afc19db3415143c70020cd67b36d48
diff --git a/METADATA b/METADATA
index 62bb7a0..bb88731 100644
--- a/METADATA
+++ b/METADATA
@@ -16,7 +16,7 @@
type: GIT
value: "https://github.com/google/ExoPlayer.git"
}
- version: "abadc768725929df0f4eb1ef0aacf53893e45d6d"
- last_upgrade_date { year: 2020 month: 4 day: 19 }
+ version: "2e9ed51503ba491a19f605e6994fa5839633c74f"
+ last_upgrade_date { year: 2020 month: 4 day: 29 }
license_type: NOTICE
}
\ No newline at end of file
diff --git a/tree/RELEASENOTES.md b/tree/RELEASENOTES.md
index 9daedd6..66641aa 100644
--- a/tree/RELEASENOTES.md
+++ b/tree/RELEASENOTES.md
@@ -26,6 +26,8 @@
consistency.
* Deprecate and rename `onLoadingChanged` to `onIsLoadingChanged` for
consistency.
+ * Deprecate `onSeekProcessed` because all seek changes happen instantly
+ now and listening to `onPositionDiscontinuity` is sufficient.
* Add `ExoPlayer.setPauseAtEndOfMediaItems` to let the player pause at the
end of each media item
([#5660](https://github.com/google/ExoPlayer/issues/5660)).
@@ -38,6 +40,8 @@
* Rename `MediaCodecRenderer.onOutputFormatChanged` to
`MediaCodecRenderer.onOutputMediaFormatChanged`, further clarifying the
distinction between `Format` and `MediaFormat`.
+ * Improve `Format` propagation within the media codec renderer
+ ([#6646](https://github.com/google/ExoPlayer/issues/6646)).
* Move player message-related constants from `C` to `Renderer`, to avoid
having the constants class depend on player/renderer classes.
* Split out `common` and `extractor` submodules.
@@ -60,8 +64,6 @@
`DecoderVideoRenderer` and `DecoderAudioRenderer` respectively, and
generalized to work with `Decoder` rather than `SimpleDecoder`.
* Add media item based playlist API to Player.
- * Update `CachedContentIndex` to use `SecureRandom` for generating the
- initialization vector used to encrypt the cache contents.
* Remove deprecated members in `DefaultTrackSelector`.
* Add `Player.DeviceComponent` and implement it for `SimpleExoPlayer` so
that the device volume can be controlled by player.
@@ -70,6 +72,11 @@
([#7207](https://github.com/google/ExoPlayer/issues/7207)).
* Add `SilenceMediaSource.Factory` to support tags
([PR #7245](https://github.com/google/ExoPlayer/pull/7245)).
+ * Fix `AdsMediaSource` child `MediaSource`s not being released.
+ * Parse track titles from Matroska files
+ ([#7247](https://github.com/google/ExoPlayer/pull/7247)).
+ * Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with
+ `CacheDataSink.Factory` and `CacheDataSource.Factory` respectively.
* Text:
* Parse `<ruby>` and `<rt>` tags in WebVTT subtitles (rendering is coming
later).
@@ -86,6 +93,17 @@
[issue #6581](https://github.com/google/ExoPlayer/issues/6581)).
* Parse `tts:ruby` and `tts:rubyPosition` properties in TTML subtitles
(rendering is coming later).
+ * Update WebVTT position alignment parsing to recognise `line-left`,
+ `center` and `line-right` as per the
+ [released spec](https://www.w3.org/TR/webvtt1/#webvtt-position-cue-setting)
+ (a
+ [previous draft](https://www.w3.org/TR/2014/WD-webvtt1-20141111/#dfn-webvtt-text-position-cue-setting)
+ used `start`, `middle` and `end`).
+ * Use anti-aliasing and bitmap filtering when displaying bitmap subtitles
+ ([#6950](https://github.com/google/ExoPlayer/pull/6950)).
+ * Implement timing-out of stuck CEA-608 captions (as permitted by
+ ANSI/CTA-608-E R-2014 Annex C.9) and set the default timeout to 16
+ seconds ([#7181](https://github.com/google/ExoPlayer/issues/7181)).
* DRM:
* Add support for attaching DRM sessions to clear content in the demo app.
* Remove `DrmSessionManager` references from all renderers.
@@ -95,8 +113,22 @@
`OfflineLicenseHelper`
([#7078](https://github.com/google/ExoPlayer/issues/7078)).
* Remove generics from DRM components.
-* Downloads: Merge downloads in `SegmentDownloader` to improve overall
- download speed ([#5978](https://github.com/google/ExoPlayer/issues/5978)).
+* Downloads and caching:
+ * Merge downloads in `SegmentDownloader` to improve overall download speed
+ ([#5978](https://github.com/google/ExoPlayer/issues/5978)).
+ * Replace `CacheDataSinkFactory` and `CacheDataSourceFactory` with
+ `CacheDataSink.Factory` and `CacheDataSource.Factory` respectively.
+ * Remove `DownloadConstructorHelper` and use `CacheDataSource.Factory`
+ directly instead.
+ * Update `CachedContentIndex` to use `SecureRandom` for generating the
+ initialization vector used to encrypt the cache contents.
+* DASH:
+ * Merge trick play adaptation sets (i.e., adaptation sets marked with
+ `http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as
+ the main adaptation sets to which they refer. Trick play tracks are
+ marked with the `C.ROLE_FLAG_TRICK_PLAY` flag.
+ * Fix assertion failure in `SampleQueue` when playing DASH streams with
+ EMSG tracks ([#7273](https://github.com/google/ExoPlayer/issues/7273)).
* MP3: Add `IndexSeeker` for accurate seeks in VBR streams
([#6787](https://github.com/google/ExoPlayer/issues/6787)). This seeker is
enabled by passing `FLAG_ENABLE_INDEX_SEEKING` to the `Mp3Extractor`. It may
@@ -104,6 +136,8 @@
costly on large files.
* MP4: Store the Android capture frame rate only in `Format.metadata`.
`Format.frameRate` now stores the calculated frame rate.
+* MPEG-TS: Fix issue where SEI NAL units were incorrectly dropped from H.265
+ samples ([#7113](https://github.com/google/ExoPlayer/issues/7113)).
* Testing
* Add `TestExoPlayer`, a utility class with APIs to create
`SimpleExoPlayer` instances with fake components for testing.
@@ -121,6 +155,8 @@
* Change the order of extractors for sniffing to reduce start-up latency in
`DefaultExtractorsFactory` and `DefaultHlsExtractorsFactory`
([#6410](https://github.com/google/ExoPlayer/issues/6410)).
+* Add missing `@Nullable` annotations to `MediaSessionConnector`
+ ([#7234](https://github.com/google/ExoPlayer/issues/7234)).
### 2.11.4 (2020-04-08)
diff --git a/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java
index bd74eb5..c36d370 100644
--- a/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java
+++ b/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java
@@ -22,18 +22,14 @@
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
-import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadManager;
-import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
-import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
-import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Log;
@@ -131,11 +127,9 @@
DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false);
upgradeActionFile(
DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ true);
- DownloaderConstructorHelper downloaderConstructorHelper =
- new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory());
downloadManager =
new DownloadManager(
- this, downloadIndex, new DefaultDownloaderFactory(downloaderConstructorHelper));
+ this, getDatabaseProvider(), getDownloadCache(), buildHttpDataSourceFactory());
downloadTracker =
new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadManager);
}
@@ -172,14 +166,12 @@
return downloadDirectory;
}
- protected static CacheDataSourceFactory buildReadOnlyCacheDataSource(
+ protected static CacheDataSource.Factory buildReadOnlyCacheDataSource(
DataSource.Factory upstreamFactory, Cache cache) {
- return new CacheDataSourceFactory(
- cache,
- upstreamFactory,
- new FileDataSource.Factory(),
- /* cacheWriteDataSinkFactory= */ null,
- CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
- /* eventListener= */ null);
+ return new CacheDataSource.Factory()
+ .setCache(cache)
+ .setUpstreamDataSourceFactory(upstreamFactory)
+ .setCacheWriteDataSinkFactory(null)
+ .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
}
}
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 2b79071..5199e1d 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
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.demo;
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+
import android.content.Context;
import android.content.DialogInterface;
import android.net.Uri;
@@ -23,8 +25,8 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.MediaItem;
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;
@@ -82,8 +84,8 @@
listeners.remove(listener);
}
- public boolean isDownloaded(Uri uri) {
- Download download = downloads.get(uri);
+ public boolean isDownloaded(MediaItem mediaItem) {
+ Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).sourceUri);
return download != null && download.state != Download.STATE_FAILED;
}
@@ -93,8 +95,8 @@
}
public void toggleDownload(
- FragmentManager fragmentManager, UriSample sample, RenderersFactory renderersFactory) {
- Download download = downloads.get(sample.uri);
+ FragmentManager fragmentManager, MediaItem mediaItem, RenderersFactory renderersFactory) {
+ Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).sourceUri);
if (download != null) {
DownloadService.sendRemoveDownload(
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
@@ -104,9 +106,7 @@
}
startDownloadDialogHelper =
new StartDownloadDialogHelper(
- fragmentManager,
- getDownloadHelper(sample.uri, sample.extension, renderersFactory),
- sample);
+ fragmentManager, getDownloadHelper(mediaItem, renderersFactory), mediaItem);
}
}
@@ -121,18 +121,24 @@
}
}
- private DownloadHelper getDownloadHelper(
- Uri uri, String extension, RenderersFactory renderersFactory) {
- int type = Util.inferContentType(uri, extension);
+ private DownloadHelper getDownloadHelper(MediaItem mediaItem, RenderersFactory renderersFactory) {
+ MediaItem.PlaybackProperties playbackProperties = checkNotNull(mediaItem.playbackProperties);
+ @C.ContentType
+ int type =
+ Util.inferContentTypeWithMimeType(
+ playbackProperties.sourceUri, playbackProperties.mimeType);
switch (type) {
case C.TYPE_DASH:
- return DownloadHelper.forDash(context, uri, dataSourceFactory, renderersFactory);
+ return DownloadHelper.forDash(
+ context, playbackProperties.sourceUri, dataSourceFactory, renderersFactory);
case C.TYPE_SS:
- return DownloadHelper.forSmoothStreaming(context, uri, dataSourceFactory, renderersFactory);
+ return DownloadHelper.forSmoothStreaming(
+ context, playbackProperties.sourceUri, dataSourceFactory, renderersFactory);
case C.TYPE_HLS:
- return DownloadHelper.forHls(context, uri, dataSourceFactory, renderersFactory);
+ return DownloadHelper.forHls(
+ context, playbackProperties.sourceUri, dataSourceFactory, renderersFactory);
case C.TYPE_OTHER:
- return DownloadHelper.forProgressive(context, uri);
+ return DownloadHelper.forProgressive(context, playbackProperties.sourceUri);
default:
throw new IllegalStateException("Unsupported type: " + type);
}
@@ -166,16 +172,16 @@
private final FragmentManager fragmentManager;
private final DownloadHelper downloadHelper;
- private final UriSample sample;
+ private final MediaItem mediaItem;
private TrackSelectionDialog trackSelectionDialog;
private MappedTrackInfo mappedTrackInfo;
public StartDownloadDialogHelper(
- FragmentManager fragmentManager, DownloadHelper downloadHelper, UriSample sample) {
+ FragmentManager fragmentManager, DownloadHelper downloadHelper, MediaItem mediaItem) {
this.fragmentManager = fragmentManager;
this.downloadHelper = downloadHelper;
- this.sample = sample;
+ this.mediaItem = mediaItem;
downloadHelper.prepare(this);
}
@@ -270,7 +276,8 @@
}
private DownloadRequest buildDownloadRequest() {
- return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(sample.name));
+ return downloadHelper.getDownloadRequest(
+ Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title)));
}
}
}
diff --git a/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java b/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java
new file mode 100644
index 0000000..b4c0e2a
--- /dev/null
+++ b/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/IntentUtil.java
@@ -0,0 +1,294 @@
+/*
+ * 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.demo;
+
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+import static com.google.android.exoplayer2.util.Assertions.checkState;
+
+import android.content.Intent;
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.offline.DownloadRequest;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+/** Util to read from and populate an intent. */
+public class IntentUtil {
+
+ /** A tag to hold custom playback configuration attributes. */
+ public static class Tag {
+
+ /** Whether the stream is a live stream. */
+ public final boolean isLive;
+ /** The spherical stereo mode or null. */
+ @Nullable public final String sphericalStereoMode;
+
+ /** Creates an instance. */
+ public Tag(boolean isLive, @Nullable String sphericalStereoMode) {
+ this.isLive = isLive;
+ this.sphericalStereoMode = sphericalStereoMode;
+ }
+ }
+
+ // Actions.
+
+ public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
+ public static final String ACTION_VIEW_LIST =
+ "com.google.android.exoplayer.demo.action.VIEW_LIST";
+
+ // Activity extras.
+
+ public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
+ public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
+ public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
+ public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
+
+ // Player configuration extras.
+
+ public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
+ public static final String ABR_ALGORITHM_DEFAULT = "default";
+ public static final String ABR_ALGORITHM_RANDOM = "random";
+
+ // Media item configuration extras.
+
+ public static final String URI_EXTRA = "uri";
+ public static final String IS_LIVE_EXTRA = "is_live";
+ public static final String MIME_TYPE_EXTRA = "mime_type";
+ // For backwards compatibility only.
+ public static final String EXTENSION_EXTRA = "extension";
+
+ public static final String DRM_SCHEME_EXTRA = "drm_scheme";
+ public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
+ public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
+ public static final String DRM_SESSION_FOR_CLEAR_TYPES_EXTRA = "drm_session_for_clear_types";
+ public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
+ public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
+ public static final String SUBTITLE_URI_EXTRA = "subtitle_uri";
+ public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type";
+ public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language";
+ // For backwards compatibility only.
+ public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
+
+ public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
+ public static final String TUNNELING_EXTRA = "tunneling";
+
+ /** Creates a list of {@link MediaItem media items} from an {@link Intent}. */
+ public static List<MediaItem> createMediaItemsFromIntent(
+ Intent intent, DownloadTracker downloadTracker) {
+ List<MediaItem> mediaItems = new ArrayList<>();
+ if (ACTION_VIEW_LIST.equals(intent.getAction())) {
+ int index = 0;
+ while (intent.hasExtra(URI_EXTRA + "_" + index)) {
+ Uri uri = Uri.parse(intent.getStringExtra(URI_EXTRA + "_" + index));
+ mediaItems.add(
+ createMediaItemFromIntent(
+ uri,
+ intent,
+ /* extrasKeySuffix= */ "_" + index,
+ downloadTracker.getDownloadRequest(uri)));
+ index++;
+ }
+ } else {
+ Uri uri = intent.getData();
+ mediaItems.add(
+ createMediaItemFromIntent(
+ uri, intent, /* extrasKeySuffix= */ "", downloadTracker.getDownloadRequest(uri)));
+ }
+ return mediaItems;
+ }
+
+ /** Populates the intent with the given list of {@link MediaItem media items}. */
+ public static void addToIntent(List<MediaItem> mediaItems, Intent intent) {
+ Assertions.checkArgument(!mediaItems.isEmpty());
+ if (mediaItems.size() == 1) {
+ MediaItem.PlaybackProperties playbackProperties =
+ checkNotNull(mediaItems.get(0).playbackProperties);
+ intent.setAction(IntentUtil.ACTION_VIEW).setData(playbackProperties.sourceUri);
+ addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "");
+ } else {
+ intent.setAction(IntentUtil.ACTION_VIEW_LIST);
+ for (int i = 0; i < mediaItems.size(); i++) {
+ MediaItem.PlaybackProperties playbackProperties =
+ checkNotNull(mediaItems.get(i).playbackProperties);
+ intent.putExtra(IntentUtil.URI_EXTRA + ("_" + i), playbackProperties.sourceUri.toString());
+ addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "_" + i);
+ }
+ }
+ }
+
+ /** Makes a best guess to infer the MIME type from a {@link Uri} and an optional extension. */
+ @Nullable
+ public static String inferAdaptiveStreamMimeType(Uri uri, @Nullable String extension) {
+ @C.ContentType int contentType = Util.inferContentType(uri, extension);
+ switch (contentType) {
+ case C.TYPE_DASH:
+ return MimeTypes.APPLICATION_MPD;
+ case C.TYPE_HLS:
+ return MimeTypes.APPLICATION_M3U8;
+ case C.TYPE_SS:
+ return MimeTypes.APPLICATION_SS;
+ case C.TYPE_OTHER:
+ default:
+ return null;
+ }
+ }
+
+ private static MediaItem createMediaItemFromIntent(
+ Uri uri, Intent intent, String extrasKeySuffix, @Nullable DownloadRequest downloadRequest) {
+ String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix);
+ if (mimeType == null) {
+ // Try to use extension for backwards compatibility.
+ String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix);
+ mimeType = inferAdaptiveStreamMimeType(uri, extension);
+ }
+ MediaItem.Builder builder =
+ new MediaItem.Builder()
+ .setSourceUri(uri)
+ .setStreamKeys(downloadRequest != null ? downloadRequest.streamKeys : null)
+ .setCustomCacheKey(downloadRequest != null ? downloadRequest.customCacheKey : null)
+ .setMimeType(mimeType)
+ .setAdTagUri(intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix))
+ .setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix));
+ return populateDrmPropertiesFromIntent(builder, intent, extrasKeySuffix).build();
+ }
+
+ private static List<MediaItem.Subtitle> createSubtitlesFromIntent(
+ Intent intent, String extrasKeySuffix) {
+ if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) {
+ return Collections.emptyList();
+ }
+ return Collections.singletonList(
+ new MediaItem.Subtitle(
+ Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)),
+ checkNotNull(intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix)),
+ intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix),
+ C.SELECTION_FLAG_DEFAULT));
+ }
+
+ private static MediaItem.Builder populateDrmPropertiesFromIntent(
+ MediaItem.Builder builder, Intent intent, String extrasKeySuffix) {
+ String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
+ String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix;
+ if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) {
+ return builder;
+ }
+ String drmSchemeExtra =
+ intent.hasExtra(schemeKey)
+ ? intent.getStringExtra(schemeKey)
+ : intent.getStringExtra(schemeUuidKey);
+ String[] drmSessionForClearTypesExtra =
+ intent.getStringArrayExtra(DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix);
+ Map<String, String> headers = new HashMap<>();
+ String[] keyRequestPropertiesArray =
+ intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
+ if (keyRequestPropertiesArray != null) {
+ for (int i = 0; i < keyRequestPropertiesArray.length; i += 2) {
+ headers.put(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
+ }
+ }
+ builder
+ .setDrmUuid(Util.getDrmUuid(Util.castNonNull(drmSchemeExtra)))
+ .setDrmLicenseUri(intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix))
+ .setDrmSessionForClearTypes(toTrackTypeList(drmSessionForClearTypesExtra))
+ .setDrmMultiSession(
+ intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false))
+ .setDrmLicenseRequestHeaders(headers);
+ return builder;
+ }
+
+ private static List<Integer> toTrackTypeList(@Nullable String[] trackTypeStringsArray) {
+ if (trackTypeStringsArray == null) {
+ return Collections.emptyList();
+ }
+ HashSet<Integer> trackTypes = new HashSet<>();
+ for (String trackTypeString : trackTypeStringsArray) {
+ switch (Util.toLowerInvariant(trackTypeString)) {
+ case "audio":
+ trackTypes.add(C.TRACK_TYPE_AUDIO);
+ break;
+ case "video":
+ trackTypes.add(C.TRACK_TYPE_VIDEO);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid track type: " + trackTypeString);
+ }
+ }
+ return new ArrayList<>(trackTypes);
+ }
+
+ private static void addPlaybackPropertiesToIntent(
+ MediaItem.PlaybackProperties playbackProperties, Intent intent, String extrasKeySuffix) {
+ boolean isLive = false;
+ String sphericalStereoMode = null;
+ if (playbackProperties.tag instanceof Tag) {
+ Tag tag = (Tag) playbackProperties.tag;
+ isLive = tag.isLive;
+ sphericalStereoMode = tag.sphericalStereoMode;
+ }
+ intent
+ .putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, playbackProperties.mimeType)
+ .putExtra(
+ AD_TAG_URI_EXTRA + extrasKeySuffix,
+ playbackProperties.adTagUri != null ? playbackProperties.adTagUri.toString() : null)
+ .putExtra(IS_LIVE_EXTRA + extrasKeySuffix, isLive)
+ .putExtra(SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
+ if (playbackProperties.drmConfiguration != null) {
+ addDrmConfigurationToIntent(playbackProperties.drmConfiguration, intent, extrasKeySuffix);
+ }
+ if (!playbackProperties.subtitles.isEmpty()) {
+ checkState(playbackProperties.subtitles.size() == 1);
+ MediaItem.Subtitle subtitle = playbackProperties.subtitles.get(0);
+ intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, subtitle.uri.toString());
+ intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, subtitle.mimeType);
+ intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, subtitle.language);
+ }
+ }
+
+ private static void addDrmConfigurationToIntent(
+ MediaItem.DrmConfiguration drmConfiguration, Intent intent, String extrasKeySuffix) {
+ intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmConfiguration.uuid.toString());
+ intent.putExtra(
+ DRM_LICENSE_URL_EXTRA + extrasKeySuffix,
+ checkNotNull(drmConfiguration.licenseUri).toString());
+ intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmConfiguration.multiSession);
+
+ String[] drmKeyRequestProperties = new String[drmConfiguration.requestHeaders.size() * 2];
+ int index = 0;
+ for (Map.Entry<String, String> entry : drmConfiguration.requestHeaders.entrySet()) {
+ drmKeyRequestProperties[index++] = entry.getKey();
+ drmKeyRequestProperties[index++] = entry.getValue();
+ }
+ intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
+
+ ArrayList<String> typeStrings = new ArrayList<>();
+ for (int type : drmConfiguration.sessionForClearTypes) {
+ // Only audio and video are supported.
+ Assertions.checkState(type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO);
+ typeStrings.add(type == C.TRACK_TYPE_AUDIO ? "audio" : "video");
+ }
+ intent.putExtra(
+ DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix, typeStrings.toArray(new String[0]));
+ }
+}
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 f941234..47d7966 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
@@ -39,16 +39,12 @@
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
-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.DownloadRequest;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
-import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader;
-import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
@@ -60,7 +56,6 @@
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.EventLogger;
@@ -69,7 +64,6 @@
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -77,45 +71,6 @@
public class PlayerActivity extends AppCompatActivity
implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener {
- // Activity extras.
-
- public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
- public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
- public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
- public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
-
- // Actions.
-
- public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW";
- public static final String ACTION_VIEW_LIST =
- "com.google.android.exoplayer.demo.action.VIEW_LIST";
-
- // Player configuration extras.
-
- public static final String ABR_ALGORITHM_EXTRA = "abr_algorithm";
- public static final String ABR_ALGORITHM_DEFAULT = "default";
- public static final String ABR_ALGORITHM_RANDOM = "random";
-
- // Media item configuration extras.
-
- public static final String URI_EXTRA = "uri";
- public static final String EXTENSION_EXTRA = "extension";
- public static final String IS_LIVE_EXTRA = "is_live";
-
- public static final String DRM_SCHEME_EXTRA = "drm_scheme";
- public static final String DRM_LICENSE_URL_EXTRA = "drm_license_url";
- public static final String DRM_KEY_REQUEST_PROPERTIES_EXTRA = "drm_key_request_properties";
- public static final String DRM_SESSION_FOR_CLEAR_TYPES_EXTRA = "drm_session_for_clear_types";
- public static final String DRM_MULTI_SESSION_EXTRA = "drm_multi_session";
- public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
- public static final String TUNNELING_EXTRA = "tunneling";
- public static final String AD_TAG_URI_EXTRA = "ad_tag_uri";
- public static final String SUBTITLE_URI_EXTRA = "subtitle_uri";
- public static final String SUBTITLE_MIME_TYPE_EXTRA = "subtitle_mime_type";
- public static final String SUBTITLE_LANGUAGE_EXTRA = "subtitle_language";
- // For backwards compatibility only.
- public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
-
// Saved instance state keys.
private static final String KEY_TRACK_SELECTOR_PARAMETERS = "track_selector_parameters";
@@ -138,12 +93,11 @@
private DataSource.Factory dataSourceFactory;
private SimpleExoPlayer player;
- private List<MediaSource> mediaSources;
+ private List<MediaItem> mediaItems;
private DefaultTrackSelector trackSelector;
private DefaultTrackSelector.Parameters trackSelectorParameters;
private DebugTextViewHelper debugViewHelper;
private TrackGroupArray lastSeenTrackGroupArray;
- private DefaultMediaSourceFactory mediaSourceFactory;
private boolean startAutoPlay;
private int startWindow;
private long startPosition;
@@ -158,14 +112,12 @@
@Override
public void onCreate(Bundle savedInstanceState) {
Intent intent = getIntent();
- String sphericalStereoMode = intent.getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
+ String sphericalStereoMode = intent.getStringExtra(IntentUtil.SPHERICAL_STEREO_MODE_EXTRA);
if (sphericalStereoMode != null) {
setTheme(R.style.PlayerTheme_Spherical);
}
super.onCreate(savedInstanceState);
dataSourceFactory = buildDataSourceFactory();
- mediaSourceFactory =
- DefaultMediaSourceFactory.newInstance(/* context= */ this, dataSourceFactory);
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
}
@@ -182,11 +134,11 @@
playerView.requestFocus();
if (sphericalStereoMode != null) {
int stereoMode;
- if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
+ if (IntentUtil.SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
stereoMode = C.STEREO_MODE_MONO;
- } else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
+ } else if (IntentUtil.SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
stereoMode = C.STEREO_MODE_TOP_BOTTOM;
- } else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
+ } else if (IntentUtil.SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
} else {
showToast(R.string.error_unrecognized_stereo_mode);
@@ -204,7 +156,7 @@
} else {
DefaultTrackSelector.ParametersBuilder builder =
new DefaultTrackSelector.ParametersBuilder(/* context= */ this);
- boolean tunneling = intent.getBooleanExtra(TUNNELING_EXTRA, false);
+ boolean tunneling = intent.getBooleanExtra(IntentUtil.TUNNELING_EXTRA, false);
if (Util.SDK_INT >= 21 && tunneling) {
builder.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(/* context= */ this));
}
@@ -328,7 +280,7 @@
@Override
public void preparePlayback() {
- player.retry();
+ player.prepare();
}
// PlaybackControlView.VisibilityListener implementation
@@ -343,15 +295,17 @@
private void initializePlayer() {
if (player == null) {
Intent intent = getIntent();
- mediaSources = createTopLevelMediaSources(intent);
- if (mediaSources.isEmpty()) {
+
+ mediaItems = createMediaItems(intent);
+ if (mediaItems.isEmpty()) {
return;
}
+
TrackSelection.Factory trackSelectionFactory;
- String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
- if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
+ String abrAlgorithm = intent.getStringExtra(IntentUtil.ABR_ALGORITHM_EXTRA);
+ if (abrAlgorithm == null || IntentUtil.ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
trackSelectionFactory = new AdaptiveTrackSelection.Factory();
- } else if (ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
+ } else if (IntentUtil.ABR_ALGORITHM_RANDOM.equals(abrAlgorithm)) {
trackSelectionFactory = new RandomTrackSelection.Factory();
} else {
showToast(R.string.error_unrecognized_abr_algorithm);
@@ -360,7 +314,7 @@
}
boolean preferExtensionDecoders =
- intent.getBooleanExtra(PREFER_EXTENSION_DECODERS_EXTRA, false);
+ intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false);
RenderersFactory renderersFactory =
((DemoApplication) getApplication()).buildRenderersFactory(preferExtensionDecoders);
@@ -370,6 +324,9 @@
player =
new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory)
+ .setMediaSourceFactory(
+ new DefaultMediaSourceFactory(
+ /* context= */ this, dataSourceFactory, new AdSupportProvider()))
.setTrackSelector(trackSelector)
.build();
player.addListener(new PlayerEventListener());
@@ -380,39 +337,32 @@
playerView.setPlaybackPreparer(this);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
- if (adsLoader != null) {
- adsLoader.setPlayer(player);
- }
}
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
if (haveStartPosition) {
player.seekTo(startWindow, startPosition);
}
- player.setMediaSources(mediaSources, /* resetPosition= */ !haveStartPosition);
+ player.setMediaItems(mediaItems, /* resetPosition= */ !haveStartPosition);
player.prepare();
updateButtonVisibility();
}
- private List<MediaSource> createTopLevelMediaSources(Intent intent) {
+ private List<MediaItem> createMediaItems(Intent intent) {
String action = intent.getAction();
- boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
- if (!actionIsListView && !ACTION_VIEW.equals(action)) {
+ boolean actionIsListView = IntentUtil.ACTION_VIEW_LIST.equals(action);
+ if (!actionIsListView && !IntentUtil.ACTION_VIEW.equals(action)) {
showToast(getString(R.string.unexpected_intent_action, action));
finish();
return Collections.emptyList();
}
- Sample intentAsSample = Sample.createFromIntent(intent);
- UriSample[] samples =
- intentAsSample instanceof Sample.PlaylistSample
- ? ((Sample.PlaylistSample) intentAsSample).children
- : new UriSample[] {(UriSample) intentAsSample};
+ List<MediaItem> mediaItems =
+ IntentUtil.createMediaItemsFromIntent(
+ intent, ((DemoApplication) getApplication()).getDownloadTracker());
+ boolean hasAds = false;
+ for (int i = 0; i < mediaItems.size(); i++) {
+ MediaItem mediaItem = mediaItems.get(i);
- List<MediaSource> mediaSources = new ArrayList<>();
- Uri adTagUri = null;
- for (UriSample sample : samples) {
- MediaItem mediaItem = sample.toMediaItem();
- Assertions.checkNotNull(mediaItem.playbackProperties);
if (!Util.checkCleartextTrafficPermitted(mediaItem)) {
showToast(R.string.error_cleartext_not_permitted);
return Collections.emptyList();
@@ -421,67 +371,26 @@
// The player will be reinitialized if the permission is granted.
return Collections.emptyList();
}
- MediaSource mediaSource = createLeafMediaSource(mediaItem);
- if (mediaSource != null) {
- adTagUri = sample.adTagUri;
- mediaSources.add(mediaSource);
- }
- }
- if (adTagUri == null) {
- releaseAdsLoader();
- } else if (mediaSources.size() == 1) {
- if (!adTagUri.equals(loadedAdTagUri)) {
- releaseAdsLoader();
- loadedAdTagUri = adTagUri;
+ MediaItem.DrmConfiguration drmConfiguration =
+ Assertions.checkNotNull(mediaItem.playbackProperties).drmConfiguration;
+ if (drmConfiguration != null) {
+ if (Util.SDK_INT < 18) {
+ showToast(R.string.error_drm_unsupported_before_api_18);
+ finish();
+ return Collections.emptyList();
+ } else if (!MediaDrm.isCryptoSchemeSupported(drmConfiguration.uuid)) {
+ showToast(R.string.error_drm_unsupported_scheme);
+ finish();
+ return Collections.emptyList();
+ }
}
- MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri);
- if (adsMediaSource != null) {
- mediaSources.set(0, adsMediaSource);
- } else {
- showToast(R.string.ima_not_loaded);
- }
- } else if (mediaSources.size() > 1) {
- showToast(R.string.unsupported_ads_in_concatenation);
+ hasAds |= mediaItem.playbackProperties.adTagUri != null;
+ }
+ if (!hasAds) {
releaseAdsLoader();
}
-
- return mediaSources;
- }
-
- @Nullable
- private MediaSource createLeafMediaSource(MediaItem mediaItem) {
- Assertions.checkNotNull(mediaItem.playbackProperties);
- HttpDataSource.Factory drmDataSourceFactory = null;
- if (mediaItem.playbackProperties.drmConfiguration != null) {
- if (Util.SDK_INT < 18) {
- showToast(R.string.error_drm_unsupported_before_api_18);
- finish();
- return null;
- } else if (!MediaDrm.isCryptoSchemeSupported(
- mediaItem.playbackProperties.drmConfiguration.uuid)) {
- showToast(R.string.error_drm_unsupported_scheme);
- finish();
- return null;
- }
- drmDataSourceFactory = ((DemoApplication) getApplication()).buildHttpDataSourceFactory();
- }
-
- DownloadRequest downloadRequest =
- ((DemoApplication) getApplication())
- .getDownloadTracker()
- .getDownloadRequest(mediaItem.playbackProperties.sourceUri);
- if (downloadRequest != null) {
- mediaItem =
- mediaItem
- .buildUpon()
- .setStreamKeys(downloadRequest.streamKeys)
- .setCustomCacheKey(downloadRequest.customCacheKey)
- .build();
- }
- return mediaSourceFactory
- .setDrmHttpDataSourceFactory(drmDataSourceFactory)
- .createMediaSource(mediaItem);
+ return mediaItems;
}
private void releasePlayer() {
@@ -492,7 +401,7 @@
debugViewHelper = null;
player.release();
player = null;
- mediaSources = Collections.emptyList();
+ mediaItems = Collections.emptyList();
trackSelector = null;
}
if (adsLoader != null) {
@@ -534,24 +443,23 @@
return ((DemoApplication) getApplication()).buildDataSourceFactory();
}
- /** Returns an ads media source, reusing the ads loader if one exists. */
+ /**
+ * Returns an ads loader for the Interactive Media Ads SDK if found in the classpath, or null
+ * otherwise.
+ */
@Nullable
- private MediaSource createAdsMediaSource(MediaSource mediaSource, Uri adTagUri) {
+ private AdsLoader maybeCreateAdsLoader(Uri adTagUri) {
// Load the extension source using reflection so the demo app doesn't have to depend on it.
- // The ads loader is reused for multiple playbacks, so that ad playback can resume.
try {
Class<?> loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader");
- if (adsLoader == null) {
- // Full class names used so the lint rule triggers should any of the classes move.
- // LINT.IfChange
- Constructor<? extends AdsLoader> loaderConstructor =
- loaderClass
- .asSubclass(AdsLoader.class)
- .getConstructor(android.content.Context.class, android.net.Uri.class);
- // LINT.ThenChange(../../../../../../../../proguard-rules.txt)
- adsLoader = loaderConstructor.newInstance(this, adTagUri);
- }
- return new AdsMediaSource(mediaSource, mediaSourceFactory, adsLoader, playerView);
+ // Full class names used so the lint rule triggers should any of the classes move.
+ // LINT.IfChange
+ Constructor<? extends AdsLoader> loaderConstructor =
+ loaderClass
+ .asSubclass(AdsLoader.class)
+ .getConstructor(android.content.Context.class, android.net.Uri.class);
+ // LINT.ThenChange(../../../../../../../../proguard-rules.txt)
+ return loaderConstructor.newInstance(this, adTagUri);
} catch (ClassNotFoundException e) {
// IMA extension not loaded.
return null;
@@ -670,4 +578,36 @@
return Pair.create(0, errorString);
}
}
+
+ private class AdSupportProvider implements DefaultMediaSourceFactory.AdSupportProvider {
+
+ @Nullable
+ @Override
+ public AdsLoader getAdsLoader(Uri adTagUri) {
+ if (mediaItems.size() > 1) {
+ showToast(R.string.unsupported_ads_in_concatenation);
+ releaseAdsLoader();
+ return null;
+ }
+ if (!adTagUri.equals(loadedAdTagUri)) {
+ releaseAdsLoader();
+ loadedAdTagUri = adTagUri;
+ }
+ // The ads loader is reused for multiple playbacks, so that ad playback can resume.
+ if (adsLoader == null) {
+ adsLoader = maybeCreateAdsLoader(adTagUri);
+ }
+ if (adsLoader != null) {
+ adsLoader.setPlayer(player);
+ } else {
+ showToast(R.string.ima_not_loaded);
+ }
+ return adsLoader;
+ }
+
+ @Override
+ public AdsLoader.AdViewProvider getAdViewProvider() {
+ return Assertions.checkNotNull(playerView);
+ }
+ }
}
diff --git a/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java b/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java
deleted file mode 100644
index 1225c8b..0000000
--- a/tree/demos/main/src/main/java/com/google/android/exoplayer2/demo/Sample.java
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2.demo;
-
-import static com.google.android.exoplayer2.demo.PlayerActivity.ACTION_VIEW_LIST;
-import static com.google.android.exoplayer2.demo.PlayerActivity.AD_TAG_URI_EXTRA;
-import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_KEY_REQUEST_PROPERTIES_EXTRA;
-import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_LICENSE_URL_EXTRA;
-import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_MULTI_SESSION_EXTRA;
-import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_EXTRA;
-import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SCHEME_UUID_EXTRA;
-import static com.google.android.exoplayer2.demo.PlayerActivity.DRM_SESSION_FOR_CLEAR_TYPES_EXTRA;
-import static com.google.android.exoplayer2.demo.PlayerActivity.EXTENSION_EXTRA;
-import static com.google.android.exoplayer2.demo.PlayerActivity.IS_LIVE_EXTRA;
-import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_LANGUAGE_EXTRA;
-import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_MIME_TYPE_EXTRA;
-import static com.google.android.exoplayer2.demo.PlayerActivity.SUBTITLE_URI_EXTRA;
-import static com.google.android.exoplayer2.demo.PlayerActivity.URI_EXTRA;
-
-import android.content.Intent;
-import android.net.Uri;
-import androidx.annotation.Nullable;
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.MediaItem;
-import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.MimeTypes;
-import com.google.android.exoplayer2.util.Util;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.UUID;
-
-/* package */ abstract class Sample {
-
- /**
- * Returns the mime type which is one of {@link MimeTypes#APPLICATION_MPD} for DASH, {@link
- * MimeTypes#APPLICATION_M3U8} for HLS, {@link MimeTypes#APPLICATION_SS} for SmoothStreaming or
- * {@code null} for all other streams.
- *
- * @param uri The uri of the stream.
- * @param extension The extension
- * @return The adaptive mime type or {@code null} for non-adaptive streams.
- */
- @Nullable
- public static String inferAdaptiveStreamMimeType(Uri uri, @Nullable String extension) {
- @C.ContentType int contentType = Util.inferContentType(uri, extension);
- switch (contentType) {
- case C.TYPE_DASH:
- return MimeTypes.APPLICATION_MPD;
- case C.TYPE_HLS:
- return MimeTypes.APPLICATION_M3U8;
- case C.TYPE_SS:
- return MimeTypes.APPLICATION_SS;
- case C.TYPE_OTHER:
- default:
- return null;
- }
- }
-
- public static final class UriSample extends Sample {
-
- public static UriSample createFromIntent(Uri uri, Intent intent, String extrasKeySuffix) {
- String extension = intent.getStringExtra(EXTENSION_EXTRA + extrasKeySuffix);
- String adsTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix);
- boolean isLive =
- intent.getBooleanExtra(IS_LIVE_EXTRA + extrasKeySuffix, /* defaultValue= */ false);
- Uri adTagUri = adsTagUriString != null ? Uri.parse(adsTagUriString) : null;
- return new UriSample(
- /* name= */ null,
- uri,
- extension,
- isLive,
- DrmInfo.createFromIntent(intent, extrasKeySuffix),
- adTagUri,
- /* sphericalStereoMode= */ null,
- SubtitleInfo.createFromIntent(intent, extrasKeySuffix));
- }
-
- public final Uri uri;
- public final String extension;
- public final boolean isLive;
- public final DrmInfo drmInfo;
- public final Uri adTagUri;
- @Nullable public final String sphericalStereoMode;
- @Nullable SubtitleInfo subtitleInfo;
-
- public UriSample(
- String name,
- Uri uri,
- String extension,
- boolean isLive,
- DrmInfo drmInfo,
- Uri adTagUri,
- @Nullable String sphericalStereoMode,
- @Nullable SubtitleInfo subtitleInfo) {
- super(name);
- this.uri = uri;
- this.extension = extension;
- this.isLive = isLive;
- this.drmInfo = drmInfo;
- this.adTagUri = adTagUri;
- this.sphericalStereoMode = sphericalStereoMode;
- this.subtitleInfo = subtitleInfo;
- }
-
- @Override
- public void addToIntent(Intent intent) {
- intent.setAction(PlayerActivity.ACTION_VIEW).setData(uri);
- intent.putExtra(PlayerActivity.IS_LIVE_EXTRA, isLive);
- intent.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
- addPlayerConfigToIntent(intent, /* extrasKeySuffix= */ "");
- }
-
- public void addToPlaylistIntent(Intent intent, String extrasKeySuffix) {
- intent.putExtra(PlayerActivity.URI_EXTRA + extrasKeySuffix, uri.toString());
- intent.putExtra(PlayerActivity.IS_LIVE_EXTRA + extrasKeySuffix, isLive);
- addPlayerConfigToIntent(intent, extrasKeySuffix);
- }
-
- private void addPlayerConfigToIntent(Intent intent, String extrasKeySuffix) {
- intent
- .putExtra(EXTENSION_EXTRA + extrasKeySuffix, extension)
- .putExtra(
- AD_TAG_URI_EXTRA + extrasKeySuffix, adTagUri != null ? adTagUri.toString() : null);
- if (drmInfo != null) {
- drmInfo.addToIntent(intent, extrasKeySuffix);
- }
- if (subtitleInfo != null) {
- subtitleInfo.addToIntent(intent, extrasKeySuffix);
- }
- }
-
- public MediaItem toMediaItem() {
- MediaItem.Builder builder = new MediaItem.Builder().setSourceUri(uri);
- builder.setMimeType(inferAdaptiveStreamMimeType(uri, extension));
- if (drmInfo != null) {
- Map<String, String> headers = new HashMap<>();
- if (drmInfo.drmKeyRequestProperties != null) {
- for (int i = 0; i < drmInfo.drmKeyRequestProperties.length; i += 2) {
- headers.put(drmInfo.drmKeyRequestProperties[i], drmInfo.drmKeyRequestProperties[i + 1]);
- }
- }
- builder
- .setDrmLicenseUri(drmInfo.drmLicenseUrl)
- .setDrmLicenseRequestHeaders(headers)
- .setDrmUuid(drmInfo.drmScheme)
- .setDrmMultiSession(drmInfo.drmMultiSession)
- .setDrmSessionForClearTypes(Util.toList(drmInfo.drmSessionForClearTypes));
- }
- if (subtitleInfo != null) {
- builder.setSubtitles(
- Collections.singletonList(
- new MediaItem.Subtitle(
- subtitleInfo.uri,
- subtitleInfo.mimeType,
- subtitleInfo.language,
- C.SELECTION_FLAG_DEFAULT)));
- }
- return builder.build();
- }
- }
-
- public static final class PlaylistSample extends Sample {
-
- public final UriSample[] children;
-
- public PlaylistSample(String name, UriSample... children) {
- super(name);
- this.children = children;
- }
-
- @Override
- public void addToIntent(Intent intent) {
- intent.setAction(PlayerActivity.ACTION_VIEW_LIST);
- for (int i = 0; i < children.length; i++) {
- children[i].addToPlaylistIntent(intent, /* extrasKeySuffix= */ "_" + i);
- }
- }
- }
-
- public static final class DrmInfo {
-
- public static DrmInfo createFromIntent(Intent intent, String extrasKeySuffix) {
- String schemeKey = DRM_SCHEME_EXTRA + extrasKeySuffix;
- String schemeUuidKey = DRM_SCHEME_UUID_EXTRA + extrasKeySuffix;
- if (!intent.hasExtra(schemeKey) && !intent.hasExtra(schemeUuidKey)) {
- return null;
- }
- String drmSchemeExtra =
- intent.hasExtra(schemeKey)
- ? intent.getStringExtra(schemeKey)
- : intent.getStringExtra(schemeUuidKey);
- UUID drmScheme = Util.getDrmUuid(drmSchemeExtra);
- String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix);
- String[] keyRequestPropertiesArray =
- intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix);
- String[] drmSessionForClearTypesExtra =
- intent.getStringArrayExtra(DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix);
- int[] drmSessionForClearTypes = toTrackTypeArray(drmSessionForClearTypesExtra);
- boolean drmMultiSession =
- intent.getBooleanExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, false);
- return new DrmInfo(
- drmScheme,
- drmLicenseUrl,
- keyRequestPropertiesArray,
- drmSessionForClearTypes,
- drmMultiSession);
- }
-
- public final UUID drmScheme;
- public final String drmLicenseUrl;
- public final String[] drmKeyRequestProperties;
- public final int[] drmSessionForClearTypes;
- public final boolean drmMultiSession;
-
- public DrmInfo(
- UUID drmScheme,
- String drmLicenseUrl,
- String[] drmKeyRequestProperties,
- int[] drmSessionForClearTypes,
- boolean drmMultiSession) {
- this.drmScheme = drmScheme;
- this.drmLicenseUrl = drmLicenseUrl;
- this.drmKeyRequestProperties = drmKeyRequestProperties;
- this.drmSessionForClearTypes = drmSessionForClearTypes;
- this.drmMultiSession = drmMultiSession;
- }
-
- public void addToIntent(Intent intent, String extrasKeySuffix) {
- Assertions.checkNotNull(intent);
- intent.putExtra(DRM_SCHEME_EXTRA + extrasKeySuffix, drmScheme.toString());
- intent.putExtra(DRM_LICENSE_URL_EXTRA + extrasKeySuffix, drmLicenseUrl);
- intent.putExtra(DRM_KEY_REQUEST_PROPERTIES_EXTRA + extrasKeySuffix, drmKeyRequestProperties);
- ArrayList<String> typeStrings = new ArrayList<>();
- for (int type : drmSessionForClearTypes) {
- // Only audio and video are supported.
- typeStrings.add(type == C.TRACK_TYPE_AUDIO ? "audio" : "video");
- }
- intent.putExtra(
- DRM_SESSION_FOR_CLEAR_TYPES_EXTRA + extrasKeySuffix, typeStrings.toArray(new String[0]));
- intent.putExtra(DRM_MULTI_SESSION_EXTRA + extrasKeySuffix, drmMultiSession);
- }
- }
-
- public static final class SubtitleInfo {
-
- @Nullable
- public static SubtitleInfo createFromIntent(Intent intent, String extrasKeySuffix) {
- if (!intent.hasExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)) {
- return null;
- }
- return new SubtitleInfo(
- Uri.parse(intent.getStringExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix)),
- intent.getStringExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix),
- intent.getStringExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix));
- }
-
- public final Uri uri;
- public final String mimeType;
- @Nullable public final String language;
-
- public SubtitleInfo(Uri uri, String mimeType, @Nullable String language) {
- this.uri = Assertions.checkNotNull(uri);
- this.mimeType = Assertions.checkNotNull(mimeType);
- this.language = language;
- }
-
- public void addToIntent(Intent intent, String extrasKeySuffix) {
- intent.putExtra(SUBTITLE_URI_EXTRA + extrasKeySuffix, uri.toString());
- intent.putExtra(SUBTITLE_MIME_TYPE_EXTRA + extrasKeySuffix, mimeType);
- intent.putExtra(SUBTITLE_LANGUAGE_EXTRA + extrasKeySuffix, language);
- }
- }
-
- public static int[] toTrackTypeArray(@Nullable String[] trackTypeStringsArray) {
- if (trackTypeStringsArray == null) {
- return new int[0];
- }
- HashSet<Integer> trackTypes = new HashSet<>();
- for (String trackTypeString : trackTypeStringsArray) {
- switch (Util.toLowerInvariant(trackTypeString)) {
- case "audio":
- trackTypes.add(C.TRACK_TYPE_AUDIO);
- break;
- case "video":
- trackTypes.add(C.TRACK_TYPE_VIDEO);
- break;
- default:
- throw new IllegalArgumentException("Invalid track type: " + trackTypeString);
- }
- }
- return Util.toArray(new ArrayList<>(trackTypes));
- }
-
- public static Sample createFromIntent(Intent intent) {
- if (ACTION_VIEW_LIST.equals(intent.getAction())) {
- ArrayList<String> intentUris = new ArrayList<>();
- int index = 0;
- while (intent.hasExtra(URI_EXTRA + "_" + index)) {
- intentUris.add(intent.getStringExtra(URI_EXTRA + "_" + index));
- index++;
- }
- UriSample[] children = new UriSample[intentUris.size()];
- for (int i = 0; i < children.length; i++) {
- Uri uri = Uri.parse(intentUris.get(i));
- children[i] = UriSample.createFromIntent(uri, intent, /* extrasKeySuffix= */ "_" + i);
- }
- return new PlaylistSample(/* name= */ null, children);
- } else {
- return UriSample.createFromIntent(intent.getData(), intent, /* extrasKeySuffix= */ "");
- }
- }
-
- public final String name;
-
- public Sample(String name) {
- this.name = name;
- }
-
- public abstract void addToIntent(Intent intent);
-}
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 740f016..bfc476f 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
@@ -15,10 +15,13 @@
*/
package com.google.android.exoplayer2.demo;
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.AsyncTask;
@@ -39,11 +42,11 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.RenderersFactory;
-import com.google.android.exoplayer2.demo.Sample.DrmInfo;
-import com.google.android.exoplayer2.demo.Sample.PlaylistSample;
-import com.google.android.exoplayer2.demo.Sample.UriSample;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
@@ -58,7 +61,10 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
/** An activity for selecting from a list of media samples. */
public class SampleChooserActivity extends AppCompatActivity
@@ -182,7 +188,7 @@
}
private void loadSample() {
- Assertions.checkNotNull(uris);
+ checkNotNull(uris);
for (int i = 0; i < uris.length; i++) {
Uri uri = Uri.parse(uris[i]);
@@ -195,12 +201,12 @@
loaderTask.execute(uris);
}
- private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) {
+ private void onPlaylistGroups(final List<PlaylistGroup> groups, boolean sawError) {
if (sawError) {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
.show();
}
- sampleAdapter.setSampleGroups(groups);
+ sampleAdapter.setPlaylistGroups(groups);
SharedPreferences preferences = getPreferences(MODE_PRIVATE);
@@ -227,24 +233,24 @@
prefEditor.putInt(CHILD_POSITION_PREFERENCE_KEY, childPosition);
prefEditor.apply();
- Sample sample = (Sample) view.getTag();
+ PlaylistHolder playlistHolder = (PlaylistHolder) view.getTag();
Intent intent = new Intent(this, PlayerActivity.class);
intent.putExtra(
- PlayerActivity.PREFER_EXTENSION_DECODERS_EXTRA,
+ IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA,
isNonNullAndChecked(preferExtensionDecodersMenuItem));
String abrAlgorithm =
isNonNullAndChecked(randomAbrMenuItem)
- ? PlayerActivity.ABR_ALGORITHM_RANDOM
- : PlayerActivity.ABR_ALGORITHM_DEFAULT;
- intent.putExtra(PlayerActivity.ABR_ALGORITHM_EXTRA, abrAlgorithm);
- intent.putExtra(PlayerActivity.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem));
- sample.addToIntent(intent);
+ ? IntentUtil.ABR_ALGORITHM_RANDOM
+ : IntentUtil.ABR_ALGORITHM_DEFAULT;
+ intent.putExtra(IntentUtil.ABR_ALGORITHM_EXTRA, abrAlgorithm);
+ intent.putExtra(IntentUtil.TUNNELING_EXTRA, isNonNullAndChecked(tunnelingMenuItem));
+ IntentUtil.addToIntent(playlistHolder.mediaItems, intent);
startActivity(intent);
return true;
}
- private void onSampleDownloadButtonClicked(Sample sample) {
- int downloadUnsupportedStringId = getDownloadUnsupportedStringId(sample);
+ private void onSampleDownloadButtonClicked(PlaylistHolder playlistHolder) {
+ int downloadUnsupportedStringId = getDownloadUnsupportedStringId(playlistHolder);
if (downloadUnsupportedStringId != 0) {
Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
.show();
@@ -253,25 +259,26 @@
((DemoApplication) getApplication())
.buildRenderersFactory(isNonNullAndChecked(preferExtensionDecodersMenuItem));
downloadTracker.toggleDownload(
- getSupportFragmentManager(), (UriSample) sample, renderersFactory);
+ getSupportFragmentManager(), playlistHolder.mediaItems.get(0), renderersFactory);
}
}
- private int getDownloadUnsupportedStringId(Sample sample) {
- if (sample instanceof PlaylistSample) {
+ private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) {
+ if (playlistHolder.mediaItems.size() > 1) {
return R.string.download_playlist_unsupported;
}
- UriSample uriSample = (UriSample) sample;
- if (uriSample.drmInfo != null) {
+ MediaItem.PlaybackProperties playbackProperties =
+ checkNotNull(playlistHolder.mediaItems.get(0).playbackProperties);
+ if (playbackProperties.drmConfiguration != null) {
return R.string.download_drm_unsupported;
}
- if (uriSample.isLive) {
+ if (((IntentUtil.Tag) checkNotNull(playbackProperties.tag)).isLive) {
return R.string.download_live_unsupported;
}
- if (uriSample.adTagUri != null) {
+ if (playbackProperties.adTagUri != null) {
return R.string.download_ads_unsupported;
}
- String scheme = uriSample.uri.getScheme();
+ String scheme = playbackProperties.sourceUri.getScheme();
if (!("http".equals(scheme) || "https".equals(scheme))) {
return R.string.download_scheme_unsupported;
}
@@ -283,13 +290,13 @@
return menuItem != null && menuItem.isChecked();
}
- private final class SampleListLoader extends AsyncTask<String, Void, List<SampleGroup>> {
+ private final class SampleListLoader extends AsyncTask<String, Void, List<PlaylistGroup>> {
private boolean sawError;
@Override
- protected List<SampleGroup> doInBackground(String... uris) {
- List<SampleGroup> result = new ArrayList<>();
+ protected List<PlaylistGroup> doInBackground(String... uris) {
+ List<PlaylistGroup> result = new ArrayList<>();
Context context = getApplicationContext();
String userAgent = Util.getUserAgent(context, "ExoPlayerDemo");
DataSource dataSource =
@@ -298,7 +305,7 @@
DataSpec dataSpec = new DataSpec(Uri.parse(uri));
InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
try {
- readSampleGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
+ readPlaylistGroups(new JsonReader(new InputStreamReader(inputStream, "UTF-8")), result);
} catch (Exception e) {
Log.e(TAG, "Error loading sample list: " + uri, e);
sawError = true;
@@ -310,21 +317,23 @@
}
@Override
- protected void onPostExecute(List<SampleGroup> result) {
- onSampleGroups(result, sawError);
+ protected void onPostExecute(List<PlaylistGroup> result) {
+ onPlaylistGroups(result, sawError);
}
- private void readSampleGroups(JsonReader reader, List<SampleGroup> groups) throws IOException {
+ private void readPlaylistGroups(JsonReader reader, List<PlaylistGroup> groups)
+ throws IOException {
reader.beginArray();
while (reader.hasNext()) {
- readSampleGroup(reader, groups);
+ readPlaylistGroup(reader, groups);
}
reader.endArray();
}
- private void readSampleGroup(JsonReader reader, List<SampleGroup> groups) throws IOException {
+ private void readPlaylistGroup(JsonReader reader, List<PlaylistGroup> groups)
+ throws IOException {
String groupName = "";
- ArrayList<Sample> samples = new ArrayList<>();
+ ArrayList<PlaylistHolder> playlistHolders = new ArrayList<>();
reader.beginObject();
while (reader.hasNext()) {
@@ -336,7 +345,7 @@
case "samples":
reader.beginArray();
while (reader.hasNext()) {
- samples.add(readEntry(reader, false));
+ playlistHolders.add(readEntry(reader, false));
}
reader.endArray();
break;
@@ -349,34 +358,28 @@
}
reader.endObject();
- SampleGroup group = getGroup(groupName, groups);
- group.samples.addAll(samples);
+ PlaylistGroup group = getGroup(groupName, groups);
+ group.playlists.addAll(playlistHolders);
}
- private Sample readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
- String sampleName = null;
+ private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) throws IOException {
Uri uri = null;
String extension = null;
+ String title = null;
boolean isLive = false;
- String drmScheme = null;
- String drmLicenseUrl = null;
- String[] drmKeyRequestProperties = null;
- String[] drmSessionForClearTypes = null;
- boolean drmMultiSession = false;
- ArrayList<UriSample> playlistSamples = null;
- String adTagUri = null;
String sphericalStereoMode = null;
- List<Sample.SubtitleInfo> subtitleInfos = new ArrayList<>();
+ ArrayList<PlaylistHolder> children = null;
Uri subtitleUri = null;
String subtitleMimeType = null;
String subtitleLanguage = null;
+ MediaItem.Builder mediaItem = new MediaItem.Builder();
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
switch (name) {
case "name":
- sampleName = reader.nextString();
+ title = reader.nextString();
break;
case "uri":
uri = Uri.parse(reader.nextString());
@@ -385,47 +388,46 @@
extension = reader.nextString();
break;
case "drm_scheme":
- drmScheme = reader.nextString();
+ mediaItem.setDrmUuid(Util.getDrmUuid(reader.nextString()));
break;
case "is_live":
isLive = reader.nextBoolean();
break;
case "drm_license_url":
- drmLicenseUrl = reader.nextString();
+ mediaItem.setDrmLicenseUri(reader.nextString());
break;
case "drm_key_request_properties":
- ArrayList<String> drmKeyRequestPropertiesList = new ArrayList<>();
+ Map<String, String> requestHeaders = new HashMap<>();
reader.beginObject();
while (reader.hasNext()) {
- drmKeyRequestPropertiesList.add(reader.nextName());
- drmKeyRequestPropertiesList.add(reader.nextString());
+ requestHeaders.put(reader.nextName(), reader.nextString());
}
reader.endObject();
- drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]);
+ mediaItem.setDrmLicenseRequestHeaders(requestHeaders);
break;
case "drm_session_for_clear_types":
- ArrayList<String> drmSessionForClearTypesList = new ArrayList<>();
+ HashSet<Integer> drmSessionForClearTypes = new HashSet<>();
reader.beginArray();
while (reader.hasNext()) {
- drmSessionForClearTypesList.add(reader.nextString());
+ drmSessionForClearTypes.add(toTrackType(reader.nextString()));
}
reader.endArray();
- drmSessionForClearTypes = drmSessionForClearTypesList.toArray(new String[0]);
+ mediaItem.setDrmSessionForClearTypes(new ArrayList<>(drmSessionForClearTypes));
break;
case "drm_multi_session":
- drmMultiSession = reader.nextBoolean();
+ mediaItem.setDrmMultiSession(reader.nextBoolean());
break;
case "playlist":
Assertions.checkState(!insidePlaylist, "Invalid nesting of playlists");
- playlistSamples = new ArrayList<>();
+ children = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
- playlistSamples.add((UriSample) readEntry(reader, /* insidePlaylist= */ true));
+ children.add(readEntry(reader, /* insidePlaylist= */ true));
}
reader.endArray();
break;
case "ad_tag_uri":
- adTagUri = reader.nextString();
+ mediaItem.setAdTagUri(reader.nextString());
break;
case "spherical_stereo_mode":
Assertions.checkState(
@@ -446,67 +448,71 @@
}
}
reader.endObject();
- DrmInfo drmInfo =
- drmScheme == null
- ? null
- : new DrmInfo(
- Util.getDrmUuid(drmScheme),
- drmLicenseUrl,
- drmKeyRequestProperties,
- Sample.toTrackTypeArray(drmSessionForClearTypes),
- drmMultiSession);
- Sample.SubtitleInfo subtitleInfo =
- subtitleUri == null
- ? null
- : new Sample.SubtitleInfo(
+
+ if (children != null) {
+ List<MediaItem> mediaItems = new ArrayList<>();
+ for (int i = 0; i < children.size(); i++) {
+ mediaItems.addAll(children.get(i).mediaItems);
+ }
+ return new PlaylistHolder(title, mediaItems);
+ } else {
+ mediaItem
+ .setSourceUri(uri)
+ .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build())
+ .setMimeType(IntentUtil.inferAdaptiveStreamMimeType(uri, extension))
+ .setTag(new IntentUtil.Tag(isLive, sphericalStereoMode));
+ if (subtitleUri != null) {
+ MediaItem.Subtitle subtitle =
+ new MediaItem.Subtitle(
subtitleUri,
- Assertions.checkNotNull(
+ checkNotNull(
subtitleMimeType, "subtitle_mime_type is required if subtitle_uri is set."),
subtitleLanguage);
- if (playlistSamples != null) {
- UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
- return new PlaylistSample(sampleName, playlistSamplesArray);
- } else {
- return new UriSample(
- sampleName,
- uri,
- extension,
- isLive,
- drmInfo,
- adTagUri != null ? Uri.parse(adTagUri) : null,
- sphericalStereoMode,
- subtitleInfo);
+ mediaItem.setSubtitles(Collections.singletonList(subtitle));
+ }
+ return new PlaylistHolder(title, Collections.singletonList(mediaItem.build()));
}
}
- private SampleGroup getGroup(String groupName, List<SampleGroup> groups) {
+ private PlaylistGroup getGroup(String groupName, List<PlaylistGroup> groups) {
for (int i = 0; i < groups.size(); i++) {
if (Util.areEqual(groupName, groups.get(i).title)) {
return groups.get(i);
}
}
- SampleGroup group = new SampleGroup(groupName);
+ PlaylistGroup group = new PlaylistGroup(groupName);
groups.add(group);
return group;
}
+
+ private int toTrackType(String trackTypeString) {
+ switch (Util.toLowerInvariant(trackTypeString)) {
+ case "audio":
+ return C.TRACK_TYPE_AUDIO;
+ case "video":
+ return C.TRACK_TYPE_VIDEO;
+ default:
+ throw new IllegalArgumentException("Invalid track type: " + trackTypeString);
+ }
+ }
}
private final class SampleAdapter extends BaseExpandableListAdapter implements OnClickListener {
- private List<SampleGroup> sampleGroups;
+ private List<PlaylistGroup> playlistGroups;
public SampleAdapter() {
- sampleGroups = Collections.emptyList();
+ playlistGroups = Collections.emptyList();
}
- public void setSampleGroups(List<SampleGroup> sampleGroups) {
- this.sampleGroups = sampleGroups;
+ public void setPlaylistGroups(List<PlaylistGroup> playlistGroups) {
+ this.playlistGroups = playlistGroups;
notifyDataSetChanged();
}
@Override
- public Sample getChild(int groupPosition, int childPosition) {
- return getGroup(groupPosition).samples.get(childPosition);
+ public PlaylistHolder getChild(int groupPosition, int childPosition) {
+ return getGroup(groupPosition).playlists.get(childPosition);
}
@Override
@@ -534,12 +540,12 @@
@Override
public int getChildrenCount(int groupPosition) {
- return getGroup(groupPosition).samples.size();
+ return getGroup(groupPosition).playlists.size();
}
@Override
- public SampleGroup getGroup(int groupPosition) {
- return sampleGroups.get(groupPosition);
+ public PlaylistGroup getGroup(int groupPosition) {
+ return playlistGroups.get(groupPosition);
}
@Override
@@ -562,7 +568,7 @@
@Override
public int getGroupCount() {
- return sampleGroups.size();
+ return playlistGroups.size();
}
@Override
@@ -577,18 +583,19 @@
@Override
public void onClick(View view) {
- onSampleDownloadButtonClicked((Sample) view.getTag());
+ onSampleDownloadButtonClicked((PlaylistHolder) view.getTag());
}
- private void initializeChildView(View view, Sample sample) {
- view.setTag(sample);
+ private void initializeChildView(View view, PlaylistHolder playlistHolder) {
+ view.setTag(playlistHolder);
TextView sampleTitle = view.findViewById(R.id.sample_title);
- sampleTitle.setText(sample.name);
+ sampleTitle.setText(playlistHolder.title);
- boolean canDownload = getDownloadUnsupportedStringId(sample) == 0;
- boolean isDownloaded = canDownload && downloadTracker.isDownloaded(((UriSample) sample).uri);
+ boolean canDownload = getDownloadUnsupportedStringId(playlistHolder) == 0;
+ boolean isDownloaded =
+ canDownload && downloadTracker.isDownloaded(playlistHolder.mediaItems.get(0));
ImageButton downloadButton = view.findViewById(R.id.download_button);
- downloadButton.setTag(sample);
+ downloadButton.setTag(playlistHolder);
downloadButton.setColorFilter(
canDownload ? (isDownloaded ? 0xFF42A5F5 : 0xFFBDBDBD) : 0xFF666666);
downloadButton.setImageResource(
@@ -596,14 +603,26 @@
}
}
- private static final class SampleGroup {
+ private static final class PlaylistHolder {
public final String title;
- public final List<Sample> samples;
+ public final List<MediaItem> mediaItems;
- public SampleGroup(String title) {
+ private PlaylistHolder(String title, List<MediaItem> mediaItems) {
+ Assertions.checkArgument(!mediaItems.isEmpty());
this.title = title;
- this.samples = new ArrayList<>();
+ this.mediaItems = Collections.unmodifiableList(new ArrayList<>(mediaItems));
+ }
+ }
+
+ private static final class PlaylistGroup {
+
+ public final String title;
+ public final List<PlaylistHolder> playlists;
+
+ public PlaylistGroup(String title) {
+ this.title = title;
+ this.playlists = new ArrayList<>();
}
}
}
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 2c25c32..a05dda1 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,7 @@
private final boolean preferGMSCoreCronet;
// Multi-catch can only be used for API 19+ in this case.
- // incompatible types in argument.
+ // Field#get(null) is blocked by the null-checker, but is safe because the field is static.
@SuppressWarnings({"UseMultiCatch", "nullness:argument.type.incompatible"})
public CronetProviderComparator(boolean preferGMSCoreCronet) {
// GMSCore CronetProvider classes are only available in some configurations.
diff --git a/tree/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/tree/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
index e203849..ed28a22 100644
--- a/tree/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
+++ b/tree/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
@@ -17,7 +17,6 @@
import static org.junit.Assert.fail;
-import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import org.junit.Before;
@@ -25,6 +24,8 @@
import org.junit.runner.RunWith;
/** Unit test for {@link FlacExtractor}. */
+// TODO(internal: b/26110951): Use org.junit.runners.Parameterized (and corresponding methods on
+// ExtractorAsserts) when it's supported by our testing infrastructure.
@RunWith(AndroidJUnit4.class)
public class FlacExtractorTest {
@@ -37,91 +38,79 @@
@Test
public void sample() throws Exception {
- ExtractorAsserts.assertBehavior(
- FlacExtractor::new,
- /* file= */ "flac/bear.flac",
- ApplicationProvider.getApplicationContext(),
- /* dumpFilesPrefix= */ "flac/bear_raw");
+ ExtractorAsserts.assertAllBehaviors(
+ FlacExtractor::new, /* file= */ "flac/bear.flac", /* dumpFilesPrefix= */ "flac/bear_raw");
}
@Test
public void sampleWithId3HeaderAndId3Enabled() throws Exception {
- ExtractorAsserts.assertBehavior(
+ ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_with_id3.flac",
- ApplicationProvider.getApplicationContext(),
/* dumpFilesPrefix= */ "flac/bear_with_id3_enabled_raw");
}
@Test
public void sampleWithId3HeaderAndId3Disabled() throws Exception {
- ExtractorAsserts.assertBehavior(
+ ExtractorAsserts.assertAllBehaviors(
() -> new FlacExtractor(FlacExtractor.FLAG_DISABLE_ID3_METADATA),
/* file= */ "flac/bear_with_id3.flac",
- ApplicationProvider.getApplicationContext(),
/* dumpFilesPrefix= */ "flac/bear_with_id3_disabled_raw");
}
@Test
public void sampleUnseekable() throws Exception {
- ExtractorAsserts.assertBehavior(
+ ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_no_seek_table_no_num_samples.flac",
- ApplicationProvider.getApplicationContext(),
/* dumpFilesPrefix= */ "flac/bear_no_seek_table_no_num_samples_raw");
}
@Test
public void sampleWithVorbisComments() throws Exception {
- ExtractorAsserts.assertBehavior(
+ ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_with_vorbis_comments.flac",
- ApplicationProvider.getApplicationContext(),
/* dumpFilesPrefix= */ "flac/bear_with_vorbis_comments_raw");
}
@Test
public void sampleWithPicture() throws Exception {
- ExtractorAsserts.assertBehavior(
+ ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_with_picture.flac",
- ApplicationProvider.getApplicationContext(),
/* dumpFilesPrefix= */ "flac/bear_with_picture_raw");
}
@Test
public void oneMetadataBlock() throws Exception {
- ExtractorAsserts.assertBehavior(
+ ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_one_metadata_block.flac",
- ApplicationProvider.getApplicationContext(),
/* dumpFilesPrefix= */ "flac/bear_one_metadata_block_raw");
}
@Test
public void noMinMaxFrameSize() throws Exception {
- ExtractorAsserts.assertBehavior(
+ ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_no_min_max_frame_size.flac",
- ApplicationProvider.getApplicationContext(),
/* dumpFilesPrefix= */ "flac/bear_no_min_max_frame_size_raw");
}
@Test
public void noNumSamples() throws Exception {
- ExtractorAsserts.assertBehavior(
+ ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_no_num_samples.flac",
- ApplicationProvider.getApplicationContext(),
/* dumpFilesPrefix= */ "flac/bear_no_num_samples_raw");
}
@Test
public void uncommonSampleRate() throws Exception {
- ExtractorAsserts.assertBehavior(
+ ExtractorAsserts.assertAllBehaviors(
FlacExtractor::new,
/* file= */ "flac/bear_uncommon_sample_rate.flac",
- ApplicationProvider.getApplicationContext(),
/* dumpFilesPrefix= */ "flac/bear_uncommon_sample_rate_raw");
}
}
diff --git a/tree/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/tree/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
index 04a12e5..947e891 100644
--- a/tree/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
+++ b/tree/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
@@ -472,7 +472,9 @@
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
adDisplayContainer = imaFactory.createAdDisplayContainer();
adDisplayContainer.setPlayer(/* videoAdPlayer= */ this);
- adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
+ adsLoader =
+ imaFactory.createAdsLoader(
+ context.getApplicationContext(), imaSdkSettings, adDisplayContainer);
adsLoader.addAdErrorListener(/* adErrorListener= */ this);
adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this);
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
diff --git a/tree/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/tree/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
index 913c1d8..fc75d4f 100644
--- a/tree/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
+++ b/tree/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
@@ -207,25 +207,25 @@
*
* @param mediaId The media id of the media item to be prepared.
* @param playWhenReady Whether playback should be started after preparation.
- * @param extras A {@link Bundle} of extras passed by the media controller.
+ * @param extras A {@link Bundle} of extras passed by the media controller, may be null.
*/
- void onPrepareFromMediaId(String mediaId, boolean playWhenReady, Bundle extras);
+ void onPrepareFromMediaId(String mediaId, boolean playWhenReady, @Nullable Bundle extras);
/**
* See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}.
*
* @param query The search query.
* @param playWhenReady Whether playback should be started after preparation.
- * @param extras A {@link Bundle} of extras passed by the media controller.
+ * @param extras A {@link Bundle} of extras passed by the media controller, may be null.
*/
- void onPrepareFromSearch(String query, boolean playWhenReady, Bundle extras);
+ void onPrepareFromSearch(String query, boolean playWhenReady, @Nullable Bundle extras);
/**
* See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}.
*
* @param uri The {@link Uri} of the media item to be prepared.
* @param playWhenReady Whether playback should be started after preparation.
- * @param extras A {@link Bundle} of extras passed by the media controller.
+ * @param extras A {@link Bundle} of extras passed by the media controller, may be null.
*/
- void onPrepareFromUri(Uri uri, boolean playWhenReady, Bundle extras);
+ void onPrepareFromUri(Uri uri, boolean playWhenReady, @Nullable Bundle extras);
}
/**
@@ -325,7 +325,7 @@
void onSetRating(Player player, RatingCompat rating);
/** See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat, Bundle)}. */
- void onSetRating(Player player, RatingCompat rating, Bundle extras);
+ void onSetRating(Player player, RatingCompat rating, @Nullable Bundle extras);
}
/** Handles requests for enabling or disabling captions. */
@@ -370,7 +370,7 @@
* @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching
* changes to the player.
* @param action The name of the action which was sent by a media controller.
- * @param extras Optional extras sent by a media controller.
+ * @param extras Optional extras sent by a media controller, may be null.
*/
void onCustomAction(
Player player, ControlDispatcher controlDispatcher, String action, @Nullable Bundle extras);
@@ -1284,42 +1284,42 @@
}
@Override
- public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+ public void onPrepareFromMediaId(String mediaId, @Nullable Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) {
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ false, extras);
}
}
@Override
- public void onPrepareFromSearch(String query, Bundle extras) {
+ public void onPrepareFromSearch(String query, @Nullable Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) {
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ false, extras);
}
}
@Override
- public void onPrepareFromUri(Uri uri, Bundle extras) {
+ public void onPrepareFromUri(Uri uri, @Nullable Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) {
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ false, extras);
}
}
@Override
- public void onPlayFromMediaId(String mediaId, Bundle extras) {
+ public void onPlayFromMediaId(String mediaId, @Nullable Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) {
playbackPreparer.onPrepareFromMediaId(mediaId, /* playWhenReady= */ true, extras);
}
}
@Override
- public void onPlayFromSearch(String query, Bundle extras) {
+ public void onPlayFromSearch(String query, @Nullable Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) {
playbackPreparer.onPrepareFromSearch(query, /* playWhenReady= */ true, extras);
}
}
@Override
- public void onPlayFromUri(Uri uri, Bundle extras) {
+ public void onPlayFromUri(Uri uri, @Nullable Bundle extras) {
if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) {
playbackPreparer.onPrepareFromUri(uri, /* playWhenReady= */ true, extras);
}
@@ -1333,7 +1333,7 @@
}
@Override
- public void onSetRating(RatingCompat rating, Bundle extras) {
+ public void onSetRating(RatingCompat rating, @Nullable Bundle extras) {
if (canDispatchSetRating()) {
ratingCallback.onSetRating(player, rating, extras);
}
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 f484d3f..e3fc32e 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
@@ -68,9 +68,11 @@
private boolean drmMultiSession;
private boolean drmPlayClearContentWithoutKey;
private List<Integer> drmSessionForClearTypes;
+ @Nullable private byte[] drmKeySetId;
private List<StreamKey> streamKeys;
@Nullable private String customCacheKey;
private List<Subtitle> subtitles;
+ @Nullable private Uri adTagUri;
@Nullable private Object tag;
@Nullable private MediaMetadata mediaMetadata;
@@ -88,12 +90,13 @@
clipEndPositionMs = mediaItem.clippingProperties.endPositionMs;
clipRelativeToLiveWindow = mediaItem.clippingProperties.relativeToLiveWindow;
clipRelativeToDefaultPosition = mediaItem.clippingProperties.relativeToDefaultPosition;
- clipStartsAtKeyFrame = mediaItem.clippingProperties.startsAtKeyFrame;
clipStartPositionMs = mediaItem.clippingProperties.startPositionMs;
+ clipStartsAtKeyFrame = mediaItem.clippingProperties.startsAtKeyFrame;
mediaId = mediaItem.mediaId;
mediaMetadata = mediaItem.mediaMetadata;
@Nullable PlaybackProperties playbackProperties = mediaItem.playbackProperties;
if (playbackProperties != null) {
+ adTagUri = playbackProperties.adTagUri;
customCacheKey = playbackProperties.customCacheKey;
mimeType = playbackProperties.mimeType;
sourceUri = playbackProperties.sourceUri;
@@ -108,6 +111,7 @@
drmPlayClearContentWithoutKey = drmConfiguration.playClearContentWithoutKey;
drmSessionForClearTypes = drmConfiguration.sessionForClearTypes;
drmUuid = drmConfiguration.uuid;
+ drmKeySetId = drmConfiguration.getKeySetId();
}
}
}
@@ -310,6 +314,20 @@
}
/**
+ * Sets the key set ID of the offline license.
+ *
+ * <p>The key set ID identifies an offline license. The ID is required to query, renew or
+ * release an existing offline license (see {@code DefaultDrmSessionManager#setMode(int
+ * mode,byte[] offlineLicenseKeySetId)}).
+ *
+ * <p>If no valid DRM configuration is specified, the key set ID is ignored.
+ */
+ public Builder setDrmKeySetId(@Nullable byte[] keySetId) {
+ this.drmKeySetId = keySetId != null ? Arrays.copyOf(keySetId, keySetId.length) : null;
+ return this;
+ }
+
+ /**
* Sets the optional stream keys by which the manifest is filtered (only used for adaptive
* streams).
*
@@ -354,6 +372,28 @@
}
/**
+ * Sets the optional ad tag URI.
+ *
+ * <p>If a {@link PlaybackProperties#sourceUri} is set, the ad tag URI is used to create a
+ * {@link PlaybackProperties} object. Otherwise it will be ignored.
+ */
+ public Builder setAdTagUri(@Nullable String adTagUri) {
+ this.adTagUri = adTagUri != null ? Uri.parse(adTagUri) : null;
+ return this;
+ }
+
+ /**
+ * Sets the optional ad tag {@link Uri}.
+ *
+ * <p>If a {@link PlaybackProperties#sourceUri} is set, the ad tag URI is used to create a
+ * {@link PlaybackProperties} object. Otherwise it will be ignored.
+ */
+ public Builder setAdTagUri(@Nullable Uri adTagUri) {
+ this.adTagUri = adTagUri;
+ return this;
+ }
+
+ /**
* Sets the optional tag for custom attributes. The tag for the media source which will be
* published in the {@code com.google.android.exoplayer2.Timeline} of the source as {@code
* com.google.android.exoplayer2.Timeline.Window#tag}.
@@ -390,11 +430,13 @@
drmLicenseRequestHeaders,
drmMultiSession,
drmPlayClearContentWithoutKey,
- drmSessionForClearTypes)
+ drmSessionForClearTypes,
+ drmKeySetId)
: null,
streamKeys,
customCacheKey,
subtitles,
+ adTagUri,
tag);
mediaId = mediaId != null ? mediaId : sourceUri.toString();
}
@@ -438,19 +480,29 @@
/** The types of clear tracks for which to use a drm session. */
public final List<Integer> sessionForClearTypes;
+ @Nullable private final byte[] keySetId;
+
private DrmConfiguration(
UUID uuid,
@Nullable Uri licenseUri,
Map<String, String> requestHeaders,
boolean multiSession,
boolean playClearContentWithoutKey,
- List<Integer> drmSessionForClearTypes) {
+ List<Integer> drmSessionForClearTypes,
+ @Nullable byte[] keySetId) {
this.uuid = uuid;
this.licenseUri = licenseUri;
this.requestHeaders = requestHeaders;
this.multiSession = multiSession;
this.playClearContentWithoutKey = playClearContentWithoutKey;
this.sessionForClearTypes = drmSessionForClearTypes;
+ this.keySetId = keySetId != null ? Arrays.copyOf(keySetId, keySetId.length) : null;
+ }
+
+ /** Returns the key set ID of the offline license. */
+ @Nullable
+ public byte[] getKeySetId() {
+ return keySetId != null ? Arrays.copyOf(keySetId, keySetId.length) : null;
}
@Override
@@ -468,7 +520,8 @@
&& Util.areEqual(requestHeaders, other.requestHeaders)
&& multiSession == other.multiSession
&& playClearContentWithoutKey == other.playClearContentWithoutKey
- && sessionForClearTypes.equals(other.sessionForClearTypes);
+ && sessionForClearTypes.equals(other.sessionForClearTypes)
+ && Arrays.equals(keySetId, other.keySetId);
}
@Override
@@ -479,6 +532,7 @@
result = 31 * result + (multiSession ? 1 : 0);
result = 31 * result + (playClearContentWithoutKey ? 1 : 0);
result = 31 * result + sessionForClearTypes.hashCode();
+ result = 31 * result + Arrays.hashCode(keySetId);
return result;
}
}
@@ -509,6 +563,9 @@
/** Optional subtitles to be sideloaded. */
public final List<Subtitle> subtitles;
+ /** Optional ad tag {@link Uri}. */
+ @Nullable public final Uri adTagUri;
+
/**
* Optional tag for custom attributes. The tag for the media source which will be published in
* the {@code com.google.android.exoplayer2.Timeline} of the source as {@code
@@ -523,6 +580,7 @@
List<StreamKey> streamKeys,
@Nullable String customCacheKey,
List<Subtitle> subtitles,
+ @Nullable Uri adTagUri,
@Nullable Object tag) {
this.sourceUri = sourceUri;
this.mimeType = mimeType;
@@ -530,6 +588,7 @@
this.streamKeys = streamKeys;
this.customCacheKey = customCacheKey;
this.subtitles = subtitles;
+ this.adTagUri = adTagUri;
this.tag = tag;
}
@@ -549,6 +608,7 @@
&& streamKeys.equals(other.streamKeys)
&& Util.areEqual(customCacheKey, other.customCacheKey)
&& subtitles.equals(other.subtitles)
+ && Util.areEqual(adTagUri, other.adTagUri)
&& Util.areEqual(tag, other.tag);
}
@@ -560,6 +620,7 @@
result = 31 * result + streamKeys.hashCode();
result = 31 * result + (customCacheKey == null ? 0 : customCacheKey.hashCode());
result = 31 * result + subtitles.hashCode();
+ result = 31 * result + (adTagUri == null ? 0 : adTagUri.hashCode());
result = 31 * result + (tag == null ? 0 : tag.hashCode());
return result;
}
diff --git a/tree/library/common/src/main/java/com/google/android/exoplayer2/audio/AacUtil.java b/tree/library/common/src/main/java/com/google/android/exoplayer2/audio/AacUtil.java
index 024f848..8de4230 100644
--- a/tree/library/common/src/main/java/com/google/android/exoplayer2/audio/AacUtil.java
+++ b/tree/library/common/src/main/java/com/google/android/exoplayer2/audio/AacUtil.java
@@ -15,11 +15,15 @@
*/
package com.google.android.exoplayer2.audio;
+import androidx.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableBitArray;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/** Utility methods for handling AAC audio streams. */
public final class AacUtil {
@@ -132,19 +136,37 @@
private static final String CODECS_STRING_PREFIX = "mp4a.40.";
// Advanced Audio Coding Low-Complexity profile.
- private static final int AUDIO_OBJECT_TYPE_AAC_LC = 2;
+ public static final int AUDIO_OBJECT_TYPE_AAC_LC = 2;
// Spectral Band Replication.
- private static final int AUDIO_OBJECT_TYPE_AAC_SBR = 5;
+ public static final int AUDIO_OBJECT_TYPE_AAC_SBR = 5;
// Error Resilient Bit-Sliced Arithmetic Coding.
- private static final int AUDIO_OBJECT_TYPE_AAC_ER_BSAC = 22;
+ public static final int AUDIO_OBJECT_TYPE_AAC_ER_BSAC = 22;
// Enhanced low delay.
- private static final int AUDIO_OBJECT_TYPE_AAC_ELD = 23;
+ public static final int AUDIO_OBJECT_TYPE_AAC_ELD = 23;
// Parametric Stereo.
- private static final int AUDIO_OBJECT_TYPE_AAC_PS = 29;
+ public static final int AUDIO_OBJECT_TYPE_AAC_PS = 29;
// Escape code for extended audio object types.
private static final int AUDIO_OBJECT_TYPE_ESCAPE = 31;
// Extended high efficiency.
- private static final int AUDIO_OBJECT_TYPE_AAC_XHE = 42;
+ public static final int AUDIO_OBJECT_TYPE_AAC_XHE = 42;
+
+ /**
+ * Valid AAC Audio object types. One of {@link #AUDIO_OBJECT_TYPE_AAC_LC}, {@link
+ * #AUDIO_OBJECT_TYPE_AAC_SBR}, {@link #AUDIO_OBJECT_TYPE_AAC_ER_BSAC}, {@link
+ * #AUDIO_OBJECT_TYPE_AAC_ELD}, {@link #AUDIO_OBJECT_TYPE_AAC_PS} or {@link
+ * #AUDIO_OBJECT_TYPE_AAC_XHE}.
+ */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ AUDIO_OBJECT_TYPE_AAC_LC,
+ AUDIO_OBJECT_TYPE_AAC_SBR,
+ AUDIO_OBJECT_TYPE_AAC_ER_BSAC,
+ AUDIO_OBJECT_TYPE_AAC_ELD,
+ AUDIO_OBJECT_TYPE_AAC_PS,
+ AUDIO_OBJECT_TYPE_AAC_XHE
+ })
+ public @interface AacAudioObjectType {}
/**
* Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
@@ -275,7 +297,7 @@
/** Returns the encoding for a given AAC audio object type. */
@C.Encoding
- public static int getEncodingForAudioObjectType(int audioObjectType) {
+ public static int getEncodingForAudioObjectType(@AacAudioObjectType int audioObjectType) {
switch (audioObjectType) {
case AUDIO_OBJECT_TYPE_AAC_LC:
return C.ENCODING_AAC_LC;
diff --git a/tree/library/common/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/tree/library/common/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
index d4042a9..f9a97d9 100644
--- a/tree/library/common/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
+++ b/tree/library/common/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
@@ -24,6 +24,7 @@
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -516,7 +517,7 @@
int endIndex = buffer.limit() - TRUEHD_SYNCFRAME_PREFIX_LENGTH;
for (int i = startIndex; i <= endIndex; i++) {
// The syncword ends 0xBA for TrueHD or 0xBB for MLP.
- if ((buffer.getInt(i + 4) & 0xFEFFFFFF) == 0xBA6F72F8) {
+ if ((Util.getBigEndianInt(buffer, i + 4) & 0xFFFFFFFE) == 0xF8726FBA) {
return i - startIndex;
}
}
diff --git a/tree/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/tree/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java
index 60fe1a3..a074324 100644
--- a/tree/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java
+++ b/tree/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java
@@ -60,6 +60,8 @@
import java.io.InputStream;
import java.lang.reflect.Method;
import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
@@ -1667,6 +1669,29 @@
}
/**
+ * Makes a best guess to infer the type from a {@link Uri} and MIME type.
+ *
+ * @param uri The {@link Uri}.
+ * @param mimeType If not null, used to infer the type.
+ * @return The content type.
+ */
+ public static int inferContentTypeWithMimeType(Uri uri, @Nullable String mimeType) {
+ if (mimeType == null) {
+ return Util.inferContentType(uri);
+ }
+ switch (mimeType) {
+ case MimeTypes.APPLICATION_MPD:
+ return C.TYPE_DASH;
+ case MimeTypes.APPLICATION_M3U8:
+ return C.TYPE_HLS;
+ case MimeTypes.APPLICATION_SS:
+ return C.TYPE_SS;
+ default:
+ return Util.inferContentType(uri);
+ }
+ }
+
+ /**
* Returns the specified millisecond time formatted as a string.
*
* @param builder The builder that {@code formatter} will write to.
@@ -1860,6 +1885,21 @@
}
/**
+ * Absolute <i>get</i> method for reading an int value in {@link ByteOrder#BIG_ENDIAN} in a {@link
+ * ByteBuffer}. Same as {@link ByteBuffer#getInt(int)} except the buffer's order as returned by
+ * {@link ByteBuffer#order()} is ignored and {@link ByteOrder#BIG_ENDIAN} is used instead.
+ *
+ * @param buffer The buffer from which to read an int in big endian.
+ * @param index The index from which the bytes will be read.
+ * @return The int value at the given index with the buffer bytes ordered most significant to
+ * least significant.
+ */
+ public static int getBigEndianInt(ByteBuffer buffer, int index) {
+ int value = buffer.getInt(index);
+ return buffer.order() == ByteOrder.BIG_ENDIAN ? value : Integer.reverseBytes(value);
+ }
+
+ /**
* Returns the {@link C.NetworkType} of the current network connection.
*
* @param context A context to access the connectivity manager.
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 adfbc60..c7a55b8 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
@@ -91,6 +91,7 @@
Uri licenseUri = Uri.parse(URI_STRING);
Map<String, String> requestHeaders = new HashMap<>();
requestHeaders.put("Referer", "http://www.google.com");
+ byte[] keySetId = new byte[] {1, 2, 3};
MediaItem mediaItem =
new MediaItem.Builder()
.setSourceUri(URI_STRING)
@@ -100,6 +101,7 @@
.setDrmMultiSession(/* multiSession= */ true)
.setDrmPlayClearContentWithoutKey(true)
.setDrmSessionForClearTypes(Collections.singletonList(C.TRACK_TYPE_AUDIO))
+ .setDrmKeySetId(keySetId)
.build();
assertThat(mediaItem.playbackProperties.drmConfiguration).isNotNull();
@@ -111,6 +113,7 @@
assertThat(mediaItem.playbackProperties.drmConfiguration.playClearContentWithoutKey).isTrue();
assertThat(mediaItem.playbackProperties.drmConfiguration.sessionForClearTypes)
.containsExactly(C.TRACK_TYPE_AUDIO);
+ assertThat(mediaItem.playbackProperties.drmConfiguration.getKeySetId()).isEqualTo(keySetId);
}
@Test
@@ -267,6 +270,16 @@
}
@Test
+ public void builderSetAdTagUri_setsAdTagUri() {
+ Uri adTagUri = Uri.parse(URI_STRING + "/ad");
+
+ MediaItem mediaItem =
+ new MediaItem.Builder().setSourceUri(URI_STRING).setAdTagUri(adTagUri).build();
+
+ assertThat(mediaItem.playbackProperties.adTagUri).isEqualTo(adTagUri);
+ }
+
+ @Test
public void builderSetMediaMetadata_setsMetadata() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build();
@@ -280,6 +293,7 @@
public void buildUpon_equalsToOriginal() {
MediaItem mediaItem =
new MediaItem.Builder()
+ .setAdTagUri(URI_STRING)
.setClipEndPositionMs(1000)
.setClipRelativeToDefaultPosition(true)
.setClipRelativeToLiveWindow(true)
@@ -293,6 +307,7 @@
.setDrmMultiSession(true)
.setDrmPlayClearContentWithoutKey(true)
.setDrmSessionForClearTypes(Collections.singletonList(C.TRACK_TYPE_AUDIO))
+ .setDrmKeySetId(new byte[] {1, 2, 3})
.setMediaId("mediaId")
.setMediaMetadata(new MediaMetadata.Builder().setTitle("title").build())
.setMimeType(MimeTypes.APPLICATION_MP4)
diff --git a/tree/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/tree/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java
index 825988c..2e523a3 100644
--- a/tree/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java
+++ b/tree/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java
@@ -27,6 +27,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
@@ -790,6 +792,30 @@
}
@Test
+ public void getBigEndianInt_fromBigEndian() {
+ byte[] bytes = {0x1F, 0x2E, 0x3D, 0x4C};
+ ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
+
+ assertThat(Util.getBigEndianInt(byteBuffer, 0)).isEqualTo(0x1F2E3D4C);
+ }
+
+ @Test
+ public void getBigEndianInt_fromLittleEndian() {
+ byte[] bytes = {(byte) 0xC2, (byte) 0xD3, (byte) 0xE4, (byte) 0xF5};
+ ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertThat(Util.getBigEndianInt(byteBuffer, 0)).isEqualTo(0xC2D3E4F5);
+ }
+
+ @Test
+ public void getBigEndianInt_unaligned() {
+ byte[] bytes = {9, 8, 7, 6, 5};
+ ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertThat(Util.getBigEndianInt(byteBuffer, 1)).isEqualTo(0x08070605);
+ }
+
+ @Test
public void inflate_withDeflatedData_success() {
byte[] testData = TestUtil.buildTestData(/*arbitrary test data size*/ 256 * 1024);
byte[] compressedData = new byte[testData.length * 2];
diff --git a/tree/library/core/proguard-rules.txt b/tree/library/core/proguard-rules.txt
index 36038b9..cbeb74c 100644
--- a/tree/library/core/proguard-rules.txt
+++ b/tree/library/core/proguard-rules.txt
@@ -51,15 +51,15 @@
# Constructors accessed via reflection in DefaultDownloaderFactory
-dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloader
-keepclassmembers class com.google.android.exoplayer2.source.dash.offline.DashDownloader {
- <init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper);
+ <init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.upstream.cache.CacheDataSource.Factory, java.util.concurrent.Executor);
}
-dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloader
-keepclassmembers class com.google.android.exoplayer2.source.hls.offline.HlsDownloader {
- <init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper);
+ <init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.upstream.cache.CacheDataSource.Factory, java.util.concurrent.Executor);
}
-dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader
-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader {
- <init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper);
+ <init>(android.net.Uri, java.util.List, com.google.android.exoplayer2.upstream.cache.CacheDataSource.Factory, java.util.concurrent.Executor);
}
# Constructors accessed via reflection in DefaultMediaSourceFactory and DownloadHelper
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
index ce35c89..5eb1402 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
@@ -370,7 +370,8 @@
}
@Override
- public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
+ public boolean shouldContinueLoading(
+ long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferBytes;
long minBufferUs = this.minBufferUs;
if (playbackSpeed > 1) {
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
index ccdddb8..a09f85d 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
@@ -89,7 +89,7 @@
private long allowedVideoJoiningTimeMs;
private boolean enableDecoderFallback;
private MediaCodecSelector mediaCodecSelector;
- @MediaCodecRenderer.MediaCodecOperationMode private int mediaCodecOperationMode;
+ private @MediaCodecRenderer.MediaCodecOperationMode int mediaCodecOperationMode;
/** @param context A {@link Context}. */
public DefaultRenderersFactory(Context context) {
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 0344b09..2d8ea2e 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
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2;
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+
import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Looper;
@@ -28,6 +30,7 @@
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
@@ -84,6 +87,7 @@
private SeekParameters seekParameters;
private ShuffleOrder shuffleOrder;
private boolean pauseAtEndOfMediaItems;
+ private boolean hasAdsMediaSource;
// Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo;
@@ -123,8 +127,8 @@
Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
+ ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]");
Assertions.checkState(renderers.length > 0);
- this.renderers = Assertions.checkNotNull(renderers);
- this.trackSelector = Assertions.checkNotNull(trackSelector);
+ this.renderers = checkNotNull(renderers);
+ this.trackSelector = checkNotNull(trackSelector);
this.mediaSourceFactory = mediaSourceFactory;
this.useLazyPreparation = useLazyPreparation;
repeatMode = Player.REPEAT_MODE_OFF;
@@ -397,9 +401,7 @@
@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);
- }
+ validateMediaSources(mediaSources, /* mediaSourceReplacement= */ false);
int currentWindowIndex = getCurrentWindowIndex();
long currentPositionMs = getCurrentPosition();
Timeline oldTimeline = getCurrentTimeline();
@@ -973,9 +975,7 @@
int startWindowIndex,
long startPositionMs,
boolean resetToDefaultPosition) {
- for (int i = 0; i < mediaSources.size(); i++) {
- Assertions.checkArgument(mediaSources.get(i) != null);
- }
+ validateMediaSources(mediaSources, /* mediaSourceReplacement= */ true);
int currentWindowIndex = getCurrentWindowIndexInternal();
long currentPositionMs = getCurrentPosition();
pendingOperationAcks++;
@@ -1076,9 +1076,42 @@
removed.add(mediaSourceHolders.remove(i));
}
shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndexExclusive);
+ if (mediaSourceHolders.isEmpty()) {
+ hasAdsMediaSource = false;
+ }
return removed;
}
+ /**
+ * Validates media sources before any modification of the existing list of media sources is made.
+ * This way we can throw an exception before changing the state of the player in case of a
+ * validation failure.
+ *
+ * @param mediaSources The media sources to set or add.
+ * @param mediaSourceReplacement Whether the given media sources will replace existing ones.
+ */
+ private void validateMediaSources(
+ List<MediaSource> mediaSources, boolean mediaSourceReplacement) {
+ if (hasAdsMediaSource && !mediaSourceReplacement && !mediaSources.isEmpty()) {
+ // Adding media sources to an ads media source is not allowed
+ // (see https://github.com/google/ExoPlayer/issues/3750).
+ throw new IllegalStateException();
+ }
+ int sizeAfterModification =
+ mediaSources.size() + (mediaSourceReplacement ? 0 : mediaSourceHolders.size());
+ for (int i = 0; i < mediaSources.size(); i++) {
+ MediaSource mediaSource = checkNotNull(mediaSources.get(i));
+ if (mediaSource instanceof AdsMediaSource) {
+ if (sizeAfterModification > 1) {
+ // Ads media sources only allowed with a single source
+ // (see https://github.com/google/ExoPlayer/issues/3750).
+ throw new IllegalArgumentException();
+ }
+ hasAdsMediaSource = true;
+ }
+ }
+ }
+
private PlaybackInfo maskTimeline() {
return playbackInfo.copyWithTimeline(
mediaSourceHolders.isEmpty()
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index 56cce6d..02e271e 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -870,7 +870,7 @@
}
}
if (throwWhenStuckBuffering
- && !shouldContinueLoading
+ && !playbackInfo.isLoading
&& playbackInfo.totalBufferedDurationUs < 500_000
&& isLoadingPossible()) {
// Throw if the LoadControl prevents loading even if the buffer is empty or almost empty. We
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java
index d91830a..94f61bb 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java
@@ -87,28 +87,20 @@
*/
boolean retainBackBufferFromKeyframe();
- /** @deprecated Use {@link LoadControl#shouldContinueLoading(long, long, float)}. */
- @Deprecated
- default boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
- return false;
- }
-
/**
* Called by the player to determine whether it should continue to load the source.
*
* @param playbackPositionUs The current playback position in microseconds, relative to the start
* of the {@link Timeline.Period period} that will continue to be loaded if this method
- * returns {@code true}. If the playback for this period has not yet started, the value will
+ * returns {@code true}. If playback of this period has not yet started, the value will be
* negative and equal in magnitude to the duration of any media in previous periods still to
* be played.
* @param bufferedDurationUs The duration of media that's currently buffered.
* @param playbackSpeed The current playback speed.
* @return Whether the loading should continue.
*/
- default boolean shouldContinueLoading(
- long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
- return shouldContinueLoading(bufferedDurationUs, playbackSpeed);
- }
+ boolean shouldContinueLoading(
+ long playbackPositionUs, long bufferedDurationUs, float playbackSpeed);
/**
* Called repeatedly by the player when it's loading the source, has yet to start playback, and
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 f692629..83609e9 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
@@ -582,10 +582,10 @@
default void onPlaybackSpeedChanged(float playbackSpeed) {}
/**
- * Called when all pending seek requests have been processed by the player. This is guaranteed
- * to happen after any necessary changes to the player state were reported to {@link
- * #onPlaybackStateChanged(int)}.
+ * @deprecated Seeks are processed without delay. Listen to {@link
+ * #onPositionDiscontinuity(int)} with reason {@link #DISCONTINUITY_REASON_SEEK} instead.
*/
+ @Deprecated
default void onSeekProcessed() {}
}
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 67c4b88..bddfa65 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
@@ -100,6 +100,7 @@
private AnalyticsCollector analyticsCollector;
private Looper looper;
private boolean useLazyPreparation;
+ private boolean throwWhenStuckBuffering;
private boolean buildCalled;
/**
@@ -295,6 +296,19 @@
}
/**
+ * Sets whether the player should throw when it detects it's stuck buffering.
+ *
+ * <p>This method is experimental, and will be renamed or removed in a future release.
+ *
+ * @param throwWhenStuckBuffering Whether to throw when the player detects it's stuck buffering.
+ * @return This builder.
+ */
+ public Builder experimental_setThrowWhenStuckBuffering(boolean throwWhenStuckBuffering) {
+ this.throwWhenStuckBuffering = throwWhenStuckBuffering;
+ return this;
+ }
+
+ /**
* Sets the {@link Clock} that will be used by the player. Should only be set for testing
* purposes.
*
@@ -384,6 +398,9 @@
builder.useLazyPreparation,
builder.clock,
builder.looper);
+ if (builder.throwWhenStuckBuffering) {
+ player.experimental_throwWhenStuckBuffering();
+ }
}
/**
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 59ab3f1..66216de 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
@@ -53,6 +53,7 @@
@C.StreamType private int streamType;
private int volume;
private boolean muted;
+ private boolean released;
/** Creates a manager. */
public StreamVolumeManager(Context context, Handler eventHandler, Listener listener) {
@@ -158,7 +159,11 @@
/** Releases the manager. It must be called when the manager is no longer required. */
public void release() {
+ if (released) {
+ return;
+ }
applicationContext.unregisterReceiver(receiver);
+ released = true;
}
private void updateVolumeAndNotifyIfChanged() {
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
index 2af577f..715a1c0 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java
@@ -554,6 +554,7 @@
}
}
+ @SuppressWarnings("deprecation")
@Override
public final void onSeekProcessed() {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java
index 77bc211..0b841ab 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java
@@ -190,10 +190,11 @@
default void onSeekStarted(EventTime eventTime) {}
/**
- * Called when a seek operation was processed.
- *
- * @param eventTime The event time.
+ * @deprecated Seeks are processed without delay. Listen to {@link
+ * #onPositionDiscontinuity(EventTime, int)} with reason {@link
+ * Player#DISCONTINUITY_REASON_SEEK} instead.
*/
+ @Deprecated
default void onSeekProcessed(EventTime eventTime) {}
/**
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 e9baef3..04536bb 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
@@ -202,6 +202,20 @@
}
}
+ @Override
+ public void finishAllSessions(EventTime eventTime) {
+ currentSessionId = null;
+ Iterator<SessionDescriptor> iterator = sessions.values().iterator();
+ while (iterator.hasNext()) {
+ SessionDescriptor session = iterator.next();
+ iterator.remove();
+ if (session.isCreated && listener != null) {
+ listener.onSessionFinished(
+ eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false);
+ }
+ }
+ }
+
private SessionDescriptor getOrAddSession(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
// There should only be one matching session if mediaPeriodId is non-null. If mediaPeriodId is
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java
index 53d63e2..7045779 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java
@@ -117,4 +117,12 @@
* @param reason The {@link DiscontinuityReason}.
*/
void handlePositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason);
+
+ /**
+ * Finishes all existing sessions and calls their respective {@link
+ * Listener#onSessionFinished(EventTime, String, boolean)} callback.
+ *
+ * @param eventTime The event time at which sessions are finished.
+ */
+ void finishAllSessions(EventTime eventTime);
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java
index 893ecb0..3b1e056 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java
@@ -163,9 +163,8 @@
* #PLAYBACK_STATE_JOINING_FOREGROUND}, {@link #PLAYBACK_STATE_JOINING_BACKGROUND}, {@link
* #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_SEEKING},
* {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_PAUSED_BUFFERING}, {@link
- * #PLAYBACK_STATE_SEEK_BUFFERING}, {@link #PLAYBACK_STATE_SUPPRESSED}, {@link
- * #PLAYBACK_STATE_SUPPRESSED_BUFFERING}, {@link #PLAYBACK_STATE_ENDED}, {@link
- * #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED}, {@link
+ * #PLAYBACK_STATE_SUPPRESSED}, {@link #PLAYBACK_STATE_SUPPRESSED_BUFFERING}, {@link
+ * #PLAYBACK_STATE_ENDED}, {@link #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED}, {@link
* #PLAYBACK_STATE_INTERRUPTED_BY_AD} or {@link #PLAYBACK_STATE_ABANDONED}.
*/
@Documented
@@ -180,7 +179,6 @@
PLAYBACK_STATE_SEEKING,
PLAYBACK_STATE_BUFFERING,
PLAYBACK_STATE_PAUSED_BUFFERING,
- PLAYBACK_STATE_SEEK_BUFFERING,
PLAYBACK_STATE_SUPPRESSED,
PLAYBACK_STATE_SUPPRESSED_BUFFERING,
PLAYBACK_STATE_ENDED,
@@ -206,8 +204,6 @@
public static final int PLAYBACK_STATE_BUFFERING = 6;
/** Playback is buffering while paused. */
public static final int PLAYBACK_STATE_PAUSED_BUFFERING = 7;
- /** Playback is buffering after a seek. */
- public static final int PLAYBACK_STATE_SEEK_BUFFERING = 8;
/** Playback is suppressed (e.g. due to audio focus loss). */
public static final int PLAYBACK_STATE_SUPPRESSED = 9;
/** Playback is suppressed (e.g. due to audio focus loss) while buffering to resume a playback. */
@@ -769,8 +765,7 @@
* milliseconds.
*/
public long getTotalSeekTimeMs() {
- return getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING)
- + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING);
+ return getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING);
}
/**
@@ -799,8 +794,7 @@
public long getTotalWaitTimeMs() {
return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND)
+ getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING)
- + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING)
- + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING);
+ + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING);
}
/**
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java
index 97805da..0524f4d 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java
@@ -83,7 +83,7 @@
@Player.State private int playbackState;
private boolean isSuppressed;
private float playbackSpeed;
- private boolean isSeeking;
+ private boolean onSeekStartedCalled;
/**
* Creates listener for playback stats.
@@ -150,7 +150,6 @@
// TODO: Add AnalyticsListener.onAttachedToPlayer and onDetachedFromPlayer to auto-release with
// an actual EventTime. Should also simplify other cases where the listener needs to be released
// separately from the player.
- HashMap<String, PlaybackStatsTracker> trackerCopy = new HashMap<>(playbackStatsTrackers);
EventTime dummyEventTime =
new EventTime(
SystemClock.elapsedRealtime(),
@@ -160,9 +159,7 @@
/* eventPlaybackPositionMs= */ 0,
/* currentPlaybackPositionMs= */ 0,
/* totalBufferedDurationMs= */ 0);
- for (String session : trackerCopy.keySet()) {
- onSessionFinished(dummyEventTime, session, /* automaticTransition= */ false);
- }
+ sessionManager.finishAllSessions(dummyEventTime);
}
// PlaybackSessionManager.Listener implementation.
@@ -170,7 +167,7 @@
@Override
public void onSessionCreated(EventTime eventTime, String session) {
PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime);
- if (isSeeking) {
+ if (onSeekStartedCalled) {
tracker.onSeekStarted(eventTime, /* belongsToPlayback= */ true);
}
tracker.onPlaybackStateChanged(eventTime, playbackState, /* belongsToPlayback= */ true);
@@ -245,7 +242,7 @@
@Override
public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {
playbackState = state;
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
playbackStatsTrackers
@@ -258,7 +255,7 @@
public void onPlayWhenReadyChanged(
EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
this.playWhenReady = playWhenReady;
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
playbackStatsTrackers
@@ -269,9 +266,9 @@
@Override
public void onPlaybackSuppressionReasonChanged(
- EventTime eventTime, int playbackSuppressionReason) {
+ EventTime eventTime, @Player.PlaybackSuppressionReason int playbackSuppressionReason) {
isSuppressed = playbackSuppressionReason != Player.PLAYBACK_SUPPRESSION_REASON_NONE;
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
playbackStatsTrackers
@@ -281,50 +278,46 @@
}
@Override
- public void onTimelineChanged(EventTime eventTime, int reason) {
+ public void onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason) {
sessionManager.handleTimelineUpdate(eventTime);
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
- playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime);
+ playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime, /* isSeek= */ false);
}
}
}
@Override
- public void onPositionDiscontinuity(EventTime eventTime, int reason) {
+ public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) {
sessionManager.handlePositionDiscontinuity(eventTime, reason);
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
+ if (reason == Player.DISCONTINUITY_REASON_SEEK) {
+ onSeekStartedCalled = false;
+ }
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
- playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime);
+ playbackStatsTrackers
+ .get(session)
+ .onPositionDiscontinuity(
+ eventTime, /* isSeek= */ reason == Player.DISCONTINUITY_REASON_SEEK);
}
}
}
@Override
public void onSeekStarted(EventTime eventTime) {
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
playbackStatsTrackers.get(session).onSeekStarted(eventTime, belongsToPlayback);
}
- isSeeking = true;
- }
-
- @Override
- public void onSeekProcessed(EventTime eventTime) {
- sessionManager.updateSessions(eventTime);
- for (String session : playbackStatsTrackers.keySet()) {
- boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session);
- playbackStatsTrackers.get(session).onSeekProcessed(eventTime, belongsToPlayback);
- }
- isSeeking = false;
+ onSeekStartedCalled = true;
}
@Override
public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onFatalError(eventTime, error);
@@ -335,7 +328,7 @@
@Override
public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) {
this.playbackSpeed = playbackSpeed;
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) {
tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed);
}
@@ -344,7 +337,7 @@
@Override
public void onTracksChanged(
EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections);
@@ -355,7 +348,7 @@
@Override
public void onLoadStarted(
EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onLoadStarted(eventTime);
@@ -365,7 +358,7 @@
@Override
public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onDownstreamFormatChanged(eventTime, mediaLoadData);
@@ -380,7 +373,7 @@
int height,
int unappliedRotationDegrees,
float pixelWidthHeightRatio) {
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onVideoSizeChanged(eventTime, width, height);
@@ -391,7 +384,7 @@
@Override
public void onBandwidthEstimate(
EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onBandwidthData(totalLoadTimeMs, totalBytesLoaded);
@@ -402,7 +395,7 @@
@Override
public void onAudioUnderrun(
EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onAudioUnderrun();
@@ -412,7 +405,7 @@
@Override
public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onDroppedVideoFrames(droppedFrames);
@@ -427,7 +420,7 @@
MediaLoadData mediaLoadData,
IOException error,
boolean wasCanceled) {
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onNonFatalError(eventTime, error);
@@ -437,7 +430,7 @@
@Override
public void onDrmSessionManagerError(EventTime eventTime, Exception error) {
- sessionManager.updateSessions(eventTime);
+ maybeAddSession(eventTime);
for (String session : playbackStatsTrackers.keySet()) {
if (sessionManager.belongsToSession(eventTime, session)) {
playbackStatsTrackers.get(session).onNonFatalError(eventTime, error);
@@ -445,6 +438,13 @@
}
}
+ private void maybeAddSession(EventTime eventTime) {
+ boolean isCompletelyIdle = eventTime.timeline.isEmpty() && playbackState == Player.STATE_IDLE;
+ if (!isCompletelyIdle) {
+ sessionManager.updateSessions(eventTime);
+ }
+ }
+
/** Tracker for playback stats of a single playback. */
private static final class PlaybackStatsTracker {
@@ -544,6 +544,9 @@
if (state != Player.STATE_IDLE) {
hasFatalError = false;
}
+ if (state != Player.STATE_BUFFERING) {
+ isSeeking = false;
+ }
if (state == Player.STATE_IDLE || state == Player.STATE_ENDED) {
isInterruptedByAd = false;
}
@@ -582,8 +585,12 @@
* Notifies the tracker of a position discontinuity or timeline update for the current playback.
*
* @param eventTime The {@link EventTime}.
+ * @param isSeek Whether the position discontinuity is for a seek.
*/
- public void onPositionDiscontinuity(EventTime eventTime) {
+ public void onPositionDiscontinuity(EventTime eventTime, boolean isSeek) {
+ if (isSeek && playerPlaybackState == Player.STATE_IDLE) {
+ isSeeking = false;
+ }
isInterruptedByAd = false;
maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true);
}
@@ -601,18 +608,6 @@
}
/**
- * Notifies the tracker that a seek has been processed, including all seeks while the playback
- * is not in the foreground.
- *
- * @param eventTime The {@link EventTime}.
- * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback.
- */
- public void onSeekProcessed(EventTime eventTime, boolean belongsToPlayback) {
- isSeeking = false;
- maybeUpdatePlaybackState(eventTime, belongsToPlayback);
- }
-
- /**
* Notifies the tracker of fatal player error in the current playback.
*
* @param eventTime The {@link EventTime}.
@@ -927,10 +922,6 @@
|| currentPlaybackState == PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) {
return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND;
}
- if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING
- || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING) {
- return PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING;
- }
if (!playWhenReady) {
return PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING;
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
index a0aebdf..78699d4 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
@@ -1242,7 +1242,8 @@
private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) {
switch (encoding) {
case C.ENCODING_MP3:
- return MpegAudioUtil.parseMpegAudioFrameSampleCount(buffer.get(buffer.position()));
+ int headerDataInBigEndian = Util.getBigEndianInt(buffer, buffer.position());
+ return MpegAudioUtil.parseMpegAudioFrameSampleCount(headerDataInBigEndian);
case C.ENCODING_AAC_LC:
return AacUtil.AAC_LC_AUDIO_SAMPLE_COUNT;
case C.ENCODING_AAC_HE_V1:
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java
index 8d84325..f630c26 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java
@@ -155,18 +155,20 @@
@Override
protected void onFlush() {
if (reconfigurationPending) {
- // This is the initial flush after reconfiguration. Prepare to trim bytes from the start/end.
+ // Flushing activates the new configuration, so prepare to trim bytes from the start/end.
reconfigurationPending = false;
endBuffer = new byte[trimEndFrames * inputAudioFormat.bytesPerFrame];
pendingTrimStartBytes = trimStartFrames * inputAudioFormat.bytesPerFrame;
- } else {
- // This is a flush during playback (after the initial flush). We assume this was caused by a
- // seek to a non-zero position and clear pending start bytes. This assumption may be wrong (we
- // may be seeking to zero), but playing data that should have been trimmed shouldn't be
- // noticeable after a seek. Ideally we would check the timestamp of the first input buffer
- // queued after flushing to decide whether to trim (see also [Internal: b/77292509]).
- pendingTrimStartBytes = 0;
}
+
+ // TODO(internal b/77292509): Flushing occurs to activate a configuration (handled above) but
+ // also when seeking within a stream. This implementation currently doesn't handle seek to start
+ // (where we need to trim at the start again), nor seeks to non-zero positions before start
+ // trimming has occurred (where we should set pendingTrimStartBytes to zero). These cases can be
+ // fixed by trimming in queueInput based on timestamp, once that information is available.
+
+ // Any data in the end buffer should no longer be output if we are playing from a different
+ // position, so discard it and refill the buffer using new input.
endBufferSize = 0;
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
index bb772a7..eea8198 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -368,7 +368,7 @@
private final long[] pendingOutputStreamSwitchTimesUs;
@Nullable private Format inputFormat;
- private Format outputFormat;
+ @Nullable private Format outputFormat;
@Nullable private DrmSession codecDrmSession;
@Nullable private DrmSession sourceDrmSession;
@Nullable private MediaCrypto mediaCrypto;
@@ -420,6 +420,7 @@
protected DecoderCounters decoderCounters;
private long outputStreamOffsetUs;
private int pendingOutputStreamOffsetCount;
+ private boolean receivedOutputMediaFormatChange;
/**
* @param trackType The track type that the renderer handles. One of the {@code C.TRACK_TYPE_*}
@@ -635,13 +636,18 @@
* method if they are taking over responsibility for output format propagation (e.g., when using
* video tunneling).
*/
- @Nullable
- protected final Format updateOutputFormatForTime(long presentationTimeUs) {
- Format format = formatQueue.pollFloor(presentationTimeUs);
+ protected final void updateOutputFormatForTime(long presentationTimeUs) {
+ @Nullable Format format = formatQueue.pollFloor(presentationTimeUs);
if (format != null) {
outputFormat = format;
+ onOutputFormatChanged(outputFormat);
+ } else if (receivedOutputMediaFormatChange && outputFormat != null) {
+ // No Format change with the MediaFormat change, so we need to update based on the existing
+ // Format.
+ configureOutput(outputFormat);
}
- return format;
+
+ receivedOutputMediaFormatChange = false;
}
@Nullable
@@ -1446,6 +1452,28 @@
}
/**
+ * Called when the output {@link Format} changes.
+ *
+ * <p>The default implementation is a no-op.
+ *
+ * @param outputFormat The new output {@link Format}.
+ */
+ protected void onOutputFormatChanged(Format outputFormat) {
+ // Do nothing.
+ }
+
+ /**
+ * Configures the renderer output based on a {@link Format}.
+ *
+ * <p>The default implementation is a no-op.
+ *
+ * @param outputFormat The format to configure the output with.
+ */
+ protected void configureOutput(Format outputFormat) {
+ // Do nothing.
+ }
+
+ /**
* Handles supplemental data associated with an input buffer.
*
* <p>The default implementation is a no-op.
@@ -1650,6 +1678,7 @@
if (outputIndex < 0) {
if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED /* (-2) */) {
processOutputMediaFormat();
+ receivedOutputMediaFormatChange = true;
return true;
} else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED /* (-3) */) {
processOutputBuffersChanged();
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java
index d8126d4..0b7434c 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactory.java
@@ -17,8 +17,10 @@
import android.net.Uri;
import androidx.annotation.Nullable;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import java.lang.reflect.Constructor;
import java.util.List;
+import java.util.concurrent.Executor;
/**
* Default {@link DownloaderFactory}, supporting creation of progressive, DASH, HLS and
@@ -32,7 +34,7 @@
@Nullable private static final Constructor<? extends Downloader> SS_DOWNLOADER_CONSTRUCTOR;
static {
- Constructor<? extends Downloader> dashDownloaderConstructor = null;
+ @Nullable Constructor<? extends Downloader> dashDownloaderConstructor = null;
try {
// LINT.IfChange
dashDownloaderConstructor =
@@ -43,7 +45,7 @@
// Expected if the app was built without the DASH module.
}
DASH_DOWNLOADER_CONSTRUCTOR = dashDownloaderConstructor;
- Constructor<? extends Downloader> hlsDownloaderConstructor = null;
+ @Nullable Constructor<? extends Downloader> hlsDownloaderConstructor = null;
try {
// LINT.IfChange
hlsDownloaderConstructor =
@@ -54,7 +56,7 @@
// Expected if the app was built without the HLS module.
}
HLS_DOWNLOADER_CONSTRUCTOR = hlsDownloaderConstructor;
- Constructor<? extends Downloader> ssDownloaderConstructor = null;
+ @Nullable Constructor<? extends Downloader> ssDownloaderConstructor = null;
try {
// LINT.IfChange
ssDownloaderConstructor =
@@ -68,11 +70,32 @@
SS_DOWNLOADER_CONSTRUCTOR = ssDownloaderConstructor;
}
- private final DownloaderConstructorHelper downloaderConstructorHelper;
+ private final CacheDataSource.Factory cacheDataSourceFactory;
+ private final Executor executor;
- /** @param downloaderConstructorHelper A helper for instantiating downloaders. */
- public DefaultDownloaderFactory(DownloaderConstructorHelper downloaderConstructorHelper) {
- this.downloaderConstructorHelper = downloaderConstructorHelper;
+ /**
+ * Creates an instance.
+ *
+ * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which
+ * downloads will be written.
+ */
+ public DefaultDownloaderFactory(CacheDataSource.Factory cacheDataSourceFactory) {
+ this(cacheDataSourceFactory, Runnable::run);
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which
+ * downloads will be written.
+ * @param executor An {@link Executor} used to make requests for media being downloaded. Providing
+ * an {@link Executor} that uses multiple threads will speed up download tasks that can be
+ * split into smaller parts for parallel execution.
+ */
+ public DefaultDownloaderFactory(
+ CacheDataSource.Factory cacheDataSourceFactory, Executor executor) {
+ this.cacheDataSourceFactory = cacheDataSourceFactory;
+ this.executor = executor;
}
@Override
@@ -80,7 +103,7 @@
switch (request.type) {
case DownloadRequest.TYPE_PROGRESSIVE:
return new ProgressiveDownloader(
- request.uri, request.customCacheKey, downloaderConstructorHelper);
+ request.uri, request.customCacheKey, cacheDataSourceFactory, executor);
case DownloadRequest.TYPE_DASH:
return createDownloader(request, DASH_DOWNLOADER_CONSTRUCTOR);
case DownloadRequest.TYPE_HLS:
@@ -98,7 +121,8 @@
throw new IllegalStateException("Module missing for: " + request.type);
}
try {
- return constructor.newInstance(request.uri, request.streamKeys, downloaderConstructorHelper);
+ return constructor.newInstance(
+ request.uri, request.streamKeys, cacheDataSourceFactory, executor);
} catch (Exception e) {
throw new RuntimeException("Failed to instantiate downloader for: " + request.type, e);
}
@@ -109,7 +133,7 @@
try {
return clazz
.asSubclass(Downloader.class)
- .getConstructor(Uri.class, List.class, DownloaderConstructorHelper.class);
+ .getConstructor(Uri.class, List.class, CacheDataSource.Factory.class, Executor.class);
} catch (NoSuchMethodException e) {
// The downloader is present, but the expected constructor is missing.
throw new RuntimeException("Downloader constructor missing", e);
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
index 3724701..d050c1d 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java
@@ -40,6 +40,7 @@
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSource.Factory;
import com.google.android.exoplayer2.upstream.cache.Cache;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheEvictor;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.util.Assertions;
@@ -197,7 +198,10 @@
this(
context,
new DefaultDownloadIndex(databaseProvider),
- new DefaultDownloaderFactory(new DownloaderConstructorHelper(cache, upstreamFactory)));
+ new DefaultDownloaderFactory(
+ new CacheDataSource.Factory()
+ .setCache(cache)
+ .setUpstreamDataSourceFactory(upstreamFactory)));
}
/**
@@ -730,7 +734,7 @@
break;
case MSG_CONTENT_LENGTH_CHANGED:
task = (Task) message.obj;
- onContentLengthChanged(task);
+ onContentLengthChanged(task, Util.toLong(message.arg1, message.arg2));
return; // No need to post back to mainHandler.
case MSG_UPDATE_PROGRESS:
updateProgress();
@@ -1026,9 +1030,8 @@
// Task event processing.
- private void onContentLengthChanged(Task task) {
+ private void onContentLengthChanged(Task task, long contentLength) {
String downloadId = task.request.id;
- long contentLength = task.contentLength;
Download download =
Assertions.checkNotNull(getDownload(downloadId, /* loadFromIndex= */ false));
if (contentLength == download.contentLength || contentLength == C.LENGTH_UNSET) {
@@ -1321,7 +1324,13 @@
this.contentLength = contentLength;
@Nullable Handler internalHandler = this.internalHandler;
if (internalHandler != null) {
- internalHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget();
+ internalHandler
+ .obtainMessage(
+ MSG_CONTENT_LENGTH_CHANGED,
+ (int) (contentLength >> 32),
+ (int) contentLength,
+ this)
+ .sendToTarget();
}
}
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java
index 9d946da..ba226e6 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadProgress.java
@@ -21,8 +21,8 @@
public class DownloadProgress {
/** The number of bytes that have been downloaded. */
- public long bytesDownloaded;
+ public volatile long bytesDownloaded;
/** The percentage that has been downloaded, or {@link C#PERCENTAGE_UNSET} if unknown. */
- public float percentDownloaded;
+ public volatile float percentDownloaded;
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java
index fa10d58..53485ac 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java
@@ -28,6 +28,10 @@
/**
* Called when progress is made during a download operation.
*
+ * <p>May be called directly from {@link #download}, or from any other thread used by the
+ * downloader. In all cases, {@link #download} is guaranteed not to return until after the last
+ * call to {@link #onProgress} has finished executing.
+ *
* @param contentLength The length of the content in bytes, or {@link C#LENGTH_UNSET} if
* unknown.
* @param bytesDownloaded The number of bytes that have been downloaded.
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java
deleted file mode 100644
index 0d53b3c..0000000
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright (C) 2017 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.offline;
-
-import androidx.annotation.Nullable;
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.upstream.DataSink;
-import com.google.android.exoplayer2.upstream.DataSource;
-import com.google.android.exoplayer2.upstream.DummyDataSource;
-import com.google.android.exoplayer2.upstream.FileDataSource;
-import com.google.android.exoplayer2.upstream.PriorityDataSourceFactory;
-import com.google.android.exoplayer2.upstream.cache.Cache;
-import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
-import com.google.android.exoplayer2.upstream.cache.CacheDataSinkFactory;
-import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
-import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
-import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory;
-import com.google.android.exoplayer2.upstream.cache.CacheUtil;
-import com.google.android.exoplayer2.util.PriorityTaskManager;
-
-/** A helper class that holds necessary parameters for {@link Downloader} construction. */
-public final class DownloaderConstructorHelper {
-
- private final Cache cache;
- @Nullable private final CacheKeyFactory cacheKeyFactory;
- @Nullable private final PriorityTaskManager priorityTaskManager;
- private final CacheDataSourceFactory onlineCacheDataSourceFactory;
- private final CacheDataSourceFactory offlineCacheDataSourceFactory;
-
- /**
- * @param cache Cache instance to be used to store downloaded data.
- * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for
- * downloading data.
- */
- public DownloaderConstructorHelper(Cache cache, DataSource.Factory upstreamFactory) {
- this(
- cache,
- upstreamFactory,
- /* cacheReadDataSourceFactory= */ null,
- /* cacheWriteDataSinkFactory= */ null,
- /* priorityTaskManager= */ null);
- }
-
- /**
- * @param cache Cache instance to be used to store downloaded data.
- * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for
- * downloading data.
- * @param cacheReadDataSourceFactory A {@link DataSource.Factory} for creating {@link DataSource}s
- * for reading data from the cache. If null then a {@link FileDataSource.Factory} will be
- * used.
- * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for creating {@link DataSource}s
- * for writing data to the cache. If null then a {@link CacheDataSinkFactory} will be used.
- * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null,
- * downloaders will register as tasks with priority {@link C#PRIORITY_DOWNLOAD} whilst
- * downloading.
- */
- public DownloaderConstructorHelper(
- Cache cache,
- DataSource.Factory upstreamFactory,
- @Nullable DataSource.Factory cacheReadDataSourceFactory,
- @Nullable DataSink.Factory cacheWriteDataSinkFactory,
- @Nullable PriorityTaskManager priorityTaskManager) {
- this(
- cache,
- upstreamFactory,
- cacheReadDataSourceFactory,
- cacheWriteDataSinkFactory,
- priorityTaskManager,
- /* cacheKeyFactory= */ null);
- }
-
- /**
- * @param cache Cache instance to be used to store downloaded data.
- * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for
- * downloading data.
- * @param cacheReadDataSourceFactory A {@link DataSource.Factory} for creating {@link DataSource}s
- * for reading data from the cache. If null then a {@link FileDataSource.Factory} will be
- * used.
- * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for creating {@link DataSource}s
- * for writing data to the cache. If null then a {@link CacheDataSinkFactory} will be used.
- * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null,
- * downloaders will register as tasks with priority {@link C#PRIORITY_DOWNLOAD} whilst
- * downloading.
- * @param cacheKeyFactory An optional factory for cache keys.
- */
- public DownloaderConstructorHelper(
- Cache cache,
- DataSource.Factory upstreamFactory,
- @Nullable DataSource.Factory cacheReadDataSourceFactory,
- @Nullable DataSink.Factory cacheWriteDataSinkFactory,
- @Nullable PriorityTaskManager priorityTaskManager,
- @Nullable CacheKeyFactory cacheKeyFactory) {
- if (priorityTaskManager != null) {
- upstreamFactory =
- new PriorityDataSourceFactory(upstreamFactory, priorityTaskManager, C.PRIORITY_DOWNLOAD);
- }
- DataSource.Factory readDataSourceFactory =
- cacheReadDataSourceFactory != null
- ? cacheReadDataSourceFactory
- : new FileDataSource.Factory();
- if (cacheWriteDataSinkFactory == null) {
- cacheWriteDataSinkFactory =
- new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE);
- }
- onlineCacheDataSourceFactory =
- new CacheDataSourceFactory(
- cache,
- upstreamFactory,
- readDataSourceFactory,
- cacheWriteDataSinkFactory,
- CacheDataSource.FLAG_BLOCK_ON_CACHE,
- /* eventListener= */ null,
- cacheKeyFactory);
- offlineCacheDataSourceFactory =
- new CacheDataSourceFactory(
- cache,
- DummyDataSource.FACTORY,
- readDataSourceFactory,
- null,
- CacheDataSource.FLAG_BLOCK_ON_CACHE,
- /* eventListener= */ null,
- cacheKeyFactory);
- this.cache = cache;
- this.priorityTaskManager = priorityTaskManager;
- this.cacheKeyFactory = cacheKeyFactory;
- }
-
- /** Returns the {@link Cache} instance. */
- public Cache getCache() {
- return cache;
- }
-
- /** Returns the {@link CacheKeyFactory}. */
- public CacheKeyFactory getCacheKeyFactory() {
- return cacheKeyFactory != null ? cacheKeyFactory : CacheUtil.DEFAULT_CACHE_KEY_FACTORY;
- }
-
- /** Returns a {@link PriorityTaskManager} instance. */
- public PriorityTaskManager getPriorityTaskManager() {
- // Return a dummy PriorityTaskManager if none is provided. Create a new PriorityTaskManager
- // each time so clients don't affect each other over the dummy PriorityTaskManager instance.
- return priorityTaskManager != null ? priorityTaskManager : new PriorityTaskManager();
- }
-
- /** Returns a new {@link CacheDataSource} instance. */
- public CacheDataSource createCacheDataSource() {
- return onlineCacheDataSourceFactory.createDataSource();
- }
-
- /**
- * Returns a new {@link CacheDataSource} instance which accesses cache read-only and throws an
- * exception on cache miss.
- */
- public CacheDataSource createOfflineCacheDataSource() {
- return offlineCacheDataSourceFactory.createDataSource();
- }
-}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java
index 055410c..ecdd174 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java
@@ -19,73 +19,78 @@
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
-import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
-import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory;
import com.google.android.exoplayer2.upstream.cache.CacheUtil;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import java.io.IOException;
+import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
-/**
- * A downloader for progressive media streams.
- *
- * <p>The downloader attempts to download the entire media bytes referenced by a {@link Uri} into a
- * cache as defined by {@link DownloaderConstructorHelper}. Callers can use the constructor to
- * specify a custom cache key for the downloaded bytes.
- *
- * <p>The downloader will avoid downloading already-downloaded media bytes.
- */
+/** A downloader for progressive media streams. */
public final class ProgressiveDownloader implements Downloader {
private static final int BUFFER_SIZE_BYTES = 128 * 1024;
private final DataSpec dataSpec;
- private final Cache cache;
private final CacheDataSource dataSource;
- private final CacheKeyFactory cacheKeyFactory;
- private final PriorityTaskManager priorityTaskManager;
private final AtomicBoolean isCanceled;
/**
* @param uri Uri of the data to be downloaded.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null.
- * @param constructorHelper A {@link DownloaderConstructorHelper} instance.
+ * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
+ * download will be written.
*/
public ProgressiveDownloader(
- Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) {
- this.dataSpec =
+ Uri uri, @Nullable String customCacheKey, CacheDataSource.Factory cacheDataSourceFactory) {
+ this(uri, customCacheKey, cacheDataSourceFactory, Runnable::run);
+ }
+
+ /**
+ * @param uri Uri of the data to be downloaded.
+ * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
+ * indexing. May be null.
+ * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
+ * download will be written.
+ * @param executor An {@link Executor} used to make requests for the media being downloaded. In
+ * the future, providing an {@link Executor} that uses multiple threads may speed up the
+ * download by allowing parts of it to be executed in parallel.
+ */
+ public ProgressiveDownloader(
+ Uri uri,
+ @Nullable String customCacheKey,
+ CacheDataSource.Factory cacheDataSourceFactory,
+ Executor executor) {
+ dataSpec =
new DataSpec.Builder()
.setUri(uri)
.setKey(customCacheKey)
.setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION)
.build();
- this.cache = constructorHelper.getCache();
- this.dataSource = constructorHelper.createCacheDataSource();
- this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();
- this.priorityTaskManager = constructorHelper.getPriorityTaskManager();
+ dataSource = cacheDataSourceFactory.createDataSourceForDownloading();
isCanceled = new AtomicBoolean();
}
@Override
public void download(@Nullable ProgressListener progressListener)
throws InterruptedException, IOException {
- priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
+ @Nullable PriorityTaskManager priorityTaskManager = dataSource.getUpstreamPriorityTaskManager();
+ if (priorityTaskManager != null) {
+ priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
+ }
try {
CacheUtil.cache(
- dataSpec,
- cache,
- cacheKeyFactory,
dataSource,
- new byte[BUFFER_SIZE_BYTES],
- priorityTaskManager,
- C.PRIORITY_DOWNLOAD,
+ dataSpec,
progressListener == null ? null : new ProgressForwarder(progressListener),
isCanceled,
- /* enableEOFException= */ true);
+ /* enableEOFException= */ true,
+ /* temporaryBuffer= */ new byte[BUFFER_SIZE_BYTES]);
} finally {
- priorityTaskManager.remove(C.PRIORITY_DOWNLOAD);
+ if (priorityTaskManager != null) {
+ priorityTaskManager.remove(C.PRIORITY_DOWNLOAD);
+ }
}
}
@@ -96,7 +101,7 @@
@Override
public void remove() {
- CacheUtil.remove(dataSpec, cache, cacheKeyFactory);
+ CacheUtil.remove(dataSpec, dataSource.getCache(), dataSource.getCacheKeyFactory());
}
private static final class ProgressForwarder implements CacheUtil.ProgressListener {
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java
index 299998e..f7b2fc7 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java
@@ -33,6 +33,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -67,29 +68,30 @@
private static final long MAX_MERGED_SEGMENT_START_TIME_DIFF_US = 20 * C.MICROS_PER_SECOND;
private final DataSpec manifestDataSpec;
- private final Cache cache;
- private final CacheDataSource dataSource;
- private final CacheDataSource offlineDataSource;
- private final CacheKeyFactory cacheKeyFactory;
- private final PriorityTaskManager priorityTaskManager;
private final ArrayList<StreamKey> streamKeys;
+ private final CacheDataSource.Factory cacheDataSourceFactory;
+ private final Executor executor;
private final AtomicBoolean isCanceled;
/**
* @param manifestUri The {@link Uri} of the manifest to be downloaded.
* @param streamKeys Keys defining which streams in the manifest should be selected for download.
* If empty, all streams are downloaded.
- * @param constructorHelper A {@link DownloaderConstructorHelper} instance.
+ * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
+ * download will be written.
+ * @param executor An {@link Executor} used to make requests for the media being downloaded.
+ * Providing an {@link Executor} that uses multiple threads will speed up the download by
+ * allowing parts of it to be executed in parallel.
*/
public SegmentDownloader(
- Uri manifestUri, List<StreamKey> streamKeys, DownloaderConstructorHelper constructorHelper) {
+ Uri manifestUri,
+ List<StreamKey> streamKeys,
+ CacheDataSource.Factory cacheDataSourceFactory,
+ Executor executor) {
this.manifestDataSpec = getCompressibleDataSpec(manifestUri);
this.streamKeys = new ArrayList<>(streamKeys);
- this.cache = constructorHelper.getCache();
- this.dataSource = constructorHelper.createCacheDataSource();
- this.offlineDataSource = constructorHelper.createOfflineCacheDataSource();
- this.cacheKeyFactory = constructorHelper.getCacheKeyFactory();
- this.priorityTaskManager = constructorHelper.getPriorityTaskManager();
+ this.cacheDataSourceFactory = cacheDataSourceFactory;
+ this.executor = executor;
isCanceled = new AtomicBoolean();
}
@@ -103,7 +105,11 @@
@Override
public final void download(@Nullable ProgressListener progressListener)
throws IOException, InterruptedException {
- priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
+ CacheDataSource dataSource = cacheDataSourceFactory.createDataSourceForDownloading();
+ @Nullable PriorityTaskManager priorityTaskManager = dataSource.getUpstreamPriorityTaskManager();
+ if (priorityTaskManager != null) {
+ priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
+ }
try {
// Get the manifest and all of the segments.
M manifest = getManifest(dataSource, manifestDataSpec);
@@ -112,7 +118,7 @@
}
List<Segment> segments = getSegments(dataSource, manifest, /* allowIncompleteList= */ false);
Collections.sort(segments);
- mergeSegments(segments, cacheKeyFactory);
+ mergeSegments(segments, dataSource.getCacheKeyFactory());
// Scan the segments, removing any that are fully downloaded.
int totalSegments = segments.size();
@@ -122,7 +128,8 @@
for (int i = segments.size() - 1; i >= 0; i--) {
Segment segment = segments.get(i);
Pair<Long, Long> segmentLengthAndBytesDownloaded =
- CacheUtil.getCached(segment.dataSpec, cache, cacheKeyFactory);
+ CacheUtil.getCached(
+ segment.dataSpec, dataSource.getCache(), dataSource.getCacheKeyFactory());
long segmentLength = segmentLengthAndBytesDownloaded.first;
long segmentBytesDownloaded = segmentLengthAndBytesDownloaded.second;
bytesDownloaded += segmentBytesDownloaded;
@@ -141,35 +148,33 @@
}
// Download the segments.
- @Nullable ProgressNotifier progressNotifier = null;
- if (progressListener != null) {
- progressNotifier =
- new ProgressNotifier(
- progressListener,
- contentLength,
- totalSegments,
- bytesDownloaded,
- segmentsDownloaded);
- }
- byte[] buffer = new byte[BUFFER_SIZE_BYTES];
+ @Nullable
+ ProgressNotifier progressNotifier =
+ progressListener != null
+ ? new ProgressNotifier(
+ progressListener,
+ contentLength,
+ totalSegments,
+ bytesDownloaded,
+ segmentsDownloaded)
+ : null;
+ byte[] temporaryBuffer = new byte[BUFFER_SIZE_BYTES];
for (int i = 0; i < segments.size(); i++) {
CacheUtil.cache(
- segments.get(i).dataSpec,
- cache,
- cacheKeyFactory,
dataSource,
- buffer,
- priorityTaskManager,
- C.PRIORITY_DOWNLOAD,
+ segments.get(i).dataSpec,
progressNotifier,
isCanceled,
- true);
+ /* enableEOFException= */ true,
+ temporaryBuffer);
if (progressNotifier != null) {
progressNotifier.onSegmentDownloaded();
}
}
} finally {
- priorityTaskManager.remove(C.PRIORITY_DOWNLOAD);
+ if (priorityTaskManager != null) {
+ priorityTaskManager.remove(C.PRIORITY_DOWNLOAD);
+ }
}
}
@@ -180,17 +185,20 @@
@Override
public final void remove() throws InterruptedException {
+ CacheDataSource dataSource = cacheDataSourceFactory.createDataSourceForRemovingDownload();
+ Cache cache = dataSource.getCache();
+ CacheKeyFactory cacheKeyFactory = dataSource.getCacheKeyFactory();
try {
- M manifest = getManifest(offlineDataSource, manifestDataSpec);
- List<Segment> segments = getSegments(offlineDataSource, manifest, true);
+ M manifest = getManifest(dataSource, manifestDataSpec);
+ List<Segment> segments = getSegments(dataSource, manifest, true);
for (int i = 0; i < segments.size(); i++) {
- removeDataSpec(segments.get(i).dataSpec);
+ CacheUtil.remove(segments.get(i).dataSpec, cache, cacheKeyFactory);
}
} catch (IOException e) {
// Ignore exceptions when removing.
} finally {
// Always attempt to remove the manifest.
- removeDataSpec(manifestDataSpec);
+ CacheUtil.remove(manifestDataSpec, cache, cacheKeyFactory);
}
}
@@ -223,10 +231,6 @@
DataSource dataSource, M manifest, boolean allowIncompleteList)
throws InterruptedException, IOException;
- private void removeDataSpec(DataSpec dataSpec) {
- CacheUtil.remove(dataSpec, cache, cacheKeyFactory);
- }
-
protected static DataSpec getCompressibleDataSpec(Uri uri) {
return new DataSpec.Builder().setUri(uri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build();
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java
index a10bd03..461b146 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java
@@ -22,6 +22,7 @@
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MediaSourceEventDispatcher;
import java.util.ArrayList;
import java.util.HashSet;
@@ -133,22 +134,44 @@
@Override
public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) {
- eventDispatcher.addEventListener(handler, eventListener);
+ addEventListenerInternal(handler, eventListener, MediaSourceEventListener.class);
}
@Override
public final void removeEventListener(MediaSourceEventListener eventListener) {
- eventDispatcher.removeEventListener(eventListener);
+ removeEventListenerInternal(eventListener, MediaSourceEventListener.class);
}
@Override
public final void addDrmEventListener(Handler handler, DrmSessionEventListener eventListener) {
- eventDispatcher.addEventListener(handler, eventListener, DrmSessionEventListener.class);
+ addEventListenerInternal(handler, eventListener, DrmSessionEventListener.class);
}
@Override
public final void removeDrmEventListener(DrmSessionEventListener eventListener) {
- eventDispatcher.removeEventListener(eventListener, DrmSessionEventListener.class);
+ removeEventListenerInternal(eventListener, DrmSessionEventListener.class);
+ }
+
+ /**
+ * Adds a listener to the internal {@link MediaSourceEventDispatcher} with the provided type.
+ *
+ * <p>NOTE: Read the caveats on {@link MediaSourceEventDispatcher#addEventListener(Handler,
+ * Object, Class)} when deciding what value to pass for {@code listenerClass}.
+ *
+ * @see MediaSourceEventDispatcher#addEventListener(Handler, Object, Class)
+ */
+ protected final <T> void addEventListenerInternal(
+ Handler handler, T eventListener, Class<T> listenerClass) {
+ eventDispatcher.addEventListener(handler, eventListener, listenerClass);
+ }
+
+ /**
+ * Removes a listener from the internal {@link MediaSourceEventDispatcher}.
+ *
+ * @see MediaSourceEventDispatcher#removeEventListener(Object, Class)
+ */
+ protected final <T> void removeEventListenerInternal(T eventListener, Class<T> listenerClass) {
+ eventDispatcher.removeEventListener(eventListener, listenerClass);
}
@Override
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java
index c3beb0d..e6cff59 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java
@@ -29,6 +29,8 @@
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.MediaDrmCallback;
import com.google.android.exoplayer2.offline.StreamKey;
+import com.google.android.exoplayer2.source.ads.AdsLoader;
+import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
@@ -36,6 +38,7 @@
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
@@ -89,10 +92,37 @@
* alternative dummy, apps can pass a drm session manager to {@link
* #setDrmSessionManager(DrmSessionManager)} which will be used for all items without a drm
* configuration.
+ *
+ * <h3>Ad support for media items with ad tag uri</h3>
+ *
+ * <p>For a media item with an ad tag uri an {@link AdSupportProvider} needs to be passed to the
+ * constructor {@link #DefaultMediaSourceFactory(Context, DataSource.Factory, AdSupportProvider)}.
*/
public final class DefaultMediaSourceFactory implements MediaSourceFactory {
/**
+ * Provides {@link AdsLoader ads loaders} and an {@link AdsLoader.AdViewProvider} to created
+ * {@link AdsMediaSource AdsMediaSources}.
+ */
+ public interface AdSupportProvider {
+
+ /**
+ * Returns an {@link AdsLoader} for the given {@link Uri ad tag uri} or null if no ads loader is
+ * available for the given ad tag uri.
+ *
+ * <p>This method is called for each media item for which a media source is created.
+ */
+ @Nullable
+ AdsLoader getAdsLoader(Uri adTagUri);
+
+ /**
+ * Returns an {@link AdsLoader.AdViewProvider} which is used to create {@link AdsMediaSource
+ * AdsMediaSources}.
+ */
+ AdsLoader.AdViewProvider getAdViewProvider();
+ }
+
+ /**
* Creates a new instance with the given {@link Context}.
*
* <p>This is functionally equivalent with calling {@code #newInstance(Context,
@@ -115,10 +145,13 @@
*/
public static DefaultMediaSourceFactory newInstance(
Context context, DataSource.Factory dataSourceFactory) {
- return new DefaultMediaSourceFactory(context, dataSourceFactory);
+ return new DefaultMediaSourceFactory(context, dataSourceFactory, /* adSupportProvider= */ null);
}
+ private static final String TAG = "DefaultMediaSourceFactory";
+
private final DataSource.Factory dataSourceFactory;
+ @Nullable private final AdSupportProvider adSupportProvider;
private final SparseArray<MediaSourceFactory> mediaSourceFactories;
@C.ContentType private final int[] supportedTypes;
private final String userAgent;
@@ -127,8 +160,20 @@
private HttpDataSource.Factory drmHttpDataSourceFactory;
@Nullable private List<StreamKey> streamKeys;
- private DefaultMediaSourceFactory(Context context, DataSource.Factory dataSourceFactory) {
+ /**
+ * Creates a new instance with the given {@link Context} and {@link DataSource.Factory}.
+ *
+ * @param context The {@link Context}.
+ * @param dataSourceFactory A {@link DataSource.Factory} to be used to create media sources.
+ * @param adSupportProvider An {@link AdSupportProvider} to get ads loaders and ad view providers
+ * to be used to create {@link AdsMediaSource AdsMediaSources}.
+ */
+ public DefaultMediaSourceFactory(
+ Context context,
+ DataSource.Factory dataSourceFactory,
+ @Nullable AdSupportProvider adSupportProvider) {
this.dataSourceFactory = dataSourceFactory;
+ this.adSupportProvider = adSupportProvider;
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
userAgent = Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY);
drmHttpDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
@@ -203,7 +248,7 @@
Assertions.checkNotNull(mediaItem.playbackProperties);
@C.ContentType
int type =
- inferContentType(
+ Util.inferContentTypeWithMimeType(
mediaItem.playbackProperties.sourceUri, mediaItem.playbackProperties.mimeType);
@Nullable MediaSourceFactory mediaSourceFactory = mediaSourceFactories.get(type);
Assertions.checkNotNull(
@@ -214,30 +259,29 @@
? mediaItem.playbackProperties.streamKeys
: streamKeys);
- MediaSource leafMediaSource = mediaSourceFactory.createMediaSource(mediaItem);
+ MediaSource mediaSource = mediaSourceFactory.createMediaSource(mediaItem);
List<MediaItem.Subtitle> subtitles = mediaItem.playbackProperties.subtitles;
- if (subtitles.isEmpty()) {
- return maybeClipMediaSource(mediaItem, leafMediaSource);
+ if (!subtitles.isEmpty()) {
+ MediaSource[] mediaSources = new MediaSource[subtitles.size() + 1];
+ mediaSources[0] = mediaSource;
+ SingleSampleMediaSource.Factory singleSampleSourceFactory =
+ new SingleSampleMediaSource.Factory(dataSourceFactory);
+ for (int i = 0; i < subtitles.size(); i++) {
+ MediaItem.Subtitle subtitle = subtitles.get(i);
+ Format subtitleFormat =
+ new Format.Builder()
+ .setSampleMimeType(subtitle.mimeType)
+ .setLanguage(subtitle.language)
+ .setSelectionFlags(subtitle.selectionFlags)
+ .build();
+ mediaSources[i + 1] =
+ singleSampleSourceFactory.createMediaSource(
+ subtitle.uri, subtitleFormat, /* durationUs= */ C.TIME_UNSET);
+ }
+ mediaSource = new MergingMediaSource(mediaSources);
}
-
- MediaSource[] mediaSources = new MediaSource[subtitles.size() + 1];
- mediaSources[0] = leafMediaSource;
- SingleSampleMediaSource.Factory singleSampleSourceFactory =
- new SingleSampleMediaSource.Factory(dataSourceFactory);
- for (int i = 0; i < subtitles.size(); i++) {
- MediaItem.Subtitle subtitle = subtitles.get(i);
- Format subtitleFormat =
- new Format.Builder()
- .setSampleMimeType(subtitle.mimeType)
- .setLanguage(subtitle.language)
- .setSelectionFlags(subtitle.selectionFlags)
- .build();
- mediaSources[i + 1] =
- singleSampleSourceFactory.createMediaSource(
- subtitle.uri, subtitleFormat, /* durationUs= */ C.TIME_UNSET);
- }
- return maybeClipMediaSource(mediaItem, new MergingMediaSource(mediaSources));
+ return maybeWrapWithAdsMediaSource(mediaItem, maybeClipMediaSource(mediaItem, mediaSource));
}
// internal methods
@@ -285,6 +329,34 @@
mediaItem.clippingProperties.relativeToDefaultPosition);
}
+ private MediaSource maybeWrapWithAdsMediaSource(MediaItem mediaItem, MediaSource mediaSource) {
+ Assertions.checkNotNull(mediaItem.playbackProperties);
+ if (mediaItem.playbackProperties.adTagUri == null) {
+ return mediaSource;
+ }
+ if (adSupportProvider == null) {
+ Log.w(
+ TAG,
+ "Playing media without ads. Pass an AdsSupportProvider to the constructor for supporting"
+ + " media items with an ad tag uri.");
+ return mediaSource;
+ }
+ AdsLoader adsLoader = adSupportProvider.getAdsLoader(mediaItem.playbackProperties.adTagUri);
+ if (adsLoader == null) {
+ Log.w(
+ TAG,
+ String.format(
+ "Playing media without ads. No AdsLoader for media item with mediaId '%s'.",
+ mediaItem.mediaId));
+ return mediaSource;
+ }
+ return new AdsMediaSource(
+ mediaSource,
+ /* adMediaSourceFactory= */ this,
+ adsLoader,
+ adSupportProvider.getAdViewProvider());
+ }
+
private static SparseArray<MediaSourceFactory> loadDelegates(
DataSource.Factory dataSourceFactory) {
SparseArray<MediaSourceFactory> factories = new SparseArray<>();
@@ -324,20 +396,4 @@
factories.put(C.TYPE_OTHER, new ProgressiveMediaSource.Factory(dataSourceFactory));
return factories;
}
-
- private static int inferContentType(Uri sourceUri, @Nullable String mimeType) {
- if (mimeType == null) {
- return Util.inferContentType(sourceUri);
- }
- switch (mimeType) {
- case MimeTypes.APPLICATION_MPD:
- return C.TYPE_DASH;
- case MimeTypes.APPLICATION_M3U8:
- return C.TYPE_HLS;
- case MimeTypes.APPLICATION_SS:
- return C.TYPE_SS;
- default:
- return Util.inferContentType(sourceUri);
- }
- }
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/LoadEventInfo.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/LoadEventInfo.java
index cfef4ee..6ad0585 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/LoadEventInfo.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/LoadEventInfo.java
@@ -18,6 +18,7 @@
import android.net.Uri;
import android.os.SystemClock;
import com.google.android.exoplayer2.upstream.DataSpec;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
@@ -51,6 +52,20 @@
public final long bytesLoaded;
/**
+ * Equivalent to {@link #LoadEventInfo(DataSpec, Uri, Map, long, long, long)
+ * LoadEventInfo(dataSpec, dataSpec.uri, Collections.emptyMap(), elapsedRealtimeMs, 0, 0)}.
+ */
+ public LoadEventInfo(DataSpec dataSpec, long elapsedRealtimeMs) {
+ this(
+ dataSpec,
+ dataSpec.uri,
+ Collections.emptyMap(),
+ elapsedRealtimeMs,
+ /* loadDurationMs= */ 0,
+ /* bytesLoaded= */ 0);
+ }
+
+ /**
* Creates load event info.
*
* @param dataSpec Defines the requested data.
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java
index 61bb55d..7c9dc34 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java
@@ -15,21 +15,15 @@
*/
package com.google.android.exoplayer2.source;
-import android.net.Uri;
-import android.os.Handler;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
-import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.CopyOnWriteMultiset;
import com.google.android.exoplayer2.util.MediaSourceEventDispatcher;
import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
/** Interface for callbacks to be notified of {@link MediaSource} events. */
public interface MediaSourceEventListener {
@@ -188,33 +182,6 @@
return new EventDispatcher(listenerInfos, windowIndex, mediaPeriodId, mediaTimeOffsetMs);
}
- /**
- * Adds a {@link MediaSourceEventListener} to the event dispatcher.
- *
- * <p>This is equivalent to {@link #addEventListener(Handler, Object, Class)} with {@code
- * listenerClass = MediaSourceEventListener.class} and is intended to ease the transition to
- * using {@link MediaSourceEventDispatcher} everywhere.
- *
- * @param handler A handler on the which listener events will be posted.
- * @param eventListener The listener to be added.
- */
- public void addEventListener(Handler handler, MediaSourceEventListener eventListener) {
- addEventListener(handler, eventListener, MediaSourceEventListener.class);
- }
-
- /**
- * Removes a {@link MediaSourceEventListener} from the event dispatcher.
- *
- * <p>This is equivalent to {@link #removeEventListener(Object, Class)} with {@code
- * listenerClass = MediaSourceEventListener.class} and is intended to ease the transition to
- * using {@link MediaSourceEventDispatcher} everywhere.
- *
- * @param eventListener The listener to be removed.
- */
- public void removeEventListener(MediaSourceEventListener eventListener) {
- removeEventListener(eventListener, MediaSourceEventListener.class);
- }
-
public void mediaPeriodCreated() {
dispatch(
(listener, windowIndex, mediaPeriodId) ->
@@ -229,37 +196,29 @@
MediaSourceEventListener.class);
}
- public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) {
+ public void loadStarted(LoadEventInfo loadEventInfo, int dataType) {
loadStarted(
- dataSpec,
+ loadEventInfo,
dataType,
- C.TRACK_TYPE_UNKNOWN,
- null,
- C.SELECTION_REASON_UNKNOWN,
- null,
- C.TIME_UNSET,
- C.TIME_UNSET,
- elapsedRealtimeMs);
+ /* trackType= */ C.TRACK_TYPE_UNKNOWN,
+ /* trackFormat= */ null,
+ /* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ C.TIME_UNSET,
+ /* mediaEndTimeUs= */ C.TIME_UNSET);
}
public void loadStarted(
- DataSpec dataSpec,
+ LoadEventInfo loadEventInfo,
int dataType,
int trackType,
@Nullable Format trackFormat,
int trackSelectionReason,
@Nullable Object trackSelectionData,
long mediaStartTimeUs,
- long mediaEndTimeUs,
- long elapsedRealtimeMs) {
+ long mediaEndTimeUs) {
loadStarted(
- new LoadEventInfo(
- dataSpec,
- dataSpec.uri,
- /* responseHeaders= */ Collections.emptyMap(),
- elapsedRealtimeMs,
- /* loadDurationMs= */ 0,
- /* bytesLoaded= */ 0),
+ loadEventInfo,
new MediaLoadData(
dataType,
trackType,
@@ -277,47 +236,29 @@
MediaSourceEventListener.class);
}
- public void loadCompleted(
- DataSpec dataSpec,
- Uri uri,
- Map<String, List<String>> responseHeaders,
- int dataType,
- long elapsedRealtimeMs,
- long loadDurationMs,
- long bytesLoaded) {
+ public void loadCompleted(LoadEventInfo loadEventInfo, int dataType) {
loadCompleted(
- dataSpec,
- uri,
- responseHeaders,
+ loadEventInfo,
dataType,
- C.TRACK_TYPE_UNKNOWN,
- null,
- C.SELECTION_REASON_UNKNOWN,
- null,
- C.TIME_UNSET,
- C.TIME_UNSET,
- elapsedRealtimeMs,
- loadDurationMs,
- bytesLoaded);
+ /* trackType= */ C.TRACK_TYPE_UNKNOWN,
+ /* trackFormat= */ null,
+ /* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ C.TIME_UNSET,
+ /* mediaEndTimeUs= */ C.TIME_UNSET);
}
public void loadCompleted(
- DataSpec dataSpec,
- Uri uri,
- Map<String, List<String>> responseHeaders,
+ LoadEventInfo loadEventInfo,
int dataType,
int trackType,
@Nullable Format trackFormat,
int trackSelectionReason,
@Nullable Object trackSelectionData,
long mediaStartTimeUs,
- long mediaEndTimeUs,
- long elapsedRealtimeMs,
- long loadDurationMs,
- long bytesLoaded) {
+ long mediaEndTimeUs) {
loadCompleted(
- new LoadEventInfo(
- dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
+ loadEventInfo,
new MediaLoadData(
dataType,
trackType,
@@ -335,47 +276,29 @@
MediaSourceEventListener.class);
}
- public void loadCanceled(
- DataSpec dataSpec,
- Uri uri,
- Map<String, List<String>> responseHeaders,
- int dataType,
- long elapsedRealtimeMs,
- long loadDurationMs,
- long bytesLoaded) {
+ public void loadCanceled(LoadEventInfo loadEventInfo, int dataType) {
loadCanceled(
- dataSpec,
- uri,
- responseHeaders,
+ loadEventInfo,
dataType,
- C.TRACK_TYPE_UNKNOWN,
- null,
- C.SELECTION_REASON_UNKNOWN,
- null,
- C.TIME_UNSET,
- C.TIME_UNSET,
- elapsedRealtimeMs,
- loadDurationMs,
- bytesLoaded);
+ /* trackType= */ C.TRACK_TYPE_UNKNOWN,
+ /* trackFormat= */ null,
+ /* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ C.TIME_UNSET,
+ /* mediaEndTimeUs= */ C.TIME_UNSET);
}
public void loadCanceled(
- DataSpec dataSpec,
- Uri uri,
- Map<String, List<String>> responseHeaders,
+ LoadEventInfo loadEventInfo,
int dataType,
int trackType,
@Nullable Format trackFormat,
int trackSelectionReason,
@Nullable Object trackSelectionData,
long mediaStartTimeUs,
- long mediaEndTimeUs,
- long elapsedRealtimeMs,
- long loadDurationMs,
- long bytesLoaded) {
+ long mediaEndTimeUs) {
loadCanceled(
- new LoadEventInfo(
- dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
+ loadEventInfo,
new MediaLoadData(
dataType,
trackType,
@@ -394,37 +317,22 @@
}
public void loadError(
- DataSpec dataSpec,
- Uri uri,
- Map<String, List<String>> responseHeaders,
- int dataType,
- long elapsedRealtimeMs,
- long loadDurationMs,
- long bytesLoaded,
- IOException error,
- boolean wasCanceled) {
+ LoadEventInfo loadEventInfo, int dataType, IOException error, boolean wasCanceled) {
loadError(
- dataSpec,
- uri,
- responseHeaders,
+ loadEventInfo,
dataType,
- C.TRACK_TYPE_UNKNOWN,
- null,
- C.SELECTION_REASON_UNKNOWN,
- null,
- C.TIME_UNSET,
- C.TIME_UNSET,
- elapsedRealtimeMs,
- loadDurationMs,
- bytesLoaded,
+ /* trackType= */ C.TRACK_TYPE_UNKNOWN,
+ /* trackFormat= */ null,
+ /* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ C.TIME_UNSET,
+ /* mediaEndTimeUs= */ C.TIME_UNSET,
error,
wasCanceled);
}
public void loadError(
- DataSpec dataSpec,
- Uri uri,
- Map<String, List<String>> responseHeaders,
+ LoadEventInfo loadEventInfo,
int dataType,
int trackType,
@Nullable Format trackFormat,
@@ -432,14 +340,10 @@
@Nullable Object trackSelectionData,
long mediaStartTimeUs,
long mediaEndTimeUs,
- long elapsedRealtimeMs,
- long loadDurationMs,
- long bytesLoaded,
IOException error,
boolean wasCanceled) {
loadError(
- new LoadEventInfo(
- dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded),
+ loadEventInfo,
new MediaLoadData(
dataType,
trackType,
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 2bba84a..0c5af44 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,8 +96,6 @@
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,
@@ -109,8 +107,8 @@
int[] streamChildIndices = new int[selections.length];
int[] selectionChildIndices = new int[selections.length];
for (int i = 0; i < selections.length; i++) {
- streamChildIndices[i] = streams[i] == null ? C.INDEX_UNSET
- : streamPeriodIndices.get(streams[i]);
+ Integer streamChildIndex = streams[i] == null ? null : streamPeriodIndices.get(streams[i]);
+ streamChildIndices[i] = streamChildIndex == null ? C.INDEX_UNSET : streamChildIndex;
selectionChildIndices[i] = C.INDEX_UNSET;
if (selections[i] != null) {
TrackGroup trackGroup = selections[i].getTrackGroup();
@@ -160,8 +158,7 @@
// Copy the new streams back into the streams array.
System.arraycopy(newStreams, 0, streams, 0, newStreams.length);
// Update the local state.
- enabledPeriods = new MediaPeriod[enabledPeriodsList.size()];
- enabledPeriodsList.toArray(enabledPeriods);
+ enabledPeriods = enabledPeriodsList.toArray(new MediaPeriod[0]);
compositeSequenceableLoader =
compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(enabledPeriods);
return positionUs;
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
index 2cbfeb8..87430d8 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java
@@ -552,20 +552,22 @@
: largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;
listener.onSourceInfoRefreshed(durationUs, isSeekable, isLive);
}
+ StatsDataSource dataSource = loadable.dataSource;
eventDispatcher.loadCompleted(
- loadable.dataSpec,
- loadable.dataSource.getLastOpenedUri(),
- loadable.dataSource.getLastResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ dataSource.getLastOpenedUri(),
+ dataSource.getLastResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ dataSource.getBytesRead()),
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ loadable.seekTimeUs,
- durationUs,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.dataSource.getBytesRead());
+ durationUs);
copyLengthFromLoader(loadable);
loadingFinished = true;
Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
@@ -574,20 +576,22 @@
@Override
public void onLoadCanceled(
ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) {
+ StatsDataSource dataSource = loadable.dataSource;
eventDispatcher.loadCanceled(
- loadable.dataSpec,
- loadable.dataSource.getLastOpenedUri(),
- loadable.dataSource.getLastResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ dataSource.getLastOpenedUri(),
+ dataSource.getLastResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ dataSource.getBytesRead()),
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ loadable.seekTimeUs,
- durationUs,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.dataSource.getBytesRead());
+ durationUs);
if (!released) {
copyLengthFromLoader(loadable);
for (SampleQueue sampleQueue : sampleQueues) {
@@ -621,10 +625,15 @@
: Loader.DONT_RETRY;
}
+ StatsDataSource dataSource = loadable.dataSource;
eventDispatcher.loadError(
- loadable.dataSpec,
- loadable.dataSource.getLastOpenedUri(),
- loadable.dataSource.getLastResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ dataSource.getLastOpenedUri(),
+ dataSource.getLastResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ dataSource.getBytesRead()),
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
@@ -632,9 +641,6 @@
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ loadable.seekTimeUs,
durationUs,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.dataSource.getBytesRead(),
error,
!loadErrorAction.isRetry());
return loadErrorAction;
@@ -680,7 +686,12 @@
return sampleQueues[i];
}
}
- SampleQueue trackOutput = new SampleQueue(allocator, drmSessionManager, eventDispatcher);
+ SampleQueue trackOutput =
+ new SampleQueue(
+ allocator,
+ /* playbackLooper= */ handler.getLooper(),
+ drmSessionManager,
+ eventDispatcher);
trackOutput.setUpstreamFormatChangeListener(this);
@NullableType
TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1);
@@ -776,16 +787,16 @@
long elapsedRealtimeMs =
loader.startLoading(
loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType));
+ DataSpec dataSpec = loadable.dataSpec;
eventDispatcher.loadStarted(
- loadable.dataSpec,
+ new LoadEventInfo(dataSpec, elapsedRealtimeMs),
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ loadable.seekTimeUs,
- durationUs,
- elapsedRealtimeMs);
+ durationUs);
}
/**
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
index 484aca5..3c08012 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
@@ -55,6 +55,7 @@
private final SampleDataQueue sampleDataQueue;
private final SampleExtrasHolder extrasHolder;
+ private final Looper playbackLooper;
private final DrmSessionManager drmSessionManager;
private final MediaSourceEventDispatcher eventDispatcher;
@Nullable private UpstreamFormatChangedListener upstreamFormatChangeListener;
@@ -94,6 +95,7 @@
* Creates a sample queue.
*
* @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
+ * @param playbackLooper The looper associated with the media playback thread.
* @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions}
* from. The created instance does not take ownership of this {@link DrmSessionManager}.
* @param eventDispatcher A {@link MediaSourceEventDispatcher} to notify of events related to this
@@ -101,11 +103,13 @@
*/
public SampleQueue(
Allocator allocator,
+ Looper playbackLooper,
DrmSessionManager drmSessionManager,
MediaSourceEventDispatcher eventDispatcher) {
- sampleDataQueue = new SampleDataQueue(allocator);
+ this.playbackLooper = playbackLooper;
this.drmSessionManager = drmSessionManager;
this.eventDispatcher = eventDispatcher;
+ sampleDataQueue = new SampleDataQueue(allocator);
extrasHolder = new SampleExtrasHolder();
capacity = SAMPLE_CAPACITY_INCREMENT;
sourceIds = new int[capacity];
@@ -477,13 +481,15 @@
}
@Override
- public final int sampleData(DataReader input, int length, boolean allowEndOfInput)
+ public final int sampleData(
+ DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart)
throws IOException {
return sampleDataQueue.sampleData(input, length, allowEndOfInput);
}
@Override
- public final void sampleData(ParsableByteArray buffer, int length) {
+ public final void sampleData(
+ ParsableByteArray buffer, int length, @SampleDataPart int sampleDataPart) {
sampleDataQueue.sampleData(buffer, length);
}
@@ -797,7 +803,6 @@
// Ensure we acquire the new session before releasing the previous one in case the same session
// is being used for both DrmInitData.
@Nullable DrmSession previousSession = currentDrmSession;
- Looper playbackLooper = Assertions.checkNotNull(Looper.myLooper());
currentDrmSession =
newDrmInitData != null
? drmSessionManager.acquireSession(playbackLooper, eventDispatcher, newDrmInitData)
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
index 8fb5d38..4bc0c0b 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
@@ -160,15 +160,14 @@
/* callback= */ this,
loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_MEDIA));
eventDispatcher.loadStarted(
- dataSpec,
+ new LoadEventInfo(dataSpec, elapsedRealtimeMs),
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
format,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
- durationUs,
- elapsedRealtimeMs);
+ durationUs);
return true;
}
@@ -217,39 +216,43 @@
sampleSize = (int) loadable.dataSource.getBytesRead();
sampleData = Assertions.checkNotNull(loadable.sampleData);
loadingFinished = true;
+ StatsDataSource dataSource = loadable.dataSource;
eventDispatcher.loadCompleted(
- loadable.dataSpec,
- loadable.dataSource.getLastOpenedUri(),
- loadable.dataSource.getLastResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ dataSource.getLastOpenedUri(),
+ dataSource.getLastResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ sampleSize),
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
format,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
- durationUs,
- elapsedRealtimeMs,
- loadDurationMs,
- sampleSize);
+ durationUs);
}
@Override
public void onLoadCanceled(
SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) {
+ StatsDataSource dataSource = loadable.dataSource;
eventDispatcher.loadCanceled(
- loadable.dataSpec,
- loadable.dataSource.getLastOpenedUri(),
- loadable.dataSource.getLastResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ dataSource.getLastOpenedUri(),
+ dataSource.getLastResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ dataSource.getBytesRead()),
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
- durationUs,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.dataSource.getBytesRead());
+ durationUs);
}
@Override
@@ -277,10 +280,15 @@
? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelay)
: Loader.DONT_RETRY_FATAL;
}
+ StatsDataSource dataSource = loadable.dataSource;
eventDispatcher.loadError(
- loadable.dataSpec,
- loadable.dataSource.getLastOpenedUri(),
- loadable.dataSource.getLastResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ dataSource.getLastOpenedUri(),
+ dataSource.getLastResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ dataSource.getBytesRead()),
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
format,
@@ -288,9 +296,6 @@
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
durationUs,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.dataSource.getBytesRead(),
error,
/* wasCanceled= */ !action.isRetry());
return action;
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
index d1b5e84..3599b42 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
@@ -18,6 +18,7 @@
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
+import android.os.SystemClock;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
@@ -44,11 +45,9 @@
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import org.checkerframework.checker.nullness.compatqual.NullableType;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link MediaSource} that inserts ads linearly with a provided content media source. This source
@@ -129,15 +128,13 @@
private final AdsLoader adsLoader;
private final AdsLoader.AdViewProvider adViewProvider;
private final Handler mainHandler;
- private final Map<MediaSource, List<MaskingMediaPeriod>> maskingMediaPeriodByAdMediaSource;
private final Timeline.Period period;
// Accessed on the player thread.
@Nullable private ComponentListener componentListener;
@Nullable private Timeline contentTimeline;
@Nullable private AdPlaybackState adPlaybackState;
- private @NullableType MediaSource[][] adGroupMediaSources;
- private @NullableType Timeline[][] adGroupTimelines;
+ private @NullableType AdMediaSourceHolder[][] adMediaSourceHolders;
/**
* Constructs a new source that inserts ads linearly with the content specified by {@code
@@ -179,10 +176,8 @@
this.adsLoader = adsLoader;
this.adViewProvider = adViewProvider;
mainHandler = new Handler(Looper.getMainLooper());
- maskingMediaPeriodByAdMediaSource = new HashMap<>();
period = new Timeline.Period();
- adGroupMediaSources = new MediaSource[0][];
- adGroupTimelines = new Timeline[0][];
+ adMediaSourceHolders = new AdMediaSourceHolder[0][];
adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes());
}
@@ -209,36 +204,21 @@
int adIndexInAdGroup = id.adIndexInAdGroup;
Uri adUri =
Assertions.checkNotNull(adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]);
- if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) {
+ if (adMediaSourceHolders[adGroupIndex].length <= adIndexInAdGroup) {
int adCount = adIndexInAdGroup + 1;
- adGroupMediaSources[adGroupIndex] =
- Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount);
- adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount);
+ adMediaSourceHolders[adGroupIndex] =
+ Arrays.copyOf(adMediaSourceHolders[adGroupIndex], adCount);
}
- MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup];
- if (mediaSource == null) {
- mediaSource = adMediaSourceFactory.createMediaSource(adUri);
- adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = mediaSource;
- maskingMediaPeriodByAdMediaSource.put(mediaSource, new ArrayList<>());
- prepareChildSource(id, mediaSource);
+ @Nullable
+ AdMediaSourceHolder adMediaSourceHolder =
+ adMediaSourceHolders[adGroupIndex][adIndexInAdGroup];
+ if (adMediaSourceHolder == null) {
+ MediaSource adMediaSource = adMediaSourceFactory.createMediaSource(adUri);
+ adMediaSourceHolder = new AdMediaSourceHolder(adMediaSource);
+ adMediaSourceHolders[adGroupIndex][adIndexInAdGroup] = adMediaSourceHolder;
+ prepareChildSource(id, adMediaSource);
}
- MaskingMediaPeriod maskingMediaPeriod =
- new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs);
- maskingMediaPeriod.setPrepareErrorListener(
- new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup));
- List<MaskingMediaPeriod> mediaPeriods = maskingMediaPeriodByAdMediaSource.get(mediaSource);
- if (mediaPeriods == null) {
- Object periodUid =
- Assertions.checkNotNull(adGroupTimelines[adGroupIndex][adIndexInAdGroup])
- .getUidOfPeriod(/* periodIndex= */ 0);
- MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber);
- maskingMediaPeriod.createPeriod(adSourceMediaPeriodId);
- } else {
- // Keep track of the masking media period so it can be populated with the real media period
- // when the source's info becomes available.
- mediaPeriods.add(maskingMediaPeriod);
- }
- return maskingMediaPeriod;
+ return adMediaSourceHolder.createMediaPeriod(adUri, id, allocator, startPositionUs);
} else {
MaskingMediaPeriod mediaPeriod =
new MaskingMediaPeriod(contentMediaSource, id, allocator, startPositionUs);
@@ -250,12 +230,18 @@
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
MaskingMediaPeriod maskingMediaPeriod = (MaskingMediaPeriod) mediaPeriod;
- List<MaskingMediaPeriod> mediaPeriods =
- maskingMediaPeriodByAdMediaSource.get(maskingMediaPeriod.mediaSource);
- if (mediaPeriods != null) {
- mediaPeriods.remove(maskingMediaPeriod);
+ MediaPeriodId id = maskingMediaPeriod.id;
+ if (id.isAd()) {
+ AdMediaSourceHolder adMediaSourceHolder =
+ Assertions.checkNotNull(adMediaSourceHolders[id.adGroupIndex][id.adIndexInAdGroup]);
+ adMediaSourceHolder.releaseMediaPeriod(maskingMediaPeriod);
+ if (adMediaSourceHolder.isInactive()) {
+ releaseChildSource(id);
+ adMediaSourceHolders[id.adGroupIndex][id.adIndexInAdGroup] = null;
+ }
+ } else {
+ maskingMediaPeriod.releasePeriod();
}
- maskingMediaPeriod.releasePeriod();
}
@Override
@@ -263,11 +249,9 @@
super.releaseSourceInternal();
Assertions.checkNotNull(componentListener).release();
componentListener = null;
- maskingMediaPeriodByAdMediaSource.clear();
contentTimeline = null;
adPlaybackState = null;
- adGroupMediaSources = new MediaSource[0][];
- adGroupTimelines = new Timeline[0][];
+ adMediaSourceHolders = new AdMediaSourceHolder[0][];
mainHandler.post(adsLoader::stop);
}
@@ -277,10 +261,13 @@
if (mediaPeriodId.isAd()) {
int adGroupIndex = mediaPeriodId.adGroupIndex;
int adIndexInAdGroup = mediaPeriodId.adIndexInAdGroup;
- onAdSourceInfoRefreshed(mediaSource, adGroupIndex, adIndexInAdGroup, timeline);
+ Assertions.checkNotNull(adMediaSourceHolders[adGroupIndex][adIndexInAdGroup])
+ .handleSourceInfoRefresh(timeline);
} else {
- onContentSourceInfoRefreshed(timeline);
+ Assertions.checkArgument(timeline.getPeriodCount() == 1);
+ contentTimeline = timeline;
}
+ maybeUpdateSourceInfo();
}
@Override
@@ -295,42 +282,17 @@
private void onAdPlaybackState(AdPlaybackState adPlaybackState) {
if (this.adPlaybackState == null) {
- adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][];
- Arrays.fill(adGroupMediaSources, new MediaSource[0]);
- adGroupTimelines = new Timeline[adPlaybackState.adGroupCount][];
- Arrays.fill(adGroupTimelines, new Timeline[0]);
+ adMediaSourceHolders = new AdMediaSourceHolder[adPlaybackState.adGroupCount][];
+ Arrays.fill(adMediaSourceHolders, new AdMediaSourceHolder[0]);
}
this.adPlaybackState = adPlaybackState;
maybeUpdateSourceInfo();
}
- private void onContentSourceInfoRefreshed(Timeline timeline) {
- Assertions.checkArgument(timeline.getPeriodCount() == 1);
- contentTimeline = timeline;
- maybeUpdateSourceInfo();
- }
-
- private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex,
- int adIndexInAdGroup, Timeline timeline) {
- Assertions.checkArgument(timeline.getPeriodCount() == 1);
- adGroupTimelines[adGroupIndex][adIndexInAdGroup] = timeline;
- List<MaskingMediaPeriod> mediaPeriods = maskingMediaPeriodByAdMediaSource.remove(mediaSource);
- if (mediaPeriods != null) {
- Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);
- for (int i = 0; i < mediaPeriods.size(); i++) {
- MaskingMediaPeriod mediaPeriod = mediaPeriods.get(i);
- MediaPeriodId adSourceMediaPeriodId =
- new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber);
- mediaPeriod.createPeriod(adSourceMediaPeriodId);
- }
- }
- maybeUpdateSourceInfo();
- }
-
private void maybeUpdateSourceInfo() {
- Timeline contentTimeline = this.contentTimeline;
+ @Nullable Timeline contentTimeline = this.contentTimeline;
if (adPlaybackState != null && contentTimeline != null) {
- adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurations(adGroupTimelines, period));
+ adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurationsUs());
Timeline timeline =
adPlaybackState.adGroupCount == 0
? contentTimeline
@@ -339,19 +301,16 @@
}
}
- private static long[][] getAdDurations(
- @NullableType Timeline[][] adTimelines, Timeline.Period period) {
- long[][] adDurations = new long[adTimelines.length][];
- for (int i = 0; i < adTimelines.length; i++) {
- adDurations[i] = new long[adTimelines[i].length];
- for (int j = 0; j < adTimelines[i].length; j++) {
- adDurations[i][j] =
- adTimelines[i][j] == null
- ? C.TIME_UNSET
- : adTimelines[i][j].getPeriod(/* periodIndex= */ 0, period).getDurationUs();
+ private long[][] getAdDurationsUs() {
+ long[][] adDurationsUs = new long[adMediaSourceHolders.length][];
+ for (int i = 0; i < adMediaSourceHolders.length; i++) {
+ adDurationsUs[i] = new long[adMediaSourceHolders[i].length];
+ for (int j = 0; j < adMediaSourceHolders[i].length; j++) {
+ @Nullable AdMediaSourceHolder holder = adMediaSourceHolders[i][j];
+ adDurationsUs[i][j] = holder == null ? C.TIME_UNSET : holder.getDurationUs();
}
}
- return adDurations;
+ return adDurationsUs;
}
/** Listener for component events. All methods are called on the main thread. */
@@ -396,13 +355,8 @@
}
createEventDispatcher(/* mediaPeriodId= */ null)
.loadError(
- dataSpec,
- dataSpec.uri,
- /* responseHeaders= */ Collections.emptyMap(),
+ new LoadEventInfo(dataSpec, /* elapsedRealtimeMs= */ SystemClock.elapsedRealtime()),
C.DATA_TYPE_AD,
- C.TRACK_TYPE_UNKNOWN,
- /* loadDurationMs= */ 0,
- /* bytesLoaded= */ 0,
error,
/* wasCanceled= */ true);
}
@@ -424,17 +378,70 @@
public void onPrepareError(MediaPeriodId mediaPeriodId, final IOException exception) {
createEventDispatcher(mediaPeriodId)
.loadError(
- new DataSpec(adUri),
- adUri,
- /* responseHeaders= */ Collections.emptyMap(),
+ new LoadEventInfo(
+ new DataSpec(adUri), /* elapsedRealtimeMs= */ SystemClock.elapsedRealtime()),
C.DATA_TYPE_AD,
- C.TRACK_TYPE_UNKNOWN,
- /* loadDurationMs= */ 0,
- /* bytesLoaded= */ 0,
AdLoadException.createForAd(exception),
/* wasCanceled= */ true);
mainHandler.post(
() -> adsLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception));
}
}
+
+ private final class AdMediaSourceHolder {
+
+ private final MediaSource adMediaSource;
+ private final List<MaskingMediaPeriod> activeMediaPeriods;
+
+ private @MonotonicNonNull Timeline timeline;
+
+ public AdMediaSourceHolder(MediaSource adMediaSource) {
+ this.adMediaSource = adMediaSource;
+ activeMediaPeriods = new ArrayList<>();
+ }
+
+ public MediaPeriod createMediaPeriod(
+ Uri adUri, MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ MaskingMediaPeriod maskingMediaPeriod =
+ new MaskingMediaPeriod(adMediaSource, id, allocator, startPositionUs);
+ maskingMediaPeriod.setPrepareErrorListener(
+ new AdPrepareErrorListener(adUri, id.adGroupIndex, id.adIndexInAdGroup));
+ activeMediaPeriods.add(maskingMediaPeriod);
+ if (timeline != null) {
+ Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);
+ MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber);
+ maskingMediaPeriod.createPeriod(adSourceMediaPeriodId);
+ }
+ return maskingMediaPeriod;
+ }
+
+ public void handleSourceInfoRefresh(Timeline timeline) {
+ Assertions.checkArgument(timeline.getPeriodCount() == 1);
+ if (this.timeline == null) {
+ Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);
+ for (int i = 0; i < activeMediaPeriods.size(); i++) {
+ MaskingMediaPeriod mediaPeriod = activeMediaPeriods.get(i);
+ MediaPeriodId adSourceMediaPeriodId =
+ new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber);
+ mediaPeriod.createPeriod(adSourceMediaPeriodId);
+ }
+ }
+ this.timeline = timeline;
+ }
+
+ public long getDurationUs() {
+ return timeline == null
+ ? C.TIME_UNSET
+ : timeline.getPeriod(/* periodIndex= */ 0, period).getDurationUs();
+ }
+
+ public void releaseMediaPeriod(MaskingMediaPeriod maskingMediaPeriod) {
+ activeMediaPeriods.remove(maskingMediaPeriod);
+ maskingMediaPeriod.releasePeriod();
+ }
+
+ public boolean isInactive() {
+ return activeMediaPeriods.isEmpty();
+ }
+ }
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
index 76a4665..f2362f2 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
@@ -203,13 +203,14 @@
}
@Override
- public int sampleData(DataReader input, int length, boolean allowEndOfInput)
+ public int sampleData(
+ DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart)
throws IOException {
return castNonNull(trackOutput).sampleData(input, length, allowEndOfInput);
}
@Override
- public void sampleData(ParsableByteArray data, int length) {
+ public void sampleData(ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) {
castNonNull(trackOutput).sampleData(data, length);
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
index fe7c583..c67b35c 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.chunk;
+import android.os.Looper;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@@ -23,6 +24,7 @@
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.SampleStream;
@@ -131,14 +133,22 @@
int[] trackTypes = new int[1 + embeddedTrackCount];
SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount];
- primarySampleQueue = new SampleQueue(allocator, drmSessionManager, eventDispatcher);
+ primarySampleQueue =
+ new SampleQueue(
+ allocator,
+ /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
+ drmSessionManager,
+ eventDispatcher);
trackTypes[0] = primaryTrackType;
sampleQueues[0] = primarySampleQueue;
for (int i = 0; i < embeddedTrackCount; i++) {
SampleQueue sampleQueue =
new SampleQueue(
- allocator, DrmSessionManager.getDummyDrmSessionManager(), eventDispatcher);
+ allocator,
+ /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
+ DrmSessionManager.getDummyDrmSessionManager(),
+ eventDispatcher);
embeddedSampleQueues[i] = sampleQueue;
sampleQueues[i + 1] = sampleQueue;
trackTypes[i + 1] = this.embeddedTrackTypes[i];
@@ -396,19 +406,20 @@
public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {
chunkSource.onChunkLoadCompleted(loadable);
eventDispatcher.loadCompleted(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
loadable.type,
primaryTrackType,
loadable.trackFormat,
loadable.trackSelectionReason,
loadable.trackSelectionData,
loadable.startTimeUs,
- loadable.endTimeUs,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ loadable.endTimeUs);
callback.onContinueLoadingRequested(this);
}
@@ -416,19 +427,20 @@
public void onLoadCanceled(
Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) {
eventDispatcher.loadCanceled(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
loadable.type,
primaryTrackType,
loadable.trackFormat,
loadable.trackSelectionReason,
loadable.trackSelectionData,
loadable.startTimeUs,
- loadable.endTimeUs,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ loadable.endTimeUs);
if (!released) {
primarySampleQueue.reset();
for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
@@ -484,9 +496,13 @@
boolean canceled = !loadErrorAction.isRetry();
eventDispatcher.loadError(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ bytesLoaded),
loadable.type,
primaryTrackType,
loadable.trackFormat,
@@ -494,9 +510,6 @@
loadable.trackSelectionData,
loadable.startTimeUs,
loadable.endTimeUs,
- elapsedRealtimeMs,
- loadDurationMs,
- bytesLoaded,
error,
canceled);
if (canceled) {
@@ -555,15 +568,14 @@
loader.startLoading(
loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));
eventDispatcher.loadStarted(
- loadable.dataSpec,
+ new LoadEventInfo(loadable.dataSpec, elapsedRealtimeMs),
loadable.type,
primaryTrackType,
loadable.trackFormat,
loadable.trackSelectionReason,
loadable.trackSelectionData,
loadable.startTimeUs,
- loadable.endTimeUs,
- elapsedRealtimeMs);
+ loadable.endTimeUs);
return true;
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
index eedcad3..8b954af 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
@@ -39,7 +39,7 @@
private final ChunkExtractorWrapper extractorWrapper;
- @MonotonicNonNull private TrackOutputProvider trackOutputProvider;
+ private @MonotonicNonNull TrackOutputProvider trackOutputProvider;
private long nextLoadPosition;
private volatile boolean loadCanceled;
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
index 927ee8b..bd652c6 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
@@ -108,7 +108,10 @@
return new Tx3gDecoder(format.initializationData);
case MimeTypes.APPLICATION_CEA608:
case MimeTypes.APPLICATION_MP4CEA608:
- return new Cea608Decoder(mimeType, format.accessibilityChannel);
+ return new Cea608Decoder(
+ mimeType,
+ format.accessibilityChannel,
+ Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS);
case MimeTypes.APPLICATION_CEA708:
return new Cea708Decoder(format.accessibilityChannel, format.initializationData);
case MimeTypes.APPLICATION_DVBSUBS:
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
index cce1bf6..75e86c4 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
@@ -26,10 +26,13 @@
import android.text.style.UnderlineSpan;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.text.SubtitleInputBuffer;
+import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
@@ -40,11 +43,15 @@
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
-/**
- * A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608").
- */
+/** A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608"). */
public final class Cea608Decoder extends CeaDecoder {
+ /**
+ * The minimum value for the {@code validDataChannelTimeoutMs} constructor parameter permitted by
+ * ANSI/CTA-608-E R-2014 Annex C.9.
+ */
+ public static final long MIN_DATA_CHANNEL_TIMEOUT_MS = 16_000;
+
private static final String TAG = "Cea608Decoder";
private static final int CC_VALID_FLAG = 0x04;
@@ -237,6 +244,7 @@
private final int packetLength;
private final int selectedField;
private final int selectedChannel;
+ private final long validDataChannelTimeoutUs;
private final ArrayList<CueBuilder> cueBuilders;
private CueBuilder currentCueBuilder;
@@ -257,11 +265,26 @@
// service bytes and drops the rest.
private boolean isInCaptionService;
- public Cea608Decoder(String mimeType, int accessibilityChannel) {
+ private long lastCueUpdateUs;
+
+ /**
+ * Constructs an instance.
+ *
+ * @param mimeType The MIME type of the CEA-608 data.
+ * @param accessibilityChannel The Accessibility channel, or {@link
+ * com.google.android.exoplayer2.Format#NO_VALUE} if unknown.
+ * @param validDataChannelTimeoutMs The timeout (in milliseconds) permitted by ANSI/CTA-608-E
+ * R-2014 Annex C.9 to clear "stuck" captions where no removal control code is received. The
+ * timeout should be at least {@link #MIN_DATA_CHANNEL_TIMEOUT_MS} or {@link C#TIME_UNSET} for
+ * no timeout.
+ */
+ public Cea608Decoder(String mimeType, int accessibilityChannel, long validDataChannelTimeoutMs) {
ccData = new ParsableByteArray();
cueBuilders = new ArrayList<>();
currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT);
currentChannel = NTSC_CC_CHANNEL_1;
+ this.validDataChannelTimeoutUs =
+ validDataChannelTimeoutMs > 0 ? validDataChannelTimeoutMs * 1000 : C.TIME_UNSET;
packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3;
switch (accessibilityChannel) {
case 1:
@@ -289,6 +312,7 @@
setCaptionMode(CC_MODE_UNKNOWN);
resetCueBuilders();
isInCaptionService = true;
+ lastCueUpdateUs = C.TIME_UNSET;
}
@Override
@@ -310,6 +334,7 @@
repeatableControlCc2 = 0;
currentChannel = NTSC_CC_CHANNEL_1;
isInCaptionService = true;
+ lastCueUpdateUs = C.TIME_UNSET;
}
@Override
@@ -317,6 +342,26 @@
// Do nothing
}
+ @Nullable
+ @Override
+ public SubtitleOutputBuffer dequeueOutputBuffer() throws SubtitleDecoderException {
+ SubtitleOutputBuffer outputBuffer = super.dequeueOutputBuffer();
+ if (outputBuffer != null) {
+ return outputBuffer;
+ }
+ if (shouldClearStuckCaptions()) {
+ outputBuffer = getAvailableOutputBuffer();
+ if (outputBuffer != null) {
+ cues = Collections.emptyList();
+ lastCueUpdateUs = C.TIME_UNSET;
+ Subtitle subtitle = createSubtitle();
+ outputBuffer.setContent(getPositionUs(), subtitle, Format.OFFSET_SAMPLE_RELATIVE);
+ return outputBuffer;
+ }
+ }
+ return null;
+ }
+
@Override
protected boolean isNewSubtitleDataAvailable() {
return cues != lastCues;
@@ -423,6 +468,7 @@
if (captionDataProcessed) {
if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
cues = getDisplayCues();
+ lastCueUpdateUs = getPositionUs();
}
}
}
@@ -1018,4 +1064,12 @@
}
+ /** See ANSI/CTA-608-E R-2014 Annex C.9 for Caption Erase Logic. */
+ private boolean shouldClearStuckCaptions() {
+ if (validDataChannelTimeoutUs == C.TIME_UNSET || lastCueUpdateUs == C.TIME_UNSET) {
+ return false;
+ }
+ long elapsedUs = getPositionUs() - lastCueUpdateUs;
+ return elapsedUs >= validDataChannelTimeoutUs;
+ }
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java
index 03a7255..f42b2a9 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java
@@ -179,6 +179,15 @@
*/
protected abstract void decode(SubtitleInputBuffer inputBuffer);
+ @Nullable
+ protected final SubtitleOutputBuffer getAvailableOutputBuffer() {
+ return availableOutputBuffers.pollFirst();
+ }
+
+ protected final long getPositionUs() {
+ return playbackPositionUs;
+ }
+
private static final class CeaInputBuffer extends SubtitleInputBuffer
implements Comparable<CeaInputBuffer> {
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java
index 51f5973..6e25dfc 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java
@@ -135,8 +135,7 @@
cues.add(Cue.EMPTY);
}
- Cue[] cuesArray = new Cue[cues.size()];
- cues.toArray(cuesArray);
+ Cue[] cuesArray = cues.toArray(new Cue[0]);
long[] cueTimesUsArray = cueTimesUs.toArray();
return new SubripSubtitle(cuesArray, cueTimesUsArray);
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
index 80009d4..e9d6d88 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
@@ -184,7 +184,7 @@
}
}
- private FrameAndTickRate parseFrameAndTickRates(XmlPullParser xmlParser)
+ private static FrameAndTickRate parseFrameAndTickRates(XmlPullParser xmlParser)
throws SubtitleDecoderException {
int frameRate = DEFAULT_FRAME_RATE;
String frameRateString = xmlParser.getAttributeValue(TTP, "frameRate");
@@ -218,8 +218,8 @@
return new FrameAndTickRate(frameRate * frameRateMultiplier, subFrameRate, tickRate);
}
- private CellResolution parseCellResolution(XmlPullParser xmlParser, CellResolution defaultValue)
- throws SubtitleDecoderException {
+ private static CellResolution parseCellResolution(
+ XmlPullParser xmlParser, CellResolution defaultValue) throws SubtitleDecoderException {
String cellResolution = xmlParser.getAttributeValue(TTP, "cellResolution");
if (cellResolution == null) {
return defaultValue;
@@ -244,7 +244,7 @@
}
@Nullable
- private TtsExtent parseTtsExtent(XmlPullParser xmlParser) {
+ private static TtsExtent parseTtsExtent(XmlPullParser xmlParser) {
@Nullable
String ttsExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
if (ttsExtent == null) {
@@ -266,7 +266,7 @@
}
}
- private Map<String, TtmlStyle> parseHeader(
+ private static Map<String, TtmlStyle> parseHeader(
XmlPullParser xmlParser,
Map<String, TtmlStyle> globalStyles,
CellResolution cellResolution,
@@ -301,7 +301,7 @@
return globalStyles;
}
- private void parseMetadata(XmlPullParser xmlParser, Map<String, String> imageMap)
+ private static void parseMetadata(XmlPullParser xmlParser, Map<String, String> imageMap)
throws IOException, XmlPullParserException {
do {
xmlParser.next();
@@ -324,7 +324,7 @@
* returned.
*/
@Nullable
- private TtmlRegion parseRegionAttributes(
+ private static TtmlRegion parseRegionAttributes(
XmlPullParser xmlParser, CellResolution cellResolution, @Nullable TtsExtent ttsExtent) {
@Nullable String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
if (regionId == null) {
@@ -456,13 +456,13 @@
/* textSize= */ regionTextHeight);
}
- private String[] parseStyleIds(String parentStyleIds) {
+ private static String[] parseStyleIds(String parentStyleIds) {
parentStyleIds = parentStyleIds.trim();
return parentStyleIds.isEmpty() ? new String[0] : Util.split(parentStyleIds, "\\s+");
}
- @PolyNull
- private TtmlStyle parseStyleAttributes(XmlPullParser parser, @PolyNull TtmlStyle style) {
+ private static @PolyNull TtmlStyle parseStyleAttributes(
+ XmlPullParser parser, @PolyNull TtmlStyle style) {
int attributeCount = parser.getAttributeCount();
for (int i = 0; i < attributeCount; i++) {
String attributeValue = parser.getAttributeValue(i);
@@ -611,11 +611,11 @@
return style;
}
- private TtmlStyle createIfNull(@Nullable TtmlStyle style) {
+ private static TtmlStyle createIfNull(@Nullable TtmlStyle style) {
return style == null ? new TtmlStyle() : style;
}
- private TtmlNode parseNode(
+ private static TtmlNode parseNode(
XmlPullParser parser,
@Nullable TtmlNode parent,
Map<String, TtmlRegion> regionMap,
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
index c8e9ed7..7b1dda1 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java
@@ -120,7 +120,7 @@
private final HashMap<String, Integer> nodeStartsByRegion;
private final HashMap<String, Integer> nodeEndsByRegion;
- @MonotonicNonNull private List<TtmlNode> children;
+ private @MonotonicNonNull List<TtmlNode> children;
public static TtmlNode buildTextNode(String text) {
return new TtmlNode(
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java
index 7bd96b2..3c974d8 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java
@@ -387,7 +387,7 @@
private static void parseLineAttribute(String s, WebvttCueInfoBuilder builder) {
int commaIndex = s.indexOf(',');
if (commaIndex != -1) {
- builder.lineAnchor = parsePositionAnchor(s.substring(commaIndex + 1));
+ builder.lineAnchor = parseLineAnchor(s.substring(commaIndex + 1));
s = s.substring(0, commaIndex);
}
if (s.endsWith("%")) {
@@ -405,6 +405,22 @@
}
}
+ @Cue.AnchorType
+ private static int parseLineAnchor(String s) {
+ switch (s) {
+ case "start":
+ return Cue.ANCHOR_TYPE_START;
+ case "center":
+ case "middle":
+ return Cue.ANCHOR_TYPE_MIDDLE;
+ case "end":
+ return Cue.ANCHOR_TYPE_END;
+ default:
+ Log.w(TAG, "Invalid anchor value: " + s);
+ return Cue.TYPE_UNSET;
+ }
+ }
+
private static void parsePositionAttribute(String s, WebvttCueInfoBuilder builder) {
int commaIndex = s.indexOf(',');
if (commaIndex != -1) {
@@ -417,11 +433,13 @@
@Cue.AnchorType
private static int parsePositionAnchor(String s) {
switch (s) {
+ case "line-left":
case "start":
return Cue.ANCHOR_TYPE_START;
case "center":
case "middle":
return Cue.ANCHOR_TYPE_MIDDLE;
+ case "line-right":
case "end":
return Cue.ANCHOR_TYPE_END;
default:
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
index 92dff8b..454674f 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
@@ -39,6 +39,78 @@
*/
public final class CacheDataSink implements DataSink {
+ /** {@link DataSink.Factory} for {@link CacheDataSink} instances. */
+ public static final class Factory implements DataSink.Factory {
+
+ private @MonotonicNonNull Cache cache;
+ private long fragmentSize;
+ private int bufferSize;
+
+ /** Creates an instance. */
+ public Factory() {
+ fragmentSize = CacheDataSink.DEFAULT_FRAGMENT_SIZE;
+ bufferSize = CacheDataSink.DEFAULT_BUFFER_SIZE;
+ }
+
+ /**
+ * Sets the cache to which data will be written.
+ *
+ * <p>Must be called before the factory is used.
+ *
+ * @param cache The cache to which data will be written.
+ * @return This factory.
+ */
+ public Factory setCache(Cache cache) {
+ this.cache = cache;
+ return this;
+ }
+
+ /**
+ * Sets the cache file fragment size. For requests that should be fragmented into multiple cache
+ * files, this is the maximum size of a cache file in bytes. If set to {@link C#LENGTH_UNSET}
+ * then no fragmentation will occur. Using a small value allows for finer-grained cache eviction
+ * policies, at the cost of increased overhead both on the cache implementation and the file
+ * system. Values under {@code (2 * 1024 * 1024)} are not recommended.
+ *
+ * <p>The default value is {@link CacheDataSink#DEFAULT_FRAGMENT_SIZE}.
+ *
+ * @param fragmentSize The fragment size in bytes, or {@link C#LENGTH_UNSET} to disable
+ * fragmentation.
+ * @return This factory.
+ */
+ public Factory setFragmentSize(long fragmentSize) {
+ this.fragmentSize = fragmentSize;
+ return this;
+ }
+
+ /**
+ * Sets the size of an in-memory buffer used when writing to a cache file. A zero or negative
+ * value disables buffering.
+ *
+ * <p>The default value is {@link CacheDataSink#DEFAULT_BUFFER_SIZE}.
+ *
+ * @param bufferSize The buffer size in bytes.
+ * @return This factory.
+ */
+ public Factory setBufferSize(int bufferSize) {
+ this.bufferSize = bufferSize;
+ return this;
+ }
+
+ @Override
+ public DataSink createDataSink() {
+ return new CacheDataSink(Assertions.checkNotNull(cache), fragmentSize, bufferSize);
+ }
+ }
+
+ /** Thrown when an {@link IOException} is encountered when writing data to the sink. */
+ public static final class CacheDataSinkException extends CacheException {
+
+ public CacheDataSinkException(IOException cause) {
+ super(cause);
+ }
+ }
+
/** Default {@code fragmentSize} recommended for caching use cases. */
public static final long DEFAULT_FRAGMENT_SIZE = 5 * 1024 * 1024;
/** Default buffer size in bytes. */
@@ -60,17 +132,6 @@
private @MonotonicNonNull ReusableBufferedOutputStream bufferedOutputStream;
/**
- * Thrown when IOException is encountered when writing data into sink.
- */
- public static class CacheDataSinkException extends CacheException {
-
- public CacheDataSinkException(IOException cause) {
- super(cause);
- }
-
- }
-
- /**
* Constructs an instance using {@link #DEFAULT_BUFFER_SIZE}.
*
* @param cache The cache into which data should be written.
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java
index ce9735b..effb5f2 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java
@@ -17,9 +17,8 @@
import com.google.android.exoplayer2.upstream.DataSink;
-/**
- * A {@link DataSink.Factory} that produces {@link CacheDataSink}.
- */
+/** @deprecated Use {@link CacheDataSink.Factory}. */
+@Deprecated
public final class CacheDataSinkFactory implements DataSink.Factory {
private final Cache cache;
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
index 5142f24..c316d75 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
@@ -23,11 +23,14 @@
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.FileDataSource;
+import com.google.android.exoplayer2.upstream.PriorityDataSource;
import com.google.android.exoplayer2.upstream.TeeDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.PriorityTaskManager;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.lang.annotation.Documented;
@@ -36,6 +39,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache
@@ -44,6 +48,251 @@
*/
public final class CacheDataSource implements DataSource {
+ /** {@link DataSource.Factory} for {@link CacheDataSource} instances. */
+ public static final class Factory implements DataSource.Factory {
+
+ private @MonotonicNonNull Cache cache;
+ private DataSource.Factory cacheReadDataSourceFactory;
+ @Nullable private DataSink.Factory cacheWriteDataSinkFactory;
+ private CacheKeyFactory cacheKeyFactory;
+ private boolean cacheIsReadOnly;
+ @Nullable private DataSource.Factory upstreamDataSourceFactory;
+ @Nullable private PriorityTaskManager upstreamPriorityTaskManager;
+ private int upstreamPriority;
+ @CacheDataSource.Flags private int flags;
+ @Nullable private CacheDataSource.EventListener eventListener;
+
+ public Factory() {
+ cacheReadDataSourceFactory = new FileDataSource.Factory();
+ cacheKeyFactory = CacheUtil.DEFAULT_CACHE_KEY_FACTORY;
+ }
+
+ /**
+ * Sets the cache that will be used.
+ *
+ * <p>Must be called before the factory is used.
+ *
+ * @param cache The cache that will be used.
+ * @return This factory.
+ */
+ public Factory setCache(Cache cache) {
+ this.cache = cache;
+ return this;
+ }
+
+ /**
+ * Sets the {@link DataSource.Factory} for {@link DataSource DataSources} for reading from the
+ * cache.
+ *
+ * <p>The default is a {@link FileDataSource.Factory} in its default configuration.
+ *
+ * @param cacheReadDataSourceFactory The {@link DataSource.Factory} for reading from the cache.
+ * @return This factory.
+ */
+ public Factory setCacheReadDataSourceFactory(DataSource.Factory cacheReadDataSourceFactory) {
+ this.cacheReadDataSourceFactory = cacheReadDataSourceFactory;
+ return this;
+ }
+
+ /**
+ * Sets the {@link DataSink.Factory} for generating {@link DataSink DataSinks} for writing data
+ * to the cache. Passing {@code null} causes the cache to be read-only.
+ *
+ * <p>The default is a {@link CacheDataSink.Factory} in its default configuration.
+ *
+ * @param cacheWriteDataSinkFactory The {@link DataSink.Factory} for generating {@link DataSink
+ * DataSinks} for writing data to the cache, or {@code null} to disable writing.
+ * @return This factory.
+ */
+ public Factory setCacheWriteDataSinkFactory(
+ @Nullable DataSink.Factory cacheWriteDataSinkFactory) {
+ this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory;
+ this.cacheIsReadOnly = cacheWriteDataSinkFactory == null;
+ return this;
+ }
+
+ /**
+ * Sets the {@link CacheKeyFactory}.
+ *
+ * <p>The default is {@link CacheUtil#DEFAULT_CACHE_KEY_FACTORY}.
+ *
+ * @param cacheKeyFactory The {@link CacheKeyFactory}.
+ * @return This factory.
+ */
+ public Factory setCacheKeyFactory(CacheKeyFactory cacheKeyFactory) {
+ this.cacheKeyFactory = cacheKeyFactory;
+ return this;
+ }
+
+ /**
+ * Sets the {@link DataSource.Factory} for upstream {@link DataSource DataSources}, which are
+ * used to read data in the case of a cache miss.
+ *
+ * <p>The default is {@code null}, and so this method must be called before the factory is used
+ * in order for data to be read from upstream in the case of a cache miss.
+ *
+ * @param upstreamDataSourceFactory The upstream {@link DataSource} for reading data not in the
+ * cache, or {@code null} to cause failure in the case of a cache miss.
+ * @return This factory.
+ */
+ public Factory setUpstreamDataSourceFactory(
+ @Nullable DataSource.Factory upstreamDataSourceFactory) {
+ this.upstreamDataSourceFactory = upstreamDataSourceFactory;
+ return this;
+ }
+
+ /**
+ * Sets an optional {@link PriorityTaskManager} to use when requesting data from upstream.
+ *
+ * <p>If set, reads from the upstream {@link DataSource} will only be allowed to proceed if
+ * there are no higher priority tasks registered to the {@link PriorityTaskManager}. If there
+ * exists a higher priority task then {@link PriorityTaskManager.PriorityTooLowException} will
+ * be thrown instead.
+ *
+ * <p>Note that requests to {@link CacheDataSource} instances are intended to be used as parts
+ * of (possibly larger) tasks that are registered with the {@link PriorityTaskManager}, and
+ * hence {@link CacheDataSource} does <em>not</em> register a task by itself. This must be done
+ * by the surrounding code that uses the {@link CacheDataSource} instances.
+ *
+ * <p>The default is {@code null}.
+ *
+ * @param upstreamPriorityTaskManager The upstream {@link PriorityTaskManager}.
+ * @return This factory.
+ */
+ public Factory setUpstreamPriorityTaskManager(
+ @Nullable PriorityTaskManager upstreamPriorityTaskManager) {
+ this.upstreamPriorityTaskManager = upstreamPriorityTaskManager;
+ return this;
+ }
+
+ /**
+ * Sets the priority to use when requesting data from upstream. The priority is only used if a
+ * {@link PriorityTaskManager} is set by calling {@link #setUpstreamPriorityTaskManager}.
+ *
+ * <p>The default is {@link C#PRIORITY_PLAYBACK}.
+ *
+ * @param upstreamPriority The priority to use when requesting data from upstream.
+ * @return This factory.
+ */
+ public Factory setUpstreamPriority(int upstreamPriority) {
+ this.upstreamPriority = upstreamPriority;
+ return this;
+ }
+
+ /**
+ * Sets the {@link CacheDataSource.Flags}.
+ *
+ * <p>The default is {@code 0}.
+ *
+ * @param flags The {@link CacheDataSource.Flags}.
+ * @return This factory.
+ */
+ public Factory setFlags(@CacheDataSource.Flags int flags) {
+ this.flags = flags;
+ return this;
+ }
+
+ /**
+ * Sets the {link EventListener} to which events are delivered.
+ *
+ * <p>The default is {@code null}.
+ *
+ * @param eventListener The {@link EventListener}.
+ * @return This factory.
+ */
+ public Factory setEventListener(@Nullable EventListener eventListener) {
+ this.eventListener = eventListener;
+ return this;
+ }
+
+ @Override
+ public CacheDataSource createDataSource() {
+ return createDataSourceInternal(
+ upstreamDataSourceFactory != null ? upstreamDataSourceFactory.createDataSource() : null,
+ flags,
+ upstreamPriority);
+ }
+
+ /**
+ * Returns an instance suitable for downloading content. The created instance is equivalent to
+ * one that would be created by {@link #createDataSource()}, except:
+ *
+ * <ul>
+ * <li>The {@link #FLAG_BLOCK_ON_CACHE} is always set.
+ * <li>The task priority is overridden to be {@link C#PRIORITY_DOWNLOAD}.
+ * </ul>
+ *
+ * @return An instance suitable for downloading content.
+ */
+ public CacheDataSource createDataSourceForDownloading() {
+ return createDataSourceInternal(
+ upstreamDataSourceFactory != null ? upstreamDataSourceFactory.createDataSource() : null,
+ flags | FLAG_BLOCK_ON_CACHE,
+ C.PRIORITY_DOWNLOAD);
+ }
+
+ /**
+ * Returns an instance suitable for reading cached content as part of removing a download. The
+ * created instance is equivalent to one that would be created by {@link #createDataSource()},
+ * except:
+ *
+ * <ul>
+ * <li>The upstream is overridden to be {@code null}, since when removing content we don't
+ * want to request anything that's not already cached.
+ * <li>The {@link #FLAG_BLOCK_ON_CACHE} is always set.
+ * <li>The task priority is overridden to be {@link C#PRIORITY_DOWNLOAD}.
+ * </ul>
+ *
+ * @return An instance suitable for reading cached content as part of removing a download.
+ */
+ public CacheDataSource createDataSourceForRemovingDownload() {
+ return createDataSourceInternal(
+ /* upstreamDataSource= */ null, flags | FLAG_BLOCK_ON_CACHE, C.PRIORITY_DOWNLOAD);
+ }
+
+ private CacheDataSource createDataSourceInternal(
+ @Nullable DataSource upstreamDataSource, @Flags int flags, int upstreamPriority) {
+ Cache cache = Assertions.checkNotNull(this.cache);
+ @Nullable DataSink cacheWriteDataSink;
+ if (cacheIsReadOnly || upstreamDataSource == null) {
+ cacheWriteDataSink = null;
+ } else if (cacheWriteDataSinkFactory != null) {
+ cacheWriteDataSink = cacheWriteDataSinkFactory.createDataSink();
+ } else {
+ cacheWriteDataSink = new CacheDataSink.Factory().setCache(cache).createDataSink();
+ }
+ return new CacheDataSource(
+ cache,
+ upstreamDataSource,
+ cacheReadDataSourceFactory.createDataSource(),
+ cacheWriteDataSink,
+ cacheKeyFactory,
+ flags,
+ upstreamPriorityTaskManager,
+ upstreamPriority,
+ eventListener);
+ }
+ }
+
+ /** Listener of {@link CacheDataSource} events. */
+ public interface EventListener {
+
+ /**
+ * Called when bytes have been read from the cache.
+ *
+ * @param cacheSizeBytes Current cache size in bytes.
+ * @param cachedBytesRead Total bytes read from the cache since this method was last called.
+ */
+ void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead);
+
+ /**
+ * Called when the current request ignores cache.
+ *
+ * @param reason Reason cache is bypassed.
+ */
+ void onCacheIgnored(@CacheIgnoredReason int reason);
+ }
+
/**
* Flags controlling the CacheDataSource's behavior. Possible flag values are {@link
* #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} and {@link
@@ -96,27 +345,6 @@
/** Cache ignored due to a request with an unset length. */
public static final int CACHE_IGNORED_REASON_UNSET_LENGTH = 1;
- /**
- * Listener of {@link CacheDataSource} events.
- */
- public interface EventListener {
-
- /**
- * Called when bytes have been read from the cache.
- *
- * @param cacheSizeBytes Current cache size in bytes.
- * @param cachedBytesRead Total bytes read from the cache since this method was last called.
- */
- void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead);
-
- /**
- * Called when the current request ignores cache.
- *
- * @param reason Reason cache is bypassed.
- */
- void onCacheIgnored(@CacheIgnoredReason int reason);
- }
-
/** Minimum number of bytes to read before checking cache for availability. */
private static final long MIN_READ_BEFORE_CHECKING_CACHE = 100 * 1024;
@@ -125,6 +353,8 @@
@Nullable private final DataSource cacheWriteDataSource;
private final DataSource upstreamDataSource;
private final CacheKeyFactory cacheKeyFactory;
+ @Nullable private final PriorityTaskManager upstreamPriorityTaskManager;
+ private final int upstreamPriority;
@Nullable private final EventListener eventListener;
private final boolean blockOnCache;
@@ -148,10 +378,11 @@
* reading and writing the cache.
*
* @param cache The cache.
- * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param upstreamDataSource A {@link DataSource} for reading data not in the cache. If null,
+ * reading will fail if a cache miss occurs.
*/
- public CacheDataSource(Cache cache, DataSource upstream) {
- this(cache, upstream, /* flags= */ 0);
+ public CacheDataSource(Cache cache, @Nullable DataSource upstreamDataSource) {
+ this(cache, upstreamDataSource, /* flags= */ 0);
}
/**
@@ -159,14 +390,15 @@
* reading and writing the cache.
*
* @param cache The cache.
- * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param upstreamDataSource A {@link DataSource} for reading data not in the cache. If null,
+ * reading will fail if a cache miss occurs.
* @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
* and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
*/
- public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) {
+ public CacheDataSource(Cache cache, @Nullable DataSource upstreamDataSource, @Flags int flags) {
this(
cache,
- upstream,
+ upstreamDataSource,
new FileDataSource(),
new CacheDataSink(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE),
flags,
@@ -179,7 +411,8 @@
* before it is written to disk.
*
* @param cache The cache.
- * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param upstreamDataSource A {@link DataSource} for reading data not in the cache. If null,
+ * reading will fail if a cache miss occurs.
* @param cacheReadDataSource A {@link DataSource} for reading data from the cache.
* @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is
* accessed read-only.
@@ -189,14 +422,14 @@
*/
public CacheDataSource(
Cache cache,
- DataSource upstream,
+ @Nullable DataSource upstreamDataSource,
DataSource cacheReadDataSource,
@Nullable DataSink cacheWriteDataSink,
@Flags int flags,
@Nullable EventListener eventListener) {
this(
cache,
- upstream,
+ upstreamDataSource,
cacheReadDataSource,
cacheWriteDataSink,
flags,
@@ -210,7 +443,8 @@
* before it is written to disk.
*
* @param cache The cache.
- * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param upstreamDataSource A {@link DataSource} for reading data not in the cache. If null,
+ * reading will fail if a cache miss occurs.
* @param cacheReadDataSource A {@link DataSource} for reading data from the cache.
* @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is
* accessed read-only.
@@ -221,12 +455,34 @@
*/
public CacheDataSource(
Cache cache,
- DataSource upstream,
+ @Nullable DataSource upstreamDataSource,
DataSource cacheReadDataSource,
@Nullable DataSink cacheWriteDataSink,
@Flags int flags,
@Nullable EventListener eventListener,
@Nullable CacheKeyFactory cacheKeyFactory) {
+ this(
+ cache,
+ upstreamDataSource,
+ cacheReadDataSource,
+ cacheWriteDataSink,
+ cacheKeyFactory,
+ flags,
+ /* upstreamPriorityTaskManager= */ null,
+ /* upstreamPriority= */ C.PRIORITY_PLAYBACK,
+ eventListener);
+ }
+
+ private CacheDataSource(
+ Cache cache,
+ @Nullable DataSource upstreamDataSource,
+ DataSource cacheReadDataSource,
+ @Nullable DataSink cacheWriteDataSink,
+ @Nullable CacheKeyFactory cacheKeyFactory,
+ @Flags int flags,
+ @Nullable PriorityTaskManager upstreamPriorityTaskManager,
+ int upstreamPriority,
+ @Nullable EventListener eventListener) {
this.cache = cache;
this.cacheReadDataSource = cacheReadDataSource;
this.cacheKeyFactory =
@@ -235,15 +491,55 @@
this.ignoreCacheOnError = (flags & FLAG_IGNORE_CACHE_ON_ERROR) != 0;
this.ignoreCacheForUnsetLengthRequests =
(flags & FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS) != 0;
- this.upstreamDataSource = upstream;
- if (cacheWriteDataSink != null) {
- this.cacheWriteDataSource = new TeeDataSource(upstream, cacheWriteDataSink);
+ this.upstreamPriority = upstreamPriority;
+ if (upstreamDataSource != null) {
+ if (upstreamPriorityTaskManager != null) {
+ upstreamDataSource =
+ new PriorityDataSource(
+ upstreamDataSource, upstreamPriorityTaskManager, upstreamPriority);
+ }
+ this.upstreamDataSource = upstreamDataSource;
+ this.upstreamPriorityTaskManager = upstreamPriorityTaskManager;
+ this.cacheWriteDataSource =
+ cacheWriteDataSink != null
+ ? new TeeDataSource(upstreamDataSource, cacheWriteDataSink)
+ : null;
} else {
+ this.upstreamDataSource = DummyDataSource.INSTANCE;
+ this.upstreamPriorityTaskManager = null;
this.cacheWriteDataSource = null;
}
this.eventListener = eventListener;
}
+ /** Returns the {@link Cache} used by this instance. */
+ public Cache getCache() {
+ return cache;
+ }
+
+ /** Returns the {@link CacheKeyFactory} used by this instance. */
+ public CacheKeyFactory getCacheKeyFactory() {
+ return cacheKeyFactory;
+ }
+
+ /**
+ * Returns the {@link PriorityTaskManager} used when there's a cache miss and requests need to be
+ * made to the upstream {@link DataSource}, or {@code null} if there is none.
+ */
+ @Nullable
+ public PriorityTaskManager getUpstreamPriorityTaskManager() {
+ return upstreamPriorityTaskManager;
+ }
+
+ /**
+ * Returns the priority used when there's a cache miss and requests need to be made to the
+ * upstream {@link DataSource}. The priority is only used if the source has a {@link
+ * PriorityTaskManager}.
+ */
+ public int getUpstreamPriority() {
+ return upstreamPriority;
+ }
+
@Override
public void addTransferListener(TransferListener transferListener) {
cacheReadDataSource.addTransferListener(transferListener);
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
index 21758bd..a9348b7 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
@@ -20,7 +20,8 @@
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.FileDataSource;
-/** A {@link DataSource.Factory} that produces {@link CacheDataSource}. */
+/** @deprecated Use {@link CacheDataSource.Factory}. */
+@Deprecated
public final class CacheDataSourceFactory implements DataSource.Factory {
private final Cache cache;
@@ -44,13 +45,14 @@
}
/** @see CacheDataSource#CacheDataSource(Cache, DataSource, int) */
+ @SuppressWarnings("deprecation")
public CacheDataSourceFactory(
Cache cache, DataSource.Factory upstreamFactory, @CacheDataSource.Flags int flags) {
this(
cache,
upstreamFactory,
new FileDataSource.Factory(),
- new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE),
+ new CacheDataSink.Factory().setCache(cache),
flags,
/* eventListener= */ null);
}
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
index 8da2fb1..5d99335 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java
@@ -107,10 +107,9 @@
*
* <p>This method may be slow and shouldn't normally be called on the main thread.
*
- * @param dataSpec Defines the data to be cached.
* @param cache A {@link Cache} to store the data.
- * @param cacheKeyFactory An optional factory for cache keys.
- * @param upstream A {@link DataSource} for reading data not in the cache.
+ * @param dataSpec Defines the data to be cached.
+ * @param upstreamDataSource A {@link DataSource} for reading data not in the cache.
* @param progressListener A listener to receive progress updates, or {@code null}.
* @param isCanceled An optional flag that will interrupt caching if set to true.
* @throws IOException If an error occurs reading from the source.
@@ -118,69 +117,55 @@
*/
@WorkerThread
public static void cache(
- DataSpec dataSpec,
Cache cache,
- @Nullable CacheKeyFactory cacheKeyFactory,
- DataSource upstream,
+ DataSpec dataSpec,
+ DataSource upstreamDataSource,
@Nullable ProgressListener progressListener,
@Nullable AtomicBoolean isCanceled)
throws IOException, InterruptedException {
cache(
+ new CacheDataSource(cache, upstreamDataSource),
dataSpec,
- cache,
- cacheKeyFactory,
- new CacheDataSource(cache, upstream),
- new byte[DEFAULT_BUFFER_SIZE_BYTES],
- /* priorityTaskManager= */ null,
- /* priority= */ 0,
progressListener,
isCanceled,
- /* enableEOFException= */ false);
+ /* enableEOFException= */ false,
+ new byte[DEFAULT_BUFFER_SIZE_BYTES]);
}
/**
- * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops
- * early if end of input is reached and {@code enableEOFException} is false.
+ * Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early
+ * if end of input is reached and {@code enableEOFException} is false.
*
- * <p>If a {@link PriorityTaskManager} is given, it's used to pause and resume caching depending
- * on {@code priority} and the priority of other tasks registered to the PriorityTaskManager.
- * Please note that it's the responsibility of the calling code to call {@link
- * PriorityTaskManager#add} to register with the manager before calling this method, and to call
- * {@link PriorityTaskManager#remove} afterwards to unregister.
+ * <p>If {@code dataSource} has a {@link PriorityTaskManager}, then it's the responsibility of the
+ * calling code to call {@link PriorityTaskManager#add} to register with the manager before
+ * calling this method, and to call {@link PriorityTaskManager#remove} afterwards to unregister.
*
* <p>This method may be slow and shouldn't normally be called on the main thread.
*
+ * @param dataSource A {@link CacheDataSource} to be used for caching the data.
* @param dataSpec Defines the data to be cached.
- * @param cache A {@link Cache} to store the data.
- * @param cacheKeyFactory An optional factory for cache keys.
- * @param dataSource A {@link CacheDataSource} that works on the {@code cache}.
- * @param buffer The buffer to be used while caching.
- * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
- * caching.
- * @param priority The priority of this task. Used with {@code priorityTaskManager}.
* @param progressListener A listener to receive progress updates, or {@code null}.
* @param isCanceled An optional flag that will interrupt caching if set to true.
* @param enableEOFException Whether to throw an {@link EOFException} if end of input has been
* reached unexpectedly.
+ * @param temporaryBuffer A temporary buffer to be used during caching.
* @throws IOException If an error occurs reading from the source.
* @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}.
*/
@WorkerThread
public static void cache(
- DataSpec dataSpec,
- Cache cache,
- @Nullable CacheKeyFactory cacheKeyFactory,
CacheDataSource dataSource,
- byte[] buffer,
- @Nullable PriorityTaskManager priorityTaskManager,
- int priority,
+ DataSpec dataSpec,
@Nullable ProgressListener progressListener,
@Nullable AtomicBoolean isCanceled,
- boolean enableEOFException)
+ boolean enableEOFException,
+ byte[] temporaryBuffer)
throws IOException, InterruptedException {
Assertions.checkNotNull(dataSource);
- Assertions.checkNotNull(buffer);
+ Assertions.checkNotNull(temporaryBuffer);
+ Cache cache = dataSource.getCache();
+ CacheKeyFactory cacheKeyFactory = dataSource.getCacheKeyFactory();
String key = buildCacheKey(dataSpec, cacheKeyFactory);
long bytesLeft;
@Nullable ProgressNotifier progressNotifier = null;
@@ -212,12 +197,10 @@
position,
length,
dataSource,
- buffer,
- priorityTaskManager,
- priority,
+ isCanceled,
progressNotifier,
isLastBlock,
- isCanceled);
+ temporaryBuffer);
if (read < blockLength) {
// Reached to the end of the data.
if (enableEOFException && !lengthUnset) {
@@ -249,14 +232,11 @@
* overwritten by the following parameters.
* @param position The position of the data to be read.
* @param length Length of the data to be read, or {@link C#LENGTH_UNSET} if it is unknown.
- * @param dataSource The {@link DataSource} to read the data from.
- * @param buffer The buffer to be used while downloading.
- * @param priorityTaskManager If not null it's used to check whether it is allowed to proceed with
- * caching.
- * @param priority The priority of this task.
+ * @param dataSource The {@link CacheDataSource} to read the data from.
+ * @param isCanceled An optional flag that will interrupt caching if set to true.
* @param progressNotifier A notifier through which to report progress updates, or {@code null}.
* @param isLastBlock Whether this read block is the last block of the content.
- * @param isCanceled An optional flag that will interrupt caching if set to true.
+ * @param temporaryBuffer A temporary buffer to be used during caching.
* @return Number of read bytes, or 0 if no data is available because the end of the opened range
* has been reached.
*/
@@ -264,21 +244,20 @@
DataSpec dataSpec,
long position,
long length,
- DataSource dataSource,
- byte[] buffer,
- @Nullable PriorityTaskManager priorityTaskManager,
- int priority,
+ CacheDataSource dataSource,
+ @Nullable AtomicBoolean isCanceled,
@Nullable ProgressNotifier progressNotifier,
boolean isLastBlock,
- @Nullable AtomicBoolean isCanceled)
+ byte[] temporaryBuffer)
throws IOException, InterruptedException {
long positionOffset = position - dataSpec.position;
long initialPositionOffset = positionOffset;
long endOffset = length != C.LENGTH_UNSET ? positionOffset + length : C.POSITION_UNSET;
+ @Nullable PriorityTaskManager priorityTaskManager = dataSource.getUpstreamPriorityTaskManager();
while (true) {
if (priorityTaskManager != null) {
// Wait for any other thread with higher priority to finish its job.
- priorityTaskManager.proceed(priority);
+ priorityTaskManager.proceed(dataSource.getUpstreamPriority());
}
throwExceptionIfInterruptedOrCancelled(isCanceled);
try {
@@ -310,11 +289,11 @@
throwExceptionIfInterruptedOrCancelled(isCanceled);
int bytesRead =
dataSource.read(
- buffer,
+ temporaryBuffer,
0,
endOffset != C.POSITION_UNSET
- ? (int) Math.min(buffer.length, endOffset - positionOffset)
- : buffer.length);
+ ? (int) Math.min(temporaryBuffer.length, endOffset - positionOffset)
+ : temporaryBuffer.length);
if (bytesRead == C.RESULT_END_OF_INPUT) {
if (progressNotifier != null) {
progressNotifier.onRequestLengthResolved(positionOffset);
diff --git a/tree/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/tree/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
index 794bc5f..91888cd 100644
--- a/tree/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
+++ b/tree/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
@@ -128,9 +128,9 @@
private long totalVideoFrameProcessingOffsetUs;
private int videoFrameProcessingOffsetCount;
- private int pendingRotationDegrees;
- private float pendingPixelWidthHeightRatio;
@Nullable private MediaFormat currentMediaFormat;
+ private int mediaFormatWidth;
+ private int mediaFormatHeight;
private int currentWidth;
private int currentHeight;
private int currentUnappliedRotationDegrees;
@@ -235,8 +235,9 @@
currentWidth = Format.NO_VALUE;
currentHeight = Format.NO_VALUE;
currentPixelWidthHeightRatio = Format.NO_VALUE;
- pendingPixelWidthHeightRatio = Format.NO_VALUE;
scalingMode = VIDEO_SCALING_MODE_DEFAULT;
+ mediaFormatWidth = Format.NO_VALUE;
+ mediaFormatHeight = Format.NO_VALUE;
clearReportedVideoSize();
}
@@ -603,10 +604,7 @@
@Override
protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
super.onInputFormatChanged(formatHolder);
- Format newFormat = formatHolder.format;
- eventDispatcher.inputFormatChanged(newFormat);
- pendingPixelWidthHeightRatio = newFormat.pixelWidthHeightRatio;
- pendingRotationDegrees = newFormat.rotationDegrees;
+ eventDispatcher.inputFormatChanged(formatHolder.format);
}
/**
@@ -637,23 +635,56 @@
&& outputMediaFormat.containsKey(KEY_CROP_LEFT)
&& outputMediaFormat.containsKey(KEY_CROP_BOTTOM)
&& outputMediaFormat.containsKey(KEY_CROP_TOP);
- int mediaFormatWidth =
+ mediaFormatWidth =
hasCrop
? outputMediaFormat.getInteger(KEY_CROP_RIGHT)
- outputMediaFormat.getInteger(KEY_CROP_LEFT)
+ 1
: outputMediaFormat.getInteger(MediaFormat.KEY_WIDTH);
- int mediaFormatHeight =
+ mediaFormatHeight =
hasCrop
? outputMediaFormat.getInteger(KEY_CROP_BOTTOM)
- outputMediaFormat.getInteger(KEY_CROP_TOP)
+ 1
: outputMediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
- processOutputFormat(codec, mediaFormatWidth, mediaFormatHeight);
+
+ // Must be applied each time the output MediaFormat changes.
+ codec.setVideoScalingMode(scalingMode);
maybeNotifyVideoFrameProcessingOffset();
}
@Override
+ protected void onOutputFormatChanged(Format outputFormat) {
+ configureOutput(outputFormat);
+ }
+
+ @Override
+ protected void configureOutput(Format outputFormat) {
+ if (tunneling) {
+ currentWidth = outputFormat.width;
+ currentHeight = outputFormat.height;
+ } else {
+ currentWidth = mediaFormatWidth;
+ currentHeight = mediaFormatHeight;
+ }
+ currentPixelWidthHeightRatio = outputFormat.pixelWidthHeightRatio;
+ if (Util.SDK_INT >= 21) {
+ // On API level 21 and above the decoder applies the rotation when rendering to the surface.
+ // Hence currentUnappliedRotation should always be 0. For 90 and 270 degree rotations, we need
+ // to flip the width, height and pixel aspect ratio to reflect the rotation that was applied.
+ if (outputFormat.rotationDegrees == 90 || outputFormat.rotationDegrees == 270) {
+ int rotatedHeight = currentWidth;
+ currentWidth = currentHeight;
+ currentHeight = rotatedHeight;
+ currentPixelWidthHeightRatio = 1 / currentPixelWidthHeightRatio;
+ }
+ } else {
+ // On API level 20 and below the decoder does not apply the rotation.
+ currentUnappliedRotationDegrees = outputFormat.rotationDegrees;
+ }
+ }
+
+ @Override
@TargetApi(29) // codecHandlesHdr10PlusOutOfBandMetadata is false if Util.SDK_INT < 29
protected void handleInputBufferSupplementalData(DecoderInputBuffer buffer)
throws ExoPlaybackException {
@@ -814,28 +845,6 @@
return false;
}
- private void processOutputFormat(MediaCodec codec, int width, int height) {
- currentWidth = width;
- currentHeight = height;
- currentPixelWidthHeightRatio = pendingPixelWidthHeightRatio;
- if (Util.SDK_INT >= 21) {
- // On API level 21 and above the decoder applies the rotation when rendering to the surface.
- // Hence currentUnappliedRotation should always be 0. For 90 and 270 degree rotations, we need
- // to flip the width, height and pixel aspect ratio to reflect the rotation that was applied.
- if (pendingRotationDegrees == 90 || pendingRotationDegrees == 270) {
- int rotatedHeight = currentWidth;
- currentWidth = currentHeight;
- currentHeight = rotatedHeight;
- currentPixelWidthHeightRatio = 1 / currentPixelWidthHeightRatio;
- }
- } else {
- // On API level 20 and below the decoder does not apply the rotation.
- currentUnappliedRotationDegrees = pendingRotationDegrees;
- }
- // Must be applied each time the output MediaFormat changes.
- codec.setVideoScalingMode(scalingMode);
- }
-
private void notifyFrameMetadataListener(
long presentationTimeUs, long releaseTimeNs, Format format, MediaFormat mediaFormat) {
if (frameMetadataListener != null) {
@@ -846,10 +855,7 @@
/** Called when a buffer was processed in tunneling mode. */
protected void onProcessedTunneledBuffer(long presentationTimeUs) {
- @Nullable Format format = updateOutputFormatForTime(presentationTimeUs);
- if (format != null) {
- processOutputFormat(getCodec(), format.width, format.height);
- }
+ updateOutputFormatForTime(presentationTimeUs);
maybeNotifyVideoSizeChanged();
decoderCounters.renderedOutputBufferCount++;
maybeNotifyRenderedFirstFrame();
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java
index 8ed0b21..f7065fb 100644
--- a/tree/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/DefaultLoadControlTest.java
@@ -49,9 +49,16 @@
public void shouldContinueLoading_untilMaxBufferExceeded() {
createDefaultLoadControl();
- assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue();
- assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isTrue();
- assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
+ assertThat(
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED))
+ .isTrue();
+ assertThat(
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, MAX_BUFFER_US - 1, SPEED))
+ .isTrue();
+ assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED))
+ .isFalse();
}
@Test
@@ -63,10 +70,18 @@
/* bufferForPlaybackAfterRebufferMs= */ 0);
createDefaultLoadControl();
- assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
- assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US - 1, SPEED)).isFalse();
- assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();
- assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue();
+ assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED))
+ .isFalse();
+ assertThat(
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, MAX_BUFFER_US - 1, SPEED))
+ .isFalse();
+ assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED))
+ .isFalse();
+ assertThat(
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, MIN_BUFFER_US - 1, SPEED))
+ .isTrue();
}
@Test
@@ -78,9 +93,14 @@
/* bufferForPlaybackAfterRebufferMs= */ 0);
createDefaultLoadControl();
- assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
- assertThat(loadControl.shouldContinueLoading(5 * C.MICROS_PER_SECOND, SPEED)).isFalse();
- assertThat(loadControl.shouldContinueLoading(500L, SPEED)).isTrue();
+ assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED))
+ .isFalse();
+ assertThat(
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, 5 * C.MICROS_PER_SECOND, SPEED))
+ .isFalse();
+ assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, 500L, SPEED))
+ .isTrue();
}
@Test
@@ -94,10 +114,18 @@
createDefaultLoadControl();
makeSureTargetBufferBytesReached();
- assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue();
- assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isTrue();
- assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();
- assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
+ assertThat(
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED))
+ .isTrue();
+ assertThat(
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, MIN_BUFFER_US - 1, SPEED))
+ .isTrue();
+ assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED))
+ .isFalse();
+ assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED))
+ .isFalse();
}
@Test
@@ -107,13 +135,24 @@
createDefaultLoadControl();
// Put loadControl in buffering state.
- assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isTrue();
+ assertThat(
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED))
+ .isTrue();
makeSureTargetBufferBytesReached();
- assertThat(loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, SPEED)).isFalse();
- assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US - 1, SPEED)).isFalse();
- assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();
- assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, SPEED)).isFalse();
+ assertThat(
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, SPEED))
+ .isFalse();
+ assertThat(
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, MIN_BUFFER_US - 1, SPEED))
+ .isFalse();
+ assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED))
+ .isFalse();
+ assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MAX_BUFFER_US, SPEED))
+ .isFalse();
}
@Test
@@ -126,16 +165,22 @@
createDefaultLoadControl();
// At normal playback speed, we stop buffering when the buffer reaches the minimum.
- assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, SPEED)).isFalse();
+ assertThat(loadControl.shouldContinueLoading(/* playbackPositionUs= */ 0, MIN_BUFFER_US, SPEED))
+ .isFalse();
// At double playback speed, we continue loading.
- assertThat(loadControl.shouldContinueLoading(MIN_BUFFER_US, /* playbackSpeed= */ 2f)).isTrue();
+ assertThat(
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, MIN_BUFFER_US, /* playbackSpeed= */ 2f))
+ .isTrue();
}
@Test
public void shouldNotContinueLoadingWithMaxBufferReached_inFastPlayback() {
createDefaultLoadControl();
- assertThat(loadControl.shouldContinueLoading(MAX_BUFFER_US, /* playbackSpeed= */ 100f))
+ assertThat(
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, MAX_BUFFER_US, /* playbackSpeed= */ 100f))
.isFalse();
}
@@ -153,7 +198,8 @@
loadControl.onTracksSelected(new Renderer[0], TrackGroupArray.EMPTY, new TrackSelectionArray());
assertThat(
- loadControl.shouldContinueLoading(/* bufferedDurationUs= */ 0, /* playbackSpeed= */ 1f))
+ loadControl.shouldContinueLoading(
+ /* playbackPositionUs= */ 0, /* bufferedDurationUs= */ 0, /* playbackSpeed= */ 1f))
.isTrue();
}
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
index b4101dc..d40052a 100644
--- a/tree/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
@@ -27,6 +27,8 @@
import android.media.AudioManager;
import android.os.Looper;
import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -48,6 +50,8 @@
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
+import com.google.android.exoplayer2.source.ads.AdsLoader;
+import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.testutil.ActionSchedule;
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget;
@@ -67,14 +71,17 @@
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.FakeTrackSelection;
import com.google.android.exoplayer2.testutil.FakeTrackSelector;
+import com.google.android.exoplayer2.testutil.TestExoPlayer;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocation;
import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -3500,11 +3507,12 @@
// Disabled until the flag to throw exceptions for [internal: b/144538905] is enabled by default.
@Ignore
@Test
- public void loadControlNeverWantsToLoad_throwsIllegalStateException() throws Exception {
+ public void loadControlNeverWantsToLoad_throwsIllegalStateException() {
LoadControl neverLoadingLoadControl =
new DefaultLoadControl() {
@Override
- public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
+ public boolean shouldContinueLoading(
+ long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
return false;
}
@@ -3540,11 +3548,85 @@
}
@Test
+ public void
+ nextLoadPositionExceedingLoadControlMaxBuffer_whileCurrentLoadInProgress_doesNotThrowException() {
+ long maxBufferUs = 2 * C.MICROS_PER_SECOND;
+ LoadControl loadControlWithMaxBufferUs =
+ new DefaultLoadControl() {
+ @Override
+ public boolean shouldContinueLoading(
+ long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
+ return bufferedDurationUs < maxBufferUs;
+ }
+
+ @Override
+ public boolean shouldStartPlayback(
+ long bufferedDurationUs, float playbackSpeed, boolean rebuffering) {
+ return true;
+ }
+ };
+ MediaSource mediaSourceWithLoadInProgress =
+ new FakeMediaSource(
+ new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT) {
+ @Override
+ protected FakeMediaPeriod createFakeMediaPeriod(
+ MediaPeriodId id,
+ TrackGroupArray trackGroupArray,
+ Allocator allocator,
+ EventDispatcher eventDispatcher,
+ @Nullable TransferListener transferListener) {
+ return new FakeMediaPeriod(trackGroupArray, eventDispatcher) {
+ @Override
+ public long getBufferedPositionUs() {
+ // Pretend not to have buffered data yet.
+ return 0;
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ // Set next load position beyond the maxBufferUs configured in the LoadControl.
+ return Long.MAX_VALUE;
+ }
+
+ @Override
+ public boolean isLoading() {
+ return true;
+ }
+ };
+ }
+ };
+ FakeRenderer rendererWaitingForData =
+ new FakeRenderer(C.TRACK_TYPE_VIDEO) {
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+ };
+
+ ExoPlayer player =
+ new TestExoPlayer.Builder(context)
+ .setRenderers(rendererWaitingForData)
+ .setLoadControl(loadControlWithMaxBufferUs)
+ .experimental_setThrowWhenStuckBuffering(true)
+ .build();
+ player.setMediaSource(mediaSourceWithLoadInProgress);
+ player.prepare();
+
+ // Wait until the MediaSource is prepared, i.e. returned its timeline, and at least one
+ // iteration of doSomeWork after this was run.
+ TestExoPlayer.runUntilTimelineChanged(player, /* expectedTimeline= */ null);
+ TestExoPlayer.runUntilPendingCommandsAreFullyHandled(player);
+
+ assertThat(player.getPlayerError()).isNull();
+ }
+
+ @Test
public void loadControlNeverWantsToPlay_playbackDoesNotGetStuck() throws Exception {
LoadControl neverLoadingOrPlayingLoadControl =
new DefaultLoadControl() {
@Override
- public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
+ public boolean shouldContinueLoading(
+ long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
return true;
}
@@ -4306,6 +4388,121 @@
}
@Test
+ public void setMediaSources_secondAdMediaSource_throws() throws Exception {
+ AdsMediaSource adsMediaSource =
+ new AdsMediaSource(
+ new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)),
+ new DefaultDataSourceFactory(
+ context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)),
+ new DummyAdsLoader(),
+ new DummyAdViewProvider());
+ Exception[] exception = {null};
+ ActionSchedule actionSchedule =
+ new ActionSchedule.Builder(TAG)
+ .executeRunnable(
+ new PlayerRunnable() {
+ @Override
+ public void run(SimpleExoPlayer player) {
+ try {
+ player.setMediaSource(adsMediaSource);
+ player.addMediaSource(adsMediaSource);
+ } catch (Exception e) {
+ exception[0] = e;
+ }
+ player.prepare();
+ }
+ })
+ .build();
+
+ new ExoPlayerTestRunner.Builder(context)
+ .setActionSchedule(actionSchedule)
+ .build()
+ .start(/* doPrepare= */ false)
+ .blockUntilActionScheduleFinished(TIMEOUT_MS)
+ .blockUntilEnded(TIMEOUT_MS);
+
+ assertThat(exception[0]).isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ public void setMediaSources_multipleMediaSourcesWithAd_throws() throws Exception {
+ MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1));
+ AdsMediaSource adsMediaSource =
+ new AdsMediaSource(
+ mediaSource,
+ new DefaultDataSourceFactory(
+ context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)),
+ new DummyAdsLoader(),
+ new DummyAdViewProvider());
+ final Exception[] exception = {null};
+ ActionSchedule actionSchedule =
+ new ActionSchedule.Builder(TAG)
+ .executeRunnable(
+ new PlayerRunnable() {
+ @Override
+ public void run(SimpleExoPlayer player) {
+ try {
+ List<MediaSource> sources = new ArrayList<>();
+ sources.add(mediaSource);
+ sources.add(adsMediaSource);
+ player.setMediaSources(sources);
+ } catch (Exception e) {
+ exception[0] = e;
+ }
+ player.prepare();
+ }
+ })
+ .build();
+
+ new ExoPlayerTestRunner.Builder(context)
+ .setActionSchedule(actionSchedule)
+ .build()
+ .start(/* doPrepare= */ false)
+ .blockUntilActionScheduleFinished(TIMEOUT_MS)
+ .blockUntilEnded(TIMEOUT_MS);
+
+ assertThat(exception[0]).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void setMediaSources_addingMediaSourcesWithAdToNonEmptyPlaylist_throws() throws Exception {
+ MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1));
+ AdsMediaSource adsMediaSource =
+ new AdsMediaSource(
+ mediaSource,
+ new DefaultDataSourceFactory(
+ context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)),
+ new DummyAdsLoader(),
+ new DummyAdViewProvider());
+ final Exception[] exception = {null};
+ ActionSchedule actionSchedule =
+ new ActionSchedule.Builder(TAG)
+ .waitForPlaybackState(Player.STATE_READY)
+ .executeRunnable(
+ new PlayerRunnable() {
+ @Override
+ public void run(SimpleExoPlayer player) {
+ try {
+ player.addMediaSource(adsMediaSource);
+ } catch (Exception e) {
+ exception[0] = e;
+ }
+ }
+ })
+ .build();
+
+ new ExoPlayerTestRunner.Builder(context)
+ .setMediaSources(mediaSource)
+ .setActionSchedule(actionSchedule)
+ .build()
+ .start()
+ .blockUntilActionScheduleFinished(TIMEOUT_MS)
+ .blockUntilEnded(TIMEOUT_MS);
+
+ assertThat(exception[0]).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
public void setMediaSources_empty_whenEmpty_correctMaskingWindowIndex() throws Exception {
Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1);
MediaSource secondMediaSource = new FakeMediaSource(secondTimeline);
@@ -6420,4 +6617,38 @@
return Loader.RETRY;
}
}
+
+ private static class DummyAdsLoader implements AdsLoader {
+
+ @Override
+ public void setPlayer(@Nullable Player player) {}
+
+ @Override
+ public void release() {}
+
+ @Override
+ public void setSupportedContentTypes(int... contentTypes) {}
+
+ @Override
+ public void start(AdsLoader.EventListener eventListener, AdViewProvider adViewProvider) {}
+
+ @Override
+ public void stop() {}
+
+ @Override
+ public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) {}
+ }
+
+ private static class DummyAdViewProvider implements AdsLoader.AdViewProvider {
+
+ @Override
+ public ViewGroup getAdViewGroup() {
+ return null;
+ }
+
+ @Override
+ public View[] getAdOverlayViews() {
+ return new View[0];
+ }
+ }
}
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java
index d0a9a49..b241351 100644
--- a/tree/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManagerTest.java
@@ -21,6 +21,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -1058,6 +1059,31 @@
verify(mockListener, never()).onSessionActive(any(), eq(adSessionId2));
}
+ @Test
+ public void finishAllSessions_callsOnSessionFinishedForAllCreatedSessions() {
+ Timeline timeline = new FakeTimeline(/* windowCount= */ 4);
+ EventTime eventTimeWindow0 =
+ createEventTime(timeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+ EventTime eventTimeWindow2 =
+ createEventTime(timeline, /* windowIndex= */ 2, /* mediaPeriodId= */ null);
+ // Actually create sessions for window 0 and 2.
+ sessionManager.updateSessions(eventTimeWindow0);
+ sessionManager.updateSessions(eventTimeWindow2);
+ // Query information about session for window 1, but don't create it.
+ sessionManager.getSessionForMediaPeriodId(
+ timeline,
+ new MediaPeriodId(
+ timeline.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true).uid,
+ /* windowSequenceNumber= */ 123));
+ verify(mockListener, times(2)).onSessionCreated(any(), anyString());
+
+ EventTime finishEventTime =
+ createEventTime(Timeline.EMPTY, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
+ sessionManager.finishAllSessions(finishEventTime);
+
+ verify(mockListener, times(2)).onSessionFinished(eq(finishEventTime), anyString(), eq(false));
+ }
+
private static EventTime createEventTime(
Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
return new EventTime(
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java
index f08d634..c6d4a59 100644
--- a/tree/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java
@@ -16,11 +16,20 @@
package com.google.android.exoplayer2.analytics;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import android.os.SystemClock;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.testutil.FakeTimeline;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -28,7 +37,7 @@
@RunWith(AndroidJUnit4.class)
public final class PlaybackStatsListenerTest {
- private static final AnalyticsListener.EventTime TEST_EVENT_TIME =
+ private static final AnalyticsListener.EventTime EMPTY_TIMELINE_EVENT_TIME =
new AnalyticsListener.EventTime(
/* realtimeMs= */ 500,
Timeline.EMPTY,
@@ -37,6 +46,58 @@
/* eventPlaybackPositionMs= */ 0,
/* currentPlaybackPositionMs= */ 0,
/* totalBufferedDurationMs= */ 0);
+ private static final Timeline TEST_TIMELINE = new FakeTimeline(/* windowCount= */ 1);
+ private static final AnalyticsListener.EventTime TEST_EVENT_TIME =
+ new AnalyticsListener.EventTime(
+ /* realtimeMs= */ 500,
+ TEST_TIMELINE,
+ /* windowIndex= */ 0,
+ new MediaSource.MediaPeriodId(
+ TEST_TIMELINE.getPeriod(
+ /* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true)
+ .uid,
+ /* windowSequenceNumber= */ 42),
+ /* eventPlaybackPositionMs= */ 123,
+ /* currentPlaybackPositionMs= */ 123,
+ /* totalBufferedDurationMs= */ 456);
+
+ @Test
+ public void events_duringInitialIdleState_dontCreateNewPlaybackStats() {
+ PlaybackStatsListener playbackStatsListener =
+ new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
+
+ playbackStatsListener.onPositionDiscontinuity(
+ EMPTY_TIMELINE_EVENT_TIME, Player.DISCONTINUITY_REASON_SEEK);
+ playbackStatsListener.onPlaybackSpeedChanged(
+ EMPTY_TIMELINE_EVENT_TIME, /* playbackSpeed= */ 2.0f);
+ playbackStatsListener.onPlayWhenReadyChanged(
+ EMPTY_TIMELINE_EVENT_TIME,
+ /* playWhenReady= */ true,
+ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
+
+ assertThat(playbackStatsListener.getPlaybackStats()).isNull();
+ }
+
+ @Test
+ public void stateChangeEvent_toNonIdle_createsInitialPlaybackStats() {
+ PlaybackStatsListener playbackStatsListener =
+ new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
+
+ playbackStatsListener.onPlaybackStateChanged(EMPTY_TIMELINE_EVENT_TIME, Player.STATE_BUFFERING);
+
+ assertThat(playbackStatsListener.getPlaybackStats()).isNotNull();
+ }
+
+ @Test
+ public void timelineChangeEvent_toNonEmpty_createsInitialPlaybackStats() {
+ PlaybackStatsListener playbackStatsListener =
+ new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
+
+ playbackStatsListener.onTimelineChanged(
+ TEST_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
+
+ assertThat(playbackStatsListener.getPlaybackStats()).isNotNull();
+ }
@Test
public void playback_withKeepHistory_updatesStats() {
@@ -65,4 +126,68 @@
assertThat(playbackStats).isNotNull();
assertThat(playbackStats.endedCount).isEqualTo(1);
}
+
+ @Test
+ public void finishedSession_callsCallback() {
+ PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class);
+ PlaybackStatsListener playbackStatsListener =
+ new PlaybackStatsListener(/* keepHistory= */ true, callback);
+
+ // Create session with an event and finish it by simulating removal from playlist.
+ playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_BUFFERING);
+ verify(callback, never()).onPlaybackStatsReady(any(), any());
+ playbackStatsListener.onTimelineChanged(
+ EMPTY_TIMELINE_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
+
+ verify(callback).onPlaybackStatsReady(eq(TEST_EVENT_TIME), any());
+ }
+
+ @Test
+ public void finishAllSessions_callsAllPendingCallbacks() {
+ AnalyticsListener.EventTime eventTimeWindow0 =
+ new AnalyticsListener.EventTime(
+ /* realtimeMs= */ 0,
+ Timeline.EMPTY,
+ /* windowIndex= */ 0,
+ /* mediaPeriodId= */ null,
+ /* eventPlaybackPositionMs= */ 0,
+ /* currentPlaybackPositionMs= */ 0,
+ /* totalBufferedDurationMs= */ 0);
+ AnalyticsListener.EventTime eventTimeWindow1 =
+ new AnalyticsListener.EventTime(
+ /* realtimeMs= */ 0,
+ Timeline.EMPTY,
+ /* windowIndex= */ 1,
+ /* mediaPeriodId= */ null,
+ /* eventPlaybackPositionMs= */ 0,
+ /* currentPlaybackPositionMs= */ 0,
+ /* totalBufferedDurationMs= */ 0);
+ PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class);
+ PlaybackStatsListener playbackStatsListener =
+ new PlaybackStatsListener(/* keepHistory= */ true, callback);
+ playbackStatsListener.onPlaybackStateChanged(eventTimeWindow0, Player.STATE_BUFFERING);
+ playbackStatsListener.onPlaybackStateChanged(eventTimeWindow1, Player.STATE_BUFFERING);
+
+ playbackStatsListener.finishAllSessions();
+
+ verify(callback, times(2)).onPlaybackStatsReady(any(), any());
+ verify(callback).onPlaybackStatsReady(eq(eventTimeWindow0), any());
+ verify(callback).onPlaybackStatsReady(eq(eventTimeWindow1), any());
+ }
+
+ @Test
+ public void finishAllSessions_doesNotCallCallbackAgainWhenSessionWouldBeAutomaticallyFinished() {
+ PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class);
+ PlaybackStatsListener playbackStatsListener =
+ new PlaybackStatsListener(/* keepHistory= */ true, callback);
+ playbackStatsListener.onPlaybackStateChanged(TEST_EVENT_TIME, Player.STATE_BUFFERING);
+ SystemClock.setCurrentTimeMillis(TEST_EVENT_TIME.realtimeMs + 100);
+
+ playbackStatsListener.finishAllSessions();
+ // Simulate removing the playback item to ensure the session would finish if it hadn't already.
+ playbackStatsListener.onTimelineChanged(
+ EMPTY_TIMELINE_EVENT_TIME, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
+
+ verify(callback).onPlaybackStatsReady(any(), any());
+ }
}
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessorTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessorTest.java
new file mode 100644
index 0000000..19a1ad1
--- /dev/null
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessorTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.audio.AudioProcessor.AudioFormat;
+import java.nio.ByteBuffer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link TrimmingAudioProcessor}. */
+@RunWith(AndroidJUnit4.class)
+public final class TrimmingAudioProcessorTest {
+
+ private static final AudioFormat AUDIO_FORMAT =
+ new AudioFormat(/* sampleRate= */ 44100, /* channelCount= */ 2, C.ENCODING_PCM_16BIT);
+ private static final int TRACK_ONE_UNTRIMMED_FRAME_COUNT = 1024;
+ private static final int TRACK_ONE_TRIM_START_FRAME_COUNT = 64;
+ private static final int TRACK_ONE_TRIM_END_FRAME_COUNT = 32;
+ private static final int TRACK_TWO_TRIM_START_FRAME_COUNT = 128;
+ private static final int TRACK_TWO_TRIM_END_FRAME_COUNT = 16;
+
+ private static final int TRACK_ONE_BUFFER_SIZE_BYTES =
+ AUDIO_FORMAT.bytesPerFrame * TRACK_ONE_UNTRIMMED_FRAME_COUNT;
+ private static final int TRACK_ONE_TRIMMED_BUFFER_SIZE_BYTES =
+ TRACK_ONE_BUFFER_SIZE_BYTES
+ - AUDIO_FORMAT.bytesPerFrame
+ * (TRACK_ONE_TRIM_START_FRAME_COUNT + TRACK_ONE_TRIM_END_FRAME_COUNT);
+
+ private TrimmingAudioProcessor trimmingAudioProcessor;
+
+ @Before
+ public void setUp() {
+ trimmingAudioProcessor = new TrimmingAudioProcessor();
+ }
+
+ @After
+ public void tearDown() {
+ trimmingAudioProcessor.reset();
+ }
+
+ @Test
+ public void flushTwice_trimsStartAndEnd() throws Exception {
+ trimmingAudioProcessor.setTrimFrameCount(
+ TRACK_ONE_TRIM_START_FRAME_COUNT, TRACK_ONE_TRIM_END_FRAME_COUNT);
+ trimmingAudioProcessor.configure(AUDIO_FORMAT);
+ trimmingAudioProcessor.flush();
+ trimmingAudioProcessor.flush();
+
+ int outputSizeBytes = feedAndDrainAudioProcessorToEndOfTrackOne();
+
+ assertThat(trimmingAudioProcessor.getTrimmedFrameCount())
+ .isEqualTo(TRACK_ONE_TRIM_START_FRAME_COUNT + TRACK_ONE_TRIM_END_FRAME_COUNT);
+ assertThat(outputSizeBytes).isEqualTo(TRACK_ONE_TRIMMED_BUFFER_SIZE_BYTES);
+ }
+
+ /**
+ * Feeds and drains the audio processor up to the end of track one, returning the total output
+ * size in bytes.
+ */
+ private int feedAndDrainAudioProcessorToEndOfTrackOne() throws Exception {
+ // Feed and drain the processor, simulating a gapless transition to another track.
+ ByteBuffer inputBuffer = ByteBuffer.allocate(TRACK_ONE_BUFFER_SIZE_BYTES);
+ int outputSize = 0;
+ while (!trimmingAudioProcessor.isEnded()) {
+ if (inputBuffer.hasRemaining()) {
+ trimmingAudioProcessor.queueInput(inputBuffer);
+ if (!inputBuffer.hasRemaining()) {
+ // Reconfigure for a next track then begin draining.
+ trimmingAudioProcessor.setTrimFrameCount(
+ TRACK_TWO_TRIM_START_FRAME_COUNT, TRACK_TWO_TRIM_END_FRAME_COUNT);
+ trimmingAudioProcessor.configure(AUDIO_FORMAT);
+ trimmingAudioProcessor.queueEndOfStream();
+ }
+ }
+ ByteBuffer outputBuffer = trimmingAudioProcessor.getOutput();
+ outputSize += outputBuffer.remaining();
+ outputBuffer.clear();
+ }
+ trimmingAudioProcessor.reset();
+ return outputSize;
+ }
+}
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.java
index c3d23c7..5955a94 100644
--- a/tree/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.java
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloaderFactoryTest.java
@@ -21,6 +21,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -32,9 +33,11 @@
@Test
public void createProgressiveDownloader() throws Exception {
- DownloaderConstructorHelper constructorHelper =
- new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
- DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
+ CacheDataSource.Factory cacheDataSourceFactory =
+ new CacheDataSource.Factory()
+ .setCache(Mockito.mock(Cache.class))
+ .setUpstreamDataSourceFactory(DummyDataSource.FACTORY);
+ DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory);
Downloader downloader =
factory.createDownloader(
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java
index 3c9d518..717d716 100644
--- a/tree/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java
@@ -16,12 +16,18 @@
package com.google.android.exoplayer2.source;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import android.content.Context;
import android.net.Uri;
+import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.source.ads.AdsLoader;
+import com.google.android.exoplayer2.source.ads.AdsMediaSource;
+import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays;
import java.util.Collections;
@@ -184,4 +190,65 @@
assertThat(supportedTypes).asList().containsExactly(C.TYPE_OTHER);
}
+
+ @Test
+ public void createMediaSource_withAdTagUri_callsAdsLoader() {
+ Context applicationContext = ApplicationProvider.getApplicationContext();
+ Uri adTagUri = Uri.parse(URI_MEDIA);
+ MediaItem mediaItem =
+ new MediaItem.Builder().setSourceUri(URI_MEDIA).setAdTagUri(adTagUri).build();
+ DefaultMediaSourceFactory defaultMediaSourceFactory =
+ new DefaultMediaSourceFactory(
+ applicationContext,
+ new DefaultDataSourceFactory(applicationContext, "userAgent"),
+ createAdSupportProvider(mock(AdsLoader.class), mock(AdsLoader.AdViewProvider.class)));
+
+ MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem);
+
+ assertThat(mediaSource).isInstanceOf(AdsMediaSource.class);
+ }
+
+ @Test
+ public void createMediaSource_withAdTagUriAdsLoaderNull_playsWithoutAdNoException() {
+ Context applicationContext = ApplicationProvider.getApplicationContext();
+ MediaItem mediaItem =
+ new MediaItem.Builder().setSourceUri(URI_MEDIA).setAdTagUri(Uri.parse(URI_MEDIA)).build();
+ DefaultMediaSourceFactory defaultMediaSourceFactory =
+ new DefaultMediaSourceFactory(
+ applicationContext,
+ new DefaultDataSourceFactory(applicationContext, "userAgent"),
+ createAdSupportProvider(/* adsLoader= */ null, mock(AdsLoader.AdViewProvider.class)));
+
+ MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem);
+
+ assertThat(mediaSource).isNotInstanceOf(AdsMediaSource.class);
+ }
+
+ @Test
+ public void createMediaSource_withAdTagUriProvidersNull_playsWithoutAdNoException() {
+ Context applicationContext = ApplicationProvider.getApplicationContext();
+ MediaItem mediaItem =
+ new MediaItem.Builder().setSourceUri(URI_MEDIA).setAdTagUri(Uri.parse(URI_MEDIA)).build();
+
+ MediaSource mediaSource =
+ DefaultMediaSourceFactory.newInstance(applicationContext).createMediaSource(mediaItem);
+
+ assertThat(mediaSource).isNotInstanceOf(AdsMediaSource.class);
+ }
+
+ private static DefaultMediaSourceFactory.AdSupportProvider createAdSupportProvider(
+ @Nullable AdsLoader adsLoader, AdsLoader.AdViewProvider adViewProvider) {
+ return new DefaultMediaSourceFactory.AdSupportProvider() {
+ @Nullable
+ @Override
+ public AdsLoader getAdsLoader(Uri adTagUri) {
+ return adsLoader;
+ }
+
+ @Override
+ public AdsLoader.AdViewProvider getAdViewProvider() {
+ return adViewProvider;
+ }
+ };
+ }
}
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
index 9a45248..41b953a 100644
--- a/tree/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java
@@ -26,6 +26,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.mockito.Mockito.when;
+import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
@@ -39,6 +40,7 @@
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MediaSourceEventDispatcher;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
@@ -139,7 +141,12 @@
ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any()))
.thenReturn(mockDrmSession);
eventDispatcher = new MediaSourceEventDispatcher();
- sampleQueue = new SampleQueue(allocator, mockDrmSessionManager, eventDispatcher);
+ sampleQueue =
+ new SampleQueue(
+ allocator,
+ /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
+ mockDrmSessionManager,
+ eventDispatcher);
formatHolder = new FormatHolder();
inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
}
@@ -356,7 +363,12 @@
public void isReadyReturnsTrueForClearSampleAndPlayClearSamplesWithoutKeysIsTrue() {
when(mockDrmSession.playClearSamplesWithoutKeys()).thenReturn(true);
// We recreate the queue to ensure the mock DRM session manager flags are taken into account.
- sampleQueue = new SampleQueue(allocator, mockDrmSessionManager, eventDispatcher);
+ sampleQueue =
+ new SampleQueue(
+ allocator,
+ /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
+ mockDrmSessionManager,
+ eventDispatcher);
writeTestDataWithEncryptedSections();
assertThat(sampleQueue.isReady(/* loadingFinished= */ false)).isTrue();
}
@@ -534,7 +546,12 @@
public void allowPlayClearSamplesWithoutKeysReadsClearSamples() {
when(mockDrmSession.playClearSamplesWithoutKeys()).thenReturn(true);
// We recreate the queue to ensure the mock DRM session manager flags are taken into account.
- sampleQueue = new SampleQueue(allocator, mockDrmSessionManager, eventDispatcher);
+ sampleQueue =
+ new SampleQueue(
+ allocator,
+ /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
+ mockDrmSessionManager,
+ eventDispatcher);
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED);
writeTestDataWithEncryptedSections();
@@ -924,7 +941,11 @@
public void adjustUpstreamFormat() {
String label = "label";
sampleQueue =
- new SampleQueue(allocator, mockDrmSessionManager, eventDispatcher) {
+ new SampleQueue(
+ allocator,
+ /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
+ mockDrmSessionManager,
+ eventDispatcher) {
@Override
public Format getAdjustedUpstreamFormat(Format format) {
return super.getAdjustedUpstreamFormat(copyWithLabel(format, label));
@@ -940,7 +961,11 @@
public void invalidateUpstreamFormatAdjustment() {
AtomicReference<String> label = new AtomicReference<>("label1");
sampleQueue =
- new SampleQueue(allocator, mockDrmSessionManager, eventDispatcher) {
+ new SampleQueue(
+ allocator,
+ /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()),
+ mockDrmSessionManager,
+ eventDispatcher) {
@Override
public Format getAdjustedUpstreamFormat(Format format) {
return super.getAdjustedUpstreamFormat(copyWithLabel(format, label.get()));
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java
new file mode 100644
index 0000000..255d129
--- /dev/null
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdsMediaSourceTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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.source.ads;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
+
+import android.net.Uri;
+import android.os.Looper;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;
+import com.google.android.exoplayer2.source.MediaSourceFactory;
+import com.google.android.exoplayer2.source.SinglePeriodTimeline;
+import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider;
+import com.google.android.exoplayer2.source.ads.AdsLoader.EventListener;
+import com.google.android.exoplayer2.testutil.FakeMediaSource;
+import com.google.android.exoplayer2.upstream.Allocator;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.annotation.LooperMode;
+
+/** Unit tests for {@link AdsMediaSource}. */
+@RunWith(AndroidJUnit4.class)
+@LooperMode(PAUSED)
+public final class AdsMediaSourceTest {
+
+ private static final long PREROLL_AD_DURATION_US = 10 * C.MICROS_PER_SECOND;
+ private static final Timeline PREROLL_AD_TIMELINE =
+ new SinglePeriodTimeline(
+ PREROLL_AD_DURATION_US,
+ /* isSeekable= */ true,
+ /* isDynamic= */ false,
+ /* isLive= */ false);
+ private static final Object PREROLL_AD_PERIOD_UID =
+ PREROLL_AD_TIMELINE.getUidOfPeriod(/* periodIndex= */ 0);
+
+ private static final long CONTENT_DURATION_US = 30 * C.MICROS_PER_SECOND;
+ private static final Timeline CONTENT_TIMELINE =
+ new SinglePeriodTimeline(
+ CONTENT_DURATION_US, /* isSeekable= */ true, /* isDynamic= */ false, /* isLive= */ false);
+ private static final Object CONTENT_PERIOD_UID =
+ CONTENT_TIMELINE.getUidOfPeriod(/* periodIndex= */ 0);
+
+ private static final AdPlaybackState AD_PLAYBACK_STATE =
+ new AdPlaybackState(/* adGroupTimesUs...= */ 0)
+ .withContentDurationUs(CONTENT_DURATION_US)
+ .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
+ .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.EMPTY)
+ .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
+ .withAdResumePositionUs(/* adResumePositionUs= */ 0);
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+ private FakeMediaSource contentMediaSource;
+ private FakeMediaSource prerollAdMediaSource;
+ @Mock private MediaSourceCaller mockMediaSourceCaller;
+ private AdsMediaSource adsMediaSource;
+
+ @Before
+ public void setUp() {
+ // Set up content and ad media sources, passing a null timeline so tests can simulate setting it
+ // later.
+ contentMediaSource = new FakeMediaSource(/* timeline= */ null);
+ prerollAdMediaSource = new FakeMediaSource(/* timeline= */ null);
+ MediaSourceFactory adMediaSourceFactory = mock(MediaSourceFactory.class);
+ when(adMediaSourceFactory.createMediaSource(any(Uri.class))).thenReturn(prerollAdMediaSource);
+
+ // Prepare the AdsMediaSource and capture its ads loader listener.
+ AdsLoader mockAdsLoader = mock(AdsLoader.class);
+ AdViewProvider mockAdViewProvider = mock(AdViewProvider.class);
+ ArgumentCaptor<EventListener> eventListenerArgumentCaptor =
+ ArgumentCaptor.forClass(AdsLoader.EventListener.class);
+ adsMediaSource =
+ new AdsMediaSource(
+ contentMediaSource, adMediaSourceFactory, mockAdsLoader, mockAdViewProvider);
+ adsMediaSource.prepareSource(mockMediaSourceCaller, /* mediaTransferListener= */ null);
+ shadowOf(Looper.getMainLooper()).idle();
+ verify(mockAdsLoader).start(eventListenerArgumentCaptor.capture(), eq(mockAdViewProvider));
+
+ // Simulate loading a preroll ad.
+ AdsLoader.EventListener adsLoaderEventListener = eventListenerArgumentCaptor.getValue();
+ adsLoaderEventListener.onAdPlaybackState(AD_PLAYBACK_STATE);
+ shadowOf(Looper.getMainLooper()).idle();
+ }
+
+ @Test
+ public void createPeriod_preparesChildAdMediaSourceAndRefreshesSourceInfo() {
+ contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE);
+ adsMediaSource.createPeriod(
+ new MediaPeriodId(
+ CONTENT_PERIOD_UID,
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0),
+ mock(Allocator.class),
+ /* startPositionUs= */ 0);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(prerollAdMediaSource.isPrepared()).isTrue();
+ verify(mockMediaSourceCaller)
+ .onSourceInfoRefreshed(
+ adsMediaSource, new SinglePeriodAdTimeline(CONTENT_TIMELINE, AD_PLAYBACK_STATE));
+ }
+
+ @Test
+ public void createPeriod_preparesChildAdMediaSourceAndRefreshesSourceInfoWithAdMediaSourceInfo() {
+ contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE);
+ adsMediaSource.createPeriod(
+ new MediaPeriodId(
+ CONTENT_PERIOD_UID,
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0),
+ mock(Allocator.class),
+ /* startPositionUs= */ 0);
+ prerollAdMediaSource.setNewSourceInfo(PREROLL_AD_TIMELINE);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ verify(mockMediaSourceCaller)
+ .onSourceInfoRefreshed(
+ adsMediaSource,
+ new SinglePeriodAdTimeline(
+ CONTENT_TIMELINE,
+ AD_PLAYBACK_STATE.withAdDurationsUs(new long[][] {{PREROLL_AD_DURATION_US}})));
+ }
+
+ @Test
+ public void createPeriod_createsChildPrerollAdMediaPeriod() {
+ contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE);
+ adsMediaSource.createPeriod(
+ new MediaPeriodId(
+ CONTENT_PERIOD_UID,
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0),
+ mock(Allocator.class),
+ /* startPositionUs= */ 0);
+ prerollAdMediaSource.setNewSourceInfo(PREROLL_AD_TIMELINE);
+ shadowOf(Looper.getMainLooper()).idle();
+
+ prerollAdMediaSource.assertMediaPeriodCreated(
+ new MediaPeriodId(PREROLL_AD_PERIOD_UID, /* windowSequenceNumber= */ 0));
+ }
+
+ @Test
+ public void createPeriod_createsChildContentMediaPeriod() {
+ contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE);
+ shadowOf(Looper.getMainLooper()).idle();
+ adsMediaSource.createPeriod(
+ new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0),
+ mock(Allocator.class),
+ /* startPositionUs= */ 0);
+
+ contentMediaSource.assertMediaPeriodCreated(
+ new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0));
+ }
+
+ @Test
+ public void releasePeriod_releasesChildMediaPeriodsAndSources() {
+ contentMediaSource.setNewSourceInfo(CONTENT_TIMELINE);
+ MediaPeriod prerollAdMediaPeriod =
+ adsMediaSource.createPeriod(
+ new MediaPeriodId(
+ CONTENT_PERIOD_UID,
+ /* adGroupIndex= */ 0,
+ /* adIndexInAdGroup= */ 0,
+ /* windowSequenceNumber= */ 0),
+ mock(Allocator.class),
+ /* startPositionUs= */ 0);
+ prerollAdMediaSource.setNewSourceInfo(PREROLL_AD_TIMELINE);
+ shadowOf(Looper.getMainLooper()).idle();
+ MediaPeriod contentMediaPeriod =
+ adsMediaSource.createPeriod(
+ new MediaPeriodId(CONTENT_PERIOD_UID, /* windowSequenceNumber= */ 0),
+ mock(Allocator.class),
+ /* startPositionUs= */ 0);
+ adsMediaSource.releasePeriod(prerollAdMediaPeriod);
+
+ prerollAdMediaSource.assertReleased();
+
+ adsMediaSource.releasePeriod(contentMediaPeriod);
+ adsMediaSource.releaseSource(mockMediaSourceCaller);
+ shadowOf(Looper.getMainLooper()).idle();
+ prerollAdMediaSource.assertReleased();
+ contentMediaSource.assertReleased();
+ }
+}
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java
index 48f1340..4bd26b8 100644
--- a/tree/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java
@@ -184,14 +184,14 @@
public void decodeWithPositioning() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE);
- assertThat(subtitle.getEventTimeCount()).isEqualTo(12);
+ assertThat(subtitle.getEventTimeCount()).isEqualTo(16);
assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
assertThat(subtitle.getEventTime(1)).isEqualTo(1_234_000L);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.text.toString()).isEqualTo("This is the first subtitle.");
- assertThat(firstCue.position).isEqualTo(0.1f);
- assertThat(firstCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_START);
+ assertThat(firstCue.position).isEqualTo(0.6f);
+ assertThat(firstCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
assertThat(firstCue.textAlignment).isEqualTo(Alignment.ALIGN_NORMAL);
assertThat(firstCue.size).isEqualTo(0.35f);
// Unspecified values should use WebVTT defaults
@@ -246,6 +246,18 @@
// Derived from `align:center`:
assertThat(sixthCue.position).isEqualTo(0.5f);
assertThat(sixthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_MIDDLE);
+
+ assertThat(subtitle.getEventTime(12)).isEqualTo(12_000_000L);
+ assertThat(subtitle.getEventTime(13)).isEqualTo(13_000_000L);
+ Cue seventhCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(12)));
+ assertThat(seventhCue.text.toString()).isEqualTo("This is the seventh subtitle.");
+ assertThat(seventhCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_START);
+
+ assertThat(subtitle.getEventTime(14)).isEqualTo(14_000_000L);
+ assertThat(subtitle.getEventTime(15)).isEqualTo(15_000_000L);
+ Cue eighthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(14)));
+ assertThat(eighthCue.text.toString()).isEqualTo("This is the eighth subtitle.");
+ assertThat(eighthCue.positionAnchor).isEqualTo(Cue.ANCHOR_TYPE_END);
}
@Test
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
index 707ea92..6562c17 100644
--- a/tree/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java
@@ -358,12 +358,7 @@
.appendReadData(1024 * 1024)
.endData());
CacheUtil.cache(
- unboundedDataSpec,
- cache,
- /* cacheKeyFactory= */ null,
- upstream2,
- /* progressListener= */ null,
- /* isCanceled= */ null);
+ cache, unboundedDataSpec, upstream2, /* progressListener= */ null, /* isCanceled= */ null);
// Read the rest of the data.
TestUtil.readToEnd(cacheDataSource);
@@ -407,12 +402,7 @@
.appendReadData(1024 * 1024)
.endData());
CacheUtil.cache(
- unboundedDataSpec,
- cache,
- /* cacheKeyFactory= */ null,
- upstream2,
- /* progressListener= */ null,
- /* isCanceled= */ null);
+ cache, unboundedDataSpec, upstream2, /* progressListener= */ null, /* isCanceled= */ null);
// Read the rest of the data.
TestUtil.readToEnd(cacheDataSource);
@@ -431,12 +421,7 @@
int halfDataLength = 512;
DataSpec dataSpec = buildDataSpec(halfDataLength, C.LENGTH_UNSET);
CacheUtil.cache(
- dataSpec,
- cache,
- /* cacheKeyFactory= */ null,
- upstream,
- /* progressListener= */ null,
- /* isCanceled= */ null);
+ cache, dataSpec, upstream, /* progressListener= */ null, /* isCanceled= */ null);
// Create cache read-only CacheDataSource.
CacheDataSource cacheDataSource =
@@ -467,12 +452,7 @@
int halfDataLength = 512;
DataSpec dataSpec = buildDataSpec(/* position= */ 0, halfDataLength);
CacheUtil.cache(
- dataSpec,
- cache,
- /* cacheKeyFactory= */ null,
- upstream,
- /* progressListener= */ null,
- /* isCanceled= */ null);
+ cache, dataSpec, upstream, /* progressListener= */ null, /* isCanceled= */ null);
// Create blocking CacheDataSource.
CacheDataSource cacheDataSource =
diff --git a/tree/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/tree/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java
index 65c9c4d..d0a4da4 100644
--- a/tree/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java
+++ b/tree/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java
@@ -201,12 +201,7 @@
CachingCounters counters = new CachingCounters();
CacheUtil.cache(
- new DataSpec(Uri.parse("test_data")),
- cache,
- /* cacheKeyFactory= */ null,
- dataSource,
- counters,
- /* isCanceled= */ null);
+ cache, new DataSpec(Uri.parse("test_data")), dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 100, 100);
assertCachedData(cache, fakeDataSet);
@@ -220,19 +215,12 @@
Uri testUri = Uri.parse("test_data");
DataSpec dataSpec = new DataSpec(testUri, /* position= */ 10, /* length= */ 20);
CachingCounters counters = new CachingCounters();
- CacheUtil.cache(
- dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
+ CacheUtil.cache(cache, dataSpec, dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 20, 20);
counters.reset();
- CacheUtil.cache(
- new DataSpec(testUri),
- cache,
- /* cacheKeyFactory= */ null,
- dataSource,
- counters,
- /* isCanceled= */ null);
+ CacheUtil.cache(cache, new DataSpec(testUri), dataSource, counters, /* isCanceled= */ null);
counters.assertValues(20, 80, 100);
assertCachedData(cache, fakeDataSet);
@@ -247,8 +235,7 @@
DataSpec dataSpec = new DataSpec(Uri.parse("test_data"));
CachingCounters counters = new CachingCounters();
- CacheUtil.cache(
- dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
+ CacheUtil.cache(cache, dataSpec, dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 100, 100);
assertCachedData(cache, fakeDataSet);
@@ -264,19 +251,12 @@
Uri testUri = Uri.parse("test_data");
DataSpec dataSpec = new DataSpec(testUri, /* position= */ 10, /* length= */ 20);
CachingCounters counters = new CachingCounters();
- CacheUtil.cache(
- dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
+ CacheUtil.cache(cache, dataSpec, dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 20, 20);
counters.reset();
- CacheUtil.cache(
- new DataSpec(testUri),
- cache,
- /* cacheKeyFactory= */ null,
- dataSource,
- counters,
- /* isCanceled= */ null);
+ CacheUtil.cache(cache, new DataSpec(testUri), dataSource, counters, /* isCanceled= */ null);
counters.assertValues(20, 80, 100);
assertCachedData(cache, fakeDataSet);
@@ -290,8 +270,7 @@
Uri testUri = Uri.parse("test_data");
DataSpec dataSpec = new DataSpec(testUri, /* position= */ 0, /* length= */ 1000);
CachingCounters counters = new CachingCounters();
- CacheUtil.cache(
- dataSpec, cache, /* cacheKeyFactory= */ null, dataSource, counters, /* isCanceled= */ null);
+ CacheUtil.cache(cache, dataSpec, dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 100, 1000);
assertCachedData(cache, fakeDataSet);
@@ -307,16 +286,12 @@
try {
CacheUtil.cache(
- dataSpec,
- cache,
- /* cacheKeyFactory= */ null,
new CacheDataSource(cache, dataSource),
- new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES],
- /* priorityTaskManager= */ null,
- /* priority= */ 0,
+ dataSpec,
/* progressListener= */ null,
/* isCanceled= */ null,
- /* enableEOFException= */ true);
+ /* enableEOFException= */ true,
+ /* temporaryBuffer= */ new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES]);
fail();
} catch (EOFException e) {
// Do nothing.
@@ -338,12 +313,7 @@
FakeDataSource dataSource = new FakeDataSource(fakeDataSet);
CacheUtil.cache(
- new DataSpec(Uri.parse("test_data")),
- cache,
- /* cacheKeyFactory= */ null,
- dataSource,
- counters,
- /* isCanceled= */ null);
+ cache, new DataSpec(Uri.parse("test_data")), dataSource, counters, /* isCanceled= */ null);
counters.assertValues(0, 300, 300);
assertCachedData(cache, fakeDataSet);
@@ -360,9 +330,6 @@
.setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION)
.build();
CacheUtil.cache(
- dataSpec,
- cache,
- /* cacheKeyFactory= */ null,
// Set fragmentSize to 10 to make sure there are multiple spans.
new CacheDataSource(
cache,
@@ -371,12 +338,11 @@
new CacheDataSink(cache, /* fragmentSize= */ 10),
/* flags= */ 0,
/* eventListener= */ null),
- new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES],
- /* priorityTaskManager= */ null,
- /* priority= */ 0,
+ dataSpec,
/* progressListener= */ null,
/* isCanceled= */ null,
- true);
+ /* enableEOFException= */ true,
+ /* temporaryBuffer= */ new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES]);
CacheUtil.remove(dataSpec, cache, /* cacheKeyFactory= */ null);
assertCacheEmpty(cache);
diff --git a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
index 2f2cc26..e1a441f 100644
--- a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
+++ b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.dash;
import android.util.Pair;
+import android.util.SparseArray;
import android.util.SparseIntArray;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
@@ -516,51 +517,94 @@
return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos);
}
+ /**
+ * Groups adaptation sets. Two adaptations sets belong to the same group if either:
+ *
+ * <ul>
+ * <li>One is a trick-play adaptation set and uses a {@code
+ * http://dashif.org/guidelines/trickmode} essential or supplemental property to indicate
+ * that the other is the main adaptation set to which it corresponds.
+ * <li>The two adaptation sets are marked as safe for switching using {@code
+ * urn:mpeg:dash:adaptation-set-switching:2016} supplemental properties.
+ * </ul>
+ *
+ * @param adaptationSets The adaptation sets to merge.
+ * @return An array of groups, where each group is an array of adaptation set indices.
+ */
private static int[][] getGroupedAdaptationSetIndices(List<AdaptationSet> adaptationSets) {
int adaptationSetCount = adaptationSets.size();
- SparseIntArray idToIndexMap = new SparseIntArray(adaptationSetCount);
+ SparseIntArray adaptationSetIdToIndex = new SparseIntArray(adaptationSetCount);
+ List<List<Integer>> adaptationSetGroupedIndices = new ArrayList<>(adaptationSetCount);
+ SparseArray<List<Integer>> adaptationSetIndexToGroupedIndices =
+ new SparseArray<>(adaptationSetCount);
+
+ // Initially make each adaptation set belong to its own group. Also build the
+ // adaptationSetIdToIndex map.
for (int i = 0; i < adaptationSetCount; i++) {
- idToIndexMap.put(adaptationSets.get(i).id, i);
+ adaptationSetIdToIndex.put(adaptationSets.get(i).id, i);
+ List<Integer> initialGroup = new ArrayList<>();
+ initialGroup.add(i);
+ adaptationSetGroupedIndices.add(initialGroup);
+ adaptationSetIndexToGroupedIndices.put(i, initialGroup);
}
- int[][] groupedAdaptationSetIndices = new int[adaptationSetCount][];
- boolean[] adaptationSetUsedFlags = new boolean[adaptationSetCount];
-
- int groupCount = 0;
+ // Merge adaptation set groups.
for (int i = 0; i < adaptationSetCount; i++) {
- if (adaptationSetUsedFlags[i]) {
- // This adaptation set has already been included in a group.
- continue;
- }
- adaptationSetUsedFlags[i] = true;
+ int mergedGroupIndex = i;
+ AdaptationSet adaptationSet = adaptationSets.get(i);
+
+ // Trick-play adaptation sets are merged with their corresponding main adaptation sets.
@Nullable
- Descriptor adaptationSetSwitchingProperty =
- findAdaptationSetSwitchingProperty(adaptationSets.get(i).supplementalProperties);
- if (adaptationSetSwitchingProperty == null) {
- groupedAdaptationSetIndices[groupCount++] = new int[] {i};
- } else {
- String[] extraAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ",");
- int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length];
- adaptationSetIndices[0] = i;
- int outputIndex = 1;
- for (String adaptationSetId : extraAdaptationSetIds) {
- int extraIndex =
- idToIndexMap.get(Integer.parseInt(adaptationSetId), /* valueIfKeyNotFound= */ -1);
- if (extraIndex != -1) {
- adaptationSetUsedFlags[extraIndex] = true;
- adaptationSetIndices[outputIndex] = extraIndex;
- outputIndex++;
+ Descriptor trickPlayProperty = findTrickPlayProperty(adaptationSet.essentialProperties);
+ if (trickPlayProperty == null) {
+ // Trick-play can also be specified using a supplemental property.
+ trickPlayProperty = findTrickPlayProperty(adaptationSet.supplementalProperties);
+ }
+ if (trickPlayProperty != null) {
+ int mainAdaptationSetId = Integer.parseInt(trickPlayProperty.value);
+ int mainAdaptationSetIndex =
+ adaptationSetIdToIndex.get(mainAdaptationSetId, /* valueIfKeyNotFound= */ -1);
+ if (mainAdaptationSetIndex != -1) {
+ mergedGroupIndex = mainAdaptationSetIndex;
+ }
+ }
+
+ // Adaptation sets that are safe for switching are merged, using the smallest index for the
+ // merged group.
+ if (mergedGroupIndex == i) {
+ @Nullable
+ Descriptor adaptationSetSwitchingProperty =
+ findAdaptationSetSwitchingProperty(adaptationSet.supplementalProperties);
+ if (adaptationSetSwitchingProperty != null) {
+ String[] otherAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ",");
+ for (String adaptationSetId : otherAdaptationSetIds) {
+ int otherAdaptationSetId =
+ adaptationSetIdToIndex.get(
+ Integer.parseInt(adaptationSetId), /* valueIfKeyNotFound= */ -1);
+ if (otherAdaptationSetId != -1) {
+ mergedGroupIndex = Math.min(mergedGroupIndex, otherAdaptationSetId);
+ }
}
}
- if (outputIndex < adaptationSetIndices.length) {
- adaptationSetIndices = Arrays.copyOf(adaptationSetIndices, outputIndex);
- }
- groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices;
+ }
+
+ // Merge the groups if necessary.
+ if (mergedGroupIndex != i) {
+ List<Integer> thisGroup = adaptationSetIndexToGroupedIndices.get(i);
+ List<Integer> mergedGroup = adaptationSetIndexToGroupedIndices.get(mergedGroupIndex);
+ mergedGroup.addAll(thisGroup);
+ adaptationSetIndexToGroupedIndices.put(i, mergedGroup);
+ adaptationSetGroupedIndices.remove(thisGroup);
}
}
- return groupCount < adaptationSetCount
- ? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : groupedAdaptationSetIndices;
+ int[][] groupedAdaptationSetIndices = new int[adaptationSetGroupedIndices.size()][];
+ for (int i = 0; i < groupedAdaptationSetIndices.length; i++) {
+ groupedAdaptationSetIndices[i] = Util.toArray(adaptationSetGroupedIndices.get(i));
+ // Restore the original adaptation set order within each group.
+ Arrays.sort(groupedAdaptationSetIndices[i]);
+ }
+ return groupedAdaptationSetIndices;
}
/**
@@ -747,9 +791,19 @@
@Nullable
private static Descriptor findAdaptationSetSwitchingProperty(List<Descriptor> descriptors) {
+ return findDescriptor(descriptors, "urn:mpeg:dash:adaptation-set-switching:2016");
+ }
+
+ @Nullable
+ private static Descriptor findTrickPlayProperty(List<Descriptor> descriptors) {
+ return findDescriptor(descriptors, "http://dashif.org/guidelines/trickmode");
+ }
+
+ @Nullable
+ private static Descriptor findDescriptor(List<Descriptor> descriptors, String schemeIdUri) {
for (int i = 0; i < descriptors.size(); i++) {
Descriptor descriptor = descriptors.get(i);
- if ("urn:mpeg:dash:adaptation-set-switching:2016".equals(descriptor.schemeIdUri)) {
+ if (schemeIdUri.equals(descriptor.schemeIdUri)) {
return descriptor;
}
}
diff --git a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
index 919997e..53c9f1c 100644
--- a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
+++ b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
@@ -33,6 +33,7 @@
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
+import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
@@ -754,13 +755,14 @@
/* package */ void onManifestLoadCompleted(ParsingLoadable<DashManifest> loadable,
long elapsedRealtimeMs, long loadDurationMs) {
manifestEventDispatcher.loadCompleted(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
- loadable.type,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
+ loadable.type);
DashManifest newManifest = loadable.getResult();
int oldPeriodCount = manifest == null ? 0 : manifest.getPeriodCount();
@@ -856,13 +858,14 @@
? Loader.DONT_RETRY_FATAL
: Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs);
manifestEventDispatcher.loadError(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
loadable.type,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded(),
error,
!loadErrorAction.isRetry());
return loadErrorAction;
@@ -871,13 +874,14 @@
/* package */ void onUtcTimestampLoadCompleted(ParsingLoadable<Long> loadable,
long elapsedRealtimeMs, long loadDurationMs) {
manifestEventDispatcher.loadCompleted(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
- loadable.type,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
+ loadable.type);
onUtcTimestampResolved(loadable.getResult() - elapsedRealtimeMs);
}
@@ -887,15 +891,16 @@
long loadDurationMs,
IOException error) {
manifestEventDispatcher.loadError(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
loadable.type,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded(),
error,
- true);
+ /* wasCanceled= */ true);
onUtcTimestampResolutionError(error);
return Loader.DONT_RETRY;
}
@@ -903,13 +908,14 @@
/* package */ void onLoadCanceled(ParsingLoadable<?> loadable, long elapsedRealtimeMs,
long loadDurationMs) {
manifestEventDispatcher.loadCanceled(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
- loadable.type,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
+ loadable.type);
}
// Internal methods.
@@ -1122,7 +1128,8 @@
private <T> void startLoading(ParsingLoadable<T> loadable,
Loader.Callback<ParsingLoadable<T>> callback, int minRetryCount) {
long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount);
- manifestEventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);
+ manifestEventDispatcher.loadStarted(
+ new LoadEventInfo(loadable.dataSpec, elapsedRealtimeMs), loadable.type);
}
private static final class PeriodSeekInfo {
diff --git a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java
index 504b2f4..7888841 100644
--- a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java
+++ b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java
@@ -288,6 +288,7 @@
this.sampleQueue =
new SampleQueue(
allocator,
+ /* playbackLooper= */ handler.getLooper(),
DrmSessionManager.getDummyDrmSessionManager(),
new MediaSourceEventDispatcher());
formatHolder = new FormatHolder();
@@ -300,13 +301,14 @@
}
@Override
- public int sampleData(DataReader input, int length, boolean allowEndOfInput)
+ public int sampleData(
+ DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart)
throws IOException {
return sampleQueue.sampleData(input, length, allowEndOfInput);
}
@Override
- public void sampleData(ParsableByteArray data, int length) {
+ public void sampleData(ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) {
sampleQueue.sampleData(data, length);
}
diff --git a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java
index 7f76e65..8629c91 100644
--- a/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java
+++ b/tree/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java
@@ -20,7 +20,6 @@
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.offline.DownloadException;
-import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.SegmentDownloader;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.dash.DashSegmentIndex;
@@ -35,9 +34,11 @@
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
/**
* A downloader for DASH streams.
@@ -46,19 +47,20 @@
*
* <pre>{@code
* SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
- * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
- * DownloaderConstructorHelper constructorHelper =
- * new DownloaderConstructorHelper(cache, factory);
+ * CacheDataSource.Factory cacheDataSourceFactory =
+ * new CacheDataSource.Factory()
+ * .setCache(cache)
+ * .setUpstreamDataSourceFactory(new DefaultHttpDataSourceFactory(userAgent));
* // Create a downloader for the first representation of the first adaptation set of the first
* // period.
* DashDownloader dashDownloader =
* new DashDownloader(
- * manifestUrl, Collections.singletonList(new StreamKey(0, 0, 0)), constructorHelper);
+ * manifestUrl, Collections.singletonList(new StreamKey(0, 0, 0)), cacheDataSourceFactory);
* // Perform the download.
* dashDownloader.download(progressListener);
- * // Access downloaded data using CacheDataSource
- * CacheDataSource cacheDataSource =
- * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
+ * // Use the downloaded data for playback.
+ * DashMediaSource mediaSource =
+ * new DashMediaSource.Factory(cacheDataSourceFactory).createMediaSource(mediaItem);
* }</pre>
*/
public final class DashDownloader extends SegmentDownloader<DashManifest> {
@@ -67,11 +69,30 @@
* @param manifestUri The {@link Uri} of the manifest to be downloaded.
* @param streamKeys Keys defining which representations in the manifest should be selected for
* download. If empty, all representations are downloaded.
- * @param constructorHelper A {@link DownloaderConstructorHelper} instance.
+ * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
+ * download will be written.
*/
public DashDownloader(
- Uri manifestUri, List<StreamKey> streamKeys, DownloaderConstructorHelper constructorHelper) {
- super(manifestUri, streamKeys, constructorHelper);
+ Uri manifestUri, List<StreamKey> streamKeys, CacheDataSource.Factory cacheDataSourceFactory) {
+ this(manifestUri, streamKeys, cacheDataSourceFactory, Runnable::run);
+ }
+
+ /**
+ * @param manifestUri The {@link Uri} of the manifest to be downloaded.
+ * @param streamKeys Keys defining which representations in the manifest should be selected for
+ * download. If empty, all representations are downloaded.
+ * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
+ * download will be written.
+ * @param executor An {@link Executor} used to make requests for the media being downloaded.
+ * Providing an {@link Executor} that uses multiple threads will speed up the download by
+ * allowing parts of it to be executed in parallel.
+ */
+ public DashDownloader(
+ Uri manifestUri,
+ List<StreamKey> streamKeys,
+ CacheDataSource.Factory cacheDataSourceFactory,
+ Executor executor) {
+ super(manifestUri, streamKeys, cacheDataSourceFactory, executor);
}
@Override
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 e9e5f30..5a5318c 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
@@ -26,6 +26,8 @@
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
@@ -35,7 +37,6 @@
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
import com.google.android.exoplayer2.testutil.MediaPeriodAsserts;
-import com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
@@ -43,6 +44,7 @@
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.LooperMode;
@@ -53,7 +55,7 @@
public final class DashMediaPeriodTest {
@Test
- public void getSteamKeys_isCompatibleWithDashManifestFilter() {
+ public void getStreamKeys_isCompatibleWithDashManifestFilter() {
// Test manifest which covers various edge cases:
// - Multiple periods.
// - Single and multiple representations per adaptation set.
@@ -61,83 +63,220 @@
// - Embedded track groups.
// All cases are deliberately combined in one test to catch potential indexing problems which
// only occur in combination.
- DashManifest testManifest =
+ DashManifest manifest =
createDashManifest(
createPeriod(
createAdaptationSet(
/* id= */ 0,
- /* trackType= */ C.TRACK_TYPE_VIDEO,
+ C.TRACK_TYPE_VIDEO,
/* descriptor= */ null,
createVideoRepresentation(/* bitrate= */ 1000000))),
createPeriod(
createAdaptationSet(
/* id= */ 100,
- /* trackType= */ C.TRACK_TYPE_VIDEO,
- /* descriptor= */ createSwitchDescriptor(/* ids...= */ 103, 104),
+ C.TRACK_TYPE_VIDEO,
+ createSwitchDescriptor(/* ids...= */ 103, 104),
createVideoRepresentationWithInbandEventStream(/* bitrate= */ 200000),
createVideoRepresentationWithInbandEventStream(/* bitrate= */ 400000),
createVideoRepresentationWithInbandEventStream(/* bitrate= */ 600000)),
createAdaptationSet(
/* id= */ 101,
- /* trackType= */ C.TRACK_TYPE_AUDIO,
- /* descriptor= */ createSwitchDescriptor(/* ids...= */ 102),
+ C.TRACK_TYPE_AUDIO,
+ createSwitchDescriptor(/* ids...= */ 102),
createAudioRepresentation(/* bitrate= */ 48000),
createAudioRepresentation(/* bitrate= */ 96000)),
createAdaptationSet(
/* id= */ 102,
- /* trackType= */ C.TRACK_TYPE_AUDIO,
- /* descriptor= */ createSwitchDescriptor(/* ids...= */ 101),
+ C.TRACK_TYPE_AUDIO,
+ createSwitchDescriptor(/* ids...= */ 101),
createAudioRepresentation(/* bitrate= */ 256000)),
createAdaptationSet(
/* id= */ 103,
- /* trackType= */ C.TRACK_TYPE_VIDEO,
- /* descriptor= */ createSwitchDescriptor(/* ids...= */ 100, 104),
+ C.TRACK_TYPE_VIDEO,
+ createSwitchDescriptor(/* ids...= */ 100, 104),
createVideoRepresentationWithInbandEventStream(/* bitrate= */ 800000),
createVideoRepresentationWithInbandEventStream(/* bitrate= */ 1000000)),
createAdaptationSet(
/* id= */ 104,
- /* trackType= */ C.TRACK_TYPE_VIDEO,
- /* descriptor= */ createSwitchDescriptor(/* ids...= */ 100, 103),
+ C.TRACK_TYPE_VIDEO,
+ createSwitchDescriptor(/* ids...= */ 100, 103),
createVideoRepresentationWithInbandEventStream(/* bitrate= */ 2000000)),
createAdaptationSet(
/* id= */ 105,
- /* trackType= */ C.TRACK_TYPE_TEXT,
+ C.TRACK_TYPE_TEXT,
/* descriptor= */ null,
createTextRepresentation(/* language= */ "eng")),
createAdaptationSet(
/* id= */ 105,
- /* trackType= */ C.TRACK_TYPE_TEXT,
+ C.TRACK_TYPE_TEXT,
/* descriptor= */ null,
createTextRepresentation(/* language= */ "ger"))));
- FilterableManifestMediaPeriodFactory<DashManifest> mediaPeriodFactory =
- (manifest, periodIndex) ->
- new DashMediaPeriod(
- /* id= */ periodIndex,
- manifest,
- periodIndex,
- mock(DashChunkSource.Factory.class),
- mock(TransferListener.class),
- DrmSessionManager.getDummyDrmSessionManager(),
- mock(LoadErrorHandlingPolicy.class),
- new EventDispatcher()
- .withParameters(
- /* windowIndex= */ 0,
- /* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()),
- /* mediaTimeOffsetMs= */ 0),
- /* elapsedRealtimeOffsetMs= */ 0,
- mock(LoaderErrorThrower.class),
- mock(Allocator.class),
- mock(CompositeSequenceableLoaderFactory.class),
- mock(PlayerEmsgCallback.class));
// Ignore embedded metadata as we don't want to select primary group just to get embedded track.
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
- mediaPeriodFactory,
- testManifest,
+ DashMediaPeriodTest::createDashMediaPeriod,
+ manifest,
/* periodIndex= */ 1,
/* ignoredMimeType= */ "application/x-emsg");
}
+ @Test
+ public void adaptationSetSwitchingProperty_mergesTrackGroups() {
+ DashManifest manifest =
+ createDashManifest(
+ createPeriod(
+ createAdaptationSet(
+ /* id= */ 0,
+ C.TRACK_TYPE_VIDEO,
+ createSwitchDescriptor(/* ids...= */ 1, 2),
+ createVideoRepresentation(/* bitrate= */ 0),
+ createVideoRepresentation(/* bitrate= */ 1)),
+ createAdaptationSet(
+ /* id= */ 3,
+ C.TRACK_TYPE_VIDEO,
+ /* descriptor= */ null,
+ createVideoRepresentation(/* bitrate= */ 300)),
+ createAdaptationSet(
+ /* id= */ 2,
+ C.TRACK_TYPE_VIDEO,
+ createSwitchDescriptor(/* ids...= */ 0, 1),
+ createVideoRepresentation(/* bitrate= */ 200),
+ createVideoRepresentation(/* bitrate= */ 201)),
+ createAdaptationSet(
+ /* id= */ 1,
+ C.TRACK_TYPE_VIDEO,
+ createSwitchDescriptor(/* ids...= */ 0, 2),
+ createVideoRepresentation(/* bitrate= */ 100))));
+ DashMediaPeriod dashMediaPeriod = createDashMediaPeriod(manifest, 0);
+ List<AdaptationSet> adaptationSets = manifest.getPeriod(0).adaptationSets;
+
+ // We expect the three adaptation sets with the switch descriptor to be merged, retaining the
+ // representations in their original order.
+ TrackGroupArray expectedTrackGroups =
+ new TrackGroupArray(
+ new TrackGroup(
+ adaptationSets.get(0).representations.get(0).format,
+ adaptationSets.get(0).representations.get(1).format,
+ adaptationSets.get(2).representations.get(0).format,
+ adaptationSets.get(2).representations.get(1).format,
+ adaptationSets.get(3).representations.get(0).format),
+ new TrackGroup(adaptationSets.get(1).representations.get(0).format));
+
+ MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups);
+ }
+
+ @Test
+ public void trickPlayProperty_mergesTrackGroups() {
+ DashManifest manifest =
+ createDashManifest(
+ createPeriod(
+ createAdaptationSet(
+ /* id= */ 0,
+ C.TRACK_TYPE_VIDEO,
+ createTrickPlayDescriptor(/* mainAdaptationSetId= */ 1),
+ createVideoRepresentation(/* bitrate= */ 0),
+ createVideoRepresentation(/* bitrate= */ 1)),
+ createAdaptationSet(
+ /* id= */ 1,
+ C.TRACK_TYPE_VIDEO,
+ /* descriptor= */ null,
+ createVideoRepresentation(/* bitrate= */ 100)),
+ createAdaptationSet(
+ /* id= */ 2,
+ C.TRACK_TYPE_VIDEO,
+ /* descriptor= */ null,
+ createVideoRepresentation(/* bitrate= */ 200),
+ createVideoRepresentation(/* bitrate= */ 201)),
+ createAdaptationSet(
+ /* id= */ 3,
+ C.TRACK_TYPE_VIDEO,
+ createTrickPlayDescriptor(/* mainAdaptationSetId= */ 2),
+ createVideoRepresentation(/* bitrate= */ 300))));
+ DashMediaPeriod dashMediaPeriod = createDashMediaPeriod(manifest, 0);
+ List<AdaptationSet> adaptationSets = manifest.getPeriod(0).adaptationSets;
+
+ // We expect the trick play adaptation sets to be merged with the ones to which they refer,
+ // retaining representations in their original order.
+ TrackGroupArray expectedTrackGroups =
+ new TrackGroupArray(
+ new TrackGroup(
+ adaptationSets.get(0).representations.get(0).format,
+ adaptationSets.get(0).representations.get(1).format,
+ adaptationSets.get(1).representations.get(0).format),
+ new TrackGroup(
+ adaptationSets.get(2).representations.get(0).format,
+ adaptationSets.get(2).representations.get(1).format,
+ adaptationSets.get(3).representations.get(0).format));
+
+ MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups);
+ }
+
+ @Test
+ public void adaptationSetSwitchingProperty_andTrickPlayProperty_mergesTrackGroups() {
+ DashManifest manifest =
+ createDashManifest(
+ createPeriod(
+ createAdaptationSet(
+ /* id= */ 0,
+ C.TRACK_TYPE_VIDEO,
+ createTrickPlayDescriptor(/* mainAdaptationSetId= */ 1),
+ createVideoRepresentation(/* bitrate= */ 0),
+ createVideoRepresentation(/* bitrate= */ 1)),
+ createAdaptationSet(
+ /* id= */ 1,
+ C.TRACK_TYPE_VIDEO,
+ createSwitchDescriptor(/* ids...= */ 2),
+ createVideoRepresentation(/* bitrate= */ 100)),
+ createAdaptationSet(
+ /* id= */ 2,
+ C.TRACK_TYPE_VIDEO,
+ createSwitchDescriptor(/* ids...= */ 1),
+ createVideoRepresentation(/* bitrate= */ 200),
+ createVideoRepresentation(/* bitrate= */ 201)),
+ createAdaptationSet(
+ /* id= */ 3,
+ C.TRACK_TYPE_VIDEO,
+ createTrickPlayDescriptor(/* mainAdaptationSetId= */ 2),
+ createVideoRepresentation(/* bitrate= */ 300))));
+ DashMediaPeriod dashMediaPeriod = createDashMediaPeriod(manifest, 0);
+ List<AdaptationSet> adaptationSets = manifest.getPeriod(0).adaptationSets;
+
+ // We expect all adaptation sets to be merged into one group, retaining representations in their
+ // original order.
+ TrackGroupArray expectedTrackGroups =
+ new TrackGroupArray(
+ new TrackGroup(
+ adaptationSets.get(0).representations.get(0).format,
+ adaptationSets.get(0).representations.get(1).format,
+ adaptationSets.get(1).representations.get(0).format,
+ adaptationSets.get(2).representations.get(0).format,
+ adaptationSets.get(2).representations.get(1).format,
+ adaptationSets.get(3).representations.get(0).format));
+
+ MediaPeriodAsserts.assertTrackGroups(dashMediaPeriod, expectedTrackGroups);
+ }
+
+ private static DashMediaPeriod createDashMediaPeriod(DashManifest manifest, int periodIndex) {
+ return new DashMediaPeriod(
+ /* id= */ periodIndex,
+ manifest,
+ periodIndex,
+ mock(DashChunkSource.Factory.class),
+ mock(TransferListener.class),
+ DrmSessionManager.getDummyDrmSessionManager(),
+ mock(LoadErrorHandlingPolicy.class),
+ new EventDispatcher()
+ .withParameters(
+ /* windowIndex= */ 0,
+ /* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()),
+ /* mediaTimeOffsetMs= */ 0),
+ /* elapsedRealtimeOffsetMs= */ 0,
+ mock(LoaderErrorThrower.class),
+ mock(Allocator.class),
+ mock(CompositeSequenceableLoaderFactory.class),
+ mock(PlayerEmsgCallback.class));
+ }
+
private static DashManifest createDashManifest(Period... periods) {
return new DashManifest(
/* availabilityStartTimeMs= */ 0,
@@ -228,6 +367,13 @@
/* id= */ null);
}
+ private static Descriptor createTrickPlayDescriptor(int mainAdaptationSetId) {
+ return new Descriptor(
+ /* schemeIdUri= */ "http://dashif.org/guidelines/trickmode",
+ /* value= */ Integer.toString(mainAdaptationSetId),
+ /* id= */ null);
+ }
+
private static Descriptor getInbandEventDescriptor() {
return new Descriptor(
/* schemeIdUri= */ "inBandSchemeIdUri", /* value= */ "inBandValue", /* id= */ "inBandId");
diff --git a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java
index fc99e20..49e111b 100644
--- a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java
+++ b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java
@@ -32,17 +32,16 @@
import com.google.android.exoplayer2.offline.DownloadException;
import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.Downloader;
-import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.DownloaderFactory;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource;
-import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util;
@@ -81,9 +80,11 @@
@Test
public void createWithDefaultDownloaderFactory() {
- DownloaderConstructorHelper constructorHelper =
- new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
- DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
+ CacheDataSource.Factory cacheDataSourceFactory =
+ new CacheDataSource.Factory()
+ .setCache(Mockito.mock(Cache.class))
+ .setUpstreamDataSourceFactory(DummyDataSource.FACTORY);
+ DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory);
Downloader downloader =
factory.createDownloader(
@@ -184,7 +185,7 @@
.setRandomData("text_segment_2", 2)
.setRandomData("text_segment_3", 3);
FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet);
- Factory factory = mock(Factory.class);
+ FakeDataSource.Factory factory = mock(FakeDataSource.Factory.class);
when(factory.createDataSource()).thenReturn(fakeDataSource);
DashDownloader dashDownloader =
@@ -216,7 +217,7 @@
.setRandomData("period_2_segment_2", 2)
.setRandomData("period_2_segment_3", 3);
FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet);
- Factory factory = mock(Factory.class);
+ FakeDataSource.Factory factory = mock(FakeDataSource.Factory.class);
when(factory.createDataSource()).thenReturn(fakeDataSource);
DashDownloader dashDownloader =
@@ -327,12 +328,16 @@
}
private DashDownloader getDashDownloader(FakeDataSet fakeDataSet, StreamKey... keys) {
- return getDashDownloader(new Factory().setFakeDataSet(fakeDataSet), keys);
+ return getDashDownloader(new FakeDataSource.Factory().setFakeDataSet(fakeDataSet), keys);
}
- private DashDownloader getDashDownloader(Factory factory, StreamKey... keys) {
- return new DashDownloader(
- TEST_MPD_URI, keysList(keys), new DownloaderConstructorHelper(cache, factory));
+ private DashDownloader getDashDownloader(
+ FakeDataSource.Factory upstreamDataSourceFactory, StreamKey... keys) {
+ CacheDataSource.Factory cacheDataSourceFactory =
+ new CacheDataSource.Factory()
+ .setCache(cache)
+ .setUpstreamDataSourceFactory(upstreamDataSourceFactory);
+ return new DashDownloader(TEST_MPD_URI, keysList(keys), cacheDataSourceFactory);
}
private static ArrayList<StreamKey> keysList(StreamKey... keys) {
diff --git a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java
index 89426f7..164299f 100644
--- a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java
+++ b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java
@@ -29,7 +29,6 @@
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadRequest;
-import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.scheduler.Requirements;
import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet;
@@ -40,6 +39,7 @@
import com.google.android.exoplayer2.testutil.TestDownloadManagerListener;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSource.Factory;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util;
@@ -253,12 +253,14 @@
runOnMainThread(
() -> {
Factory fakeDataSourceFactory = new FakeDataSource.Factory().setFakeDataSet(fakeDataSet);
+ DefaultDownloaderFactory downloaderFactory =
+ new DefaultDownloaderFactory(
+ new CacheDataSource.Factory()
+ .setCache(cache)
+ .setUpstreamDataSourceFactory(fakeDataSourceFactory));
downloadManager =
new DownloadManager(
- ApplicationProvider.getApplicationContext(),
- downloadIndex,
- new DefaultDownloaderFactory(
- new DownloaderConstructorHelper(cache, fakeDataSourceFactory)));
+ ApplicationProvider.getApplicationContext(), downloadIndex, downloaderFactory);
downloadManager.setRequirements(new Requirements(0));
downloadManagerListener =
diff --git a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java
index 0bf5089..42b0a2e 100644
--- a/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java
+++ b/tree/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java
@@ -33,7 +33,6 @@
import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.DownloadService;
-import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.scheduler.Scheduler;
import com.google.android.exoplayer2.testutil.DummyMainThread;
@@ -42,6 +41,7 @@
import com.google.android.exoplayer2.testutil.TestDownloadManagerListener;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.ConditionVariable;
@@ -113,12 +113,14 @@
() -> {
DefaultDownloadIndex downloadIndex =
new DefaultDownloadIndex(TestUtil.getInMemoryDatabaseProvider());
+ DefaultDownloaderFactory downloaderFactory =
+ new DefaultDownloaderFactory(
+ new CacheDataSource.Factory()
+ .setCache(cache)
+ .setUpstreamDataSourceFactory(fakeDataSourceFactory));
final DownloadManager dashDownloadManager =
new DownloadManager(
- ApplicationProvider.getApplicationContext(),
- downloadIndex,
- new DefaultDownloaderFactory(
- new DownloaderConstructorHelper(cache, fakeDataSourceFactory)));
+ ApplicationProvider.getApplicationContext(), downloadIndex, downloaderFactory);
downloadManagerListener =
new TestDownloadManagerListener(dashDownloadManager, dummyMainThread);
dashDownloadManager.resumeDownloads();
diff --git a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
index 7ef308e..4700bbb 100644
--- a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
+++ b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
@@ -43,7 +43,9 @@
}
@Override
- public int sampleData(DataReader input, int length, boolean allowEndOfInput) throws IOException {
+ public int sampleData(
+ DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart)
+ throws IOException {
int bytesToSkipByReading = Math.min(readBuffer.length, length);
int bytesSkipped = input.read(readBuffer, /* offset= */ 0, bytesToSkipByReading);
if (bytesSkipped == C.RESULT_END_OF_INPUT) {
@@ -56,7 +58,7 @@
}
@Override
- public void sampleData(ParsableByteArray data, int length) {
+ public void sampleData(ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) {
data.skipBytes(length);
}
diff --git a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java
index 3e95fab..f1a9ac7 100644
--- a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java
+++ b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/TrackOutput.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor;
+import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@@ -22,6 +23,9 @@
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.EOFException;
import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
/**
@@ -94,6 +98,41 @@
}
+ /** Defines the part of the sample data to which a call to {@link #sampleData} corresponds. */
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SAMPLE_DATA_PART_MAIN, SAMPLE_DATA_PART_ENCRYPTION, SAMPLE_DATA_PART_SUPPLEMENTAL})
+ @interface SampleDataPart {}
+
+ /** Main media sample data. */
+ int SAMPLE_DATA_PART_MAIN = 0;
+ /**
+ * Sample encryption data.
+ *
+ * <p>The format for encryption information is:
+ *
+ * <ul>
+ * <li>(1 byte) {@code encryption_signal_byte}: Most significant bit signals whether the
+ * encryption data contains subsample encryption data. The remaining bits contain {@code
+ * initialization_vector_size}.
+ * <li>({@code initialization_vector_size} bytes) Initialization vector.
+ * <li>If subsample encryption data is present, as per {@code encryption_signal_byte}, the
+ * encryption data also contains:
+ * <ul>
+ * <li>(2 bytes) {@code subsample_encryption_data_length}.
+ * <li>({@code subsample_encryption_data_length} bytes) Subsample encryption data
+ * (repeated {@code subsample_encryption_data_length / 6} times:
+ * <ul>
+ * <li>(3 bytes) Size of a clear section in sample.
+ * <li>(3 bytes) Size of an encryption section in sample.
+ * </ul>
+ * </ul>
+ * </ul>
+ */
+ int SAMPLE_DATA_PART_ENCRYPTION = 1;
+ /** Sample supplemental data. */
+ int SAMPLE_DATA_PART_SUPPLEMENTAL = 2;
+
/**
* Called when the {@link Format} of the track has been extracted from the stream.
*
@@ -102,6 +141,22 @@
void format(Format format);
/**
+ * Equivalent to {@link #sampleData(DataReader, int, boolean, int) sampleData(input, length,
+ * allowEndOfInput, SAMPLE_DATA_PART_MAIN)}.
+ */
+ default int sampleData(DataReader input, int length, boolean allowEndOfInput) throws IOException {
+ return sampleData(input, length, allowEndOfInput, SAMPLE_DATA_PART_MAIN);
+ }
+
+ /**
+ * Equivalent to {@link #sampleData(ParsableByteArray, int, int)} sampleData(data, length,
+ * SAMPLE_DATA_PART_MAIN)}.
+ */
+ default void sampleData(ParsableByteArray data, int length) {
+ sampleData(data, length, SAMPLE_DATA_PART_MAIN);
+ }
+
+ /**
* Called to write sample data to the output.
*
* @param input A {@link DataReader} from which to read the sample data.
@@ -109,18 +164,22 @@
* @param allowEndOfInput True if encountering the end of the input having read no data is
* allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it
* should be considered an error, causing an {@link EOFException} to be thrown.
+ * @param sampleDataPart The part of the sample data to which this call corresponds.
* @return The number of bytes appended.
* @throws IOException If an error occurred reading from the input.
*/
- int sampleData(DataReader input, int length, boolean allowEndOfInput) throws IOException;
+ int sampleData(
+ DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart)
+ throws IOException;
/**
* Called to write sample data to the output.
*
* @param data A {@link ParsableByteArray} from which to read the sample data.
* @param length The number of bytes to read, starting from {@code data.getPosition()}.
+ * @param sampleDataPart The part of the sample data to which this call corresponds.
*/
- void sampleData(ParsableByteArray data, int length);
+ void sampleData(ParsableByteArray data, int length, @SampleDataPart int sampleDataPart);
/**
* Called when metadata associated with a sample has been extracted from the stream.
diff --git a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
index 6e66049..4d24c4f 100644
--- a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
+++ b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
@@ -54,8 +54,10 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.UUID;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@@ -319,6 +321,18 @@
*/
private static final UUID WAVE_SUBFORMAT_PCM = new UUID(0x0100000000001000L, 0x800000AA00389B71L);
+ /** Some HTC devices signal rotation in track names. */
+ private static final Map<String, Integer> TRACK_NAME_TO_ROTATION_DEGREES;
+
+ static {
+ Map<String, Integer> trackNameToRotationDegrees = new HashMap<>();
+ trackNameToRotationDegrees.put("htc_video_rotA-000", 0);
+ trackNameToRotationDegrees.put("htc_video_rotA-090", 90);
+ trackNameToRotationDegrees.put("htc_video_rotA-180", 180);
+ trackNameToRotationDegrees.put("htc_video_rotA-270", 270);
+ TRACK_NAME_TO_ROTATION_DEGREES = Collections.unmodifiableMap(trackNameToRotationDegrees);
+ }
+
private final EbmlReader reader;
private final VarintReader varintReader;
private final SparseArray<Track> tracks;
@@ -1267,7 +1281,8 @@
} else {
// Append supplemental data.
int blockAdditionalSize = blockAdditionalData.limit();
- track.output.sampleData(blockAdditionalData, blockAdditionalSize);
+ track.output.sampleData(
+ blockAdditionalData, blockAdditionalSize, TrackOutput.SAMPLE_DATA_PART_SUPPLEMENTAL);
size += blockAdditionalSize;
}
}
@@ -1336,11 +1351,14 @@
// Write the signal byte, containing the IV size and the subsample encryption flag.
scratch.data[0] = (byte) (ENCRYPTION_IV_SIZE | (hasSubsampleEncryption ? 0x80 : 0x00));
scratch.setPosition(0);
- output.sampleData(scratch, 1);
+ output.sampleData(scratch, 1, TrackOutput.SAMPLE_DATA_PART_ENCRYPTION);
sampleBytesWritten++;
// Write the IV.
encryptionInitializationVector.setPosition(0);
- output.sampleData(encryptionInitializationVector, ENCRYPTION_IV_SIZE);
+ output.sampleData(
+ encryptionInitializationVector,
+ ENCRYPTION_IV_SIZE,
+ TrackOutput.SAMPLE_DATA_PART_ENCRYPTION);
sampleBytesWritten += ENCRYPTION_IV_SIZE;
}
if (hasSubsampleEncryption) {
@@ -1388,7 +1406,10 @@
encryptionSubsampleDataBuffer.putInt(0);
}
encryptionSubsampleData.reset(encryptionSubsampleDataBuffer.array(), subsampleDataSize);
- output.sampleData(encryptionSubsampleData, subsampleDataSize);
+ output.sampleData(
+ encryptionSubsampleData,
+ subsampleDataSize,
+ TrackOutput.SAMPLE_DATA_PART_ENCRYPTION);
sampleBytesWritten += subsampleDataSize;
}
}
@@ -1407,7 +1428,7 @@
scratch.data[1] = (byte) ((size >> 16) & 0xFF);
scratch.data[2] = (byte) ((size >> 8) & 0xFF);
scratch.data[3] = (byte) (size & 0xFF);
- output.sampleData(scratch, 4);
+ output.sampleData(scratch, 4, TrackOutput.SAMPLE_DATA_PART_SUPPLEMENTAL);
sampleBytesWritten += 4;
}
@@ -2088,15 +2109,9 @@
colorInfo = new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo);
}
int rotationDegrees = Format.NO_VALUE;
- // Some HTC devices signal rotation in track names.
- if ("htc_video_rotA-000".equals(name)) {
- rotationDegrees = 0;
- } else if ("htc_video_rotA-090".equals(name)) {
- rotationDegrees = 90;
- } else if ("htc_video_rotA-180".equals(name)) {
- rotationDegrees = 180;
- } else if ("htc_video_rotA-270".equals(name)) {
- rotationDegrees = 270;
+
+ if (TRACK_NAME_TO_ROTATION_DEGREES.containsKey(name)) {
+ rotationDegrees = TRACK_NAME_TO_ROTATION_DEGREES.get(name);
}
if (projectionType == C.PROJECTION_RECTANGULAR
&& Float.compare(projectionPoseYaw, 0f) == 0
@@ -2136,6 +2151,10 @@
throw new ParserException("Unexpected MIME type.");
}
+ if (!TRACK_NAME_TO_ROTATION_DEGREES.containsKey(name)) {
+ formatBuilder.setLabel(name);
+ }
+
Format format =
formatBuilder
.setId(trackId)
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 359ccc1..d20da44 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
@@ -965,20 +965,20 @@
// Offset to the entire video timeline. In the presence of B-frames this is usually used to
// ensure that the first frame's presentation timestamp is zero.
- long edtsOffset = 0;
+ long edtsOffsetUs = 0;
// Currently we only support a single edit that moves the entire media timeline (indicated by
// duration == 0). Other uses of edit lists are uncommon and unsupported.
if (track.editListDurations != null && track.editListDurations.length == 1
&& track.editListDurations[0] == 0) {
- edtsOffset =
+ edtsOffsetUs =
Util.scaleLargeTimestamp(
- track.editListMediaTimes[0], C.MILLIS_PER_SECOND, track.timescale);
+ track.editListMediaTimes[0], C.MICROS_PER_SECOND, track.timescale);
}
int[] sampleSizeTable = fragment.sampleSizeTable;
- int[] sampleCompositionTimeOffsetTable = fragment.sampleCompositionTimeOffsetTable;
- long[] sampleDecodingTimeTable = fragment.sampleDecodingTimeTable;
+ int[] sampleCompositionTimeOffsetUsTable = fragment.sampleCompositionTimeOffsetUsTable;
+ long[] sampleDecodingTimeUsTable = fragment.sampleDecodingTimeUsTable;
boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable;
boolean workaroundEveryVideoFrameIsSyncFrame = track.type == C.TRACK_TYPE_VIDEO
@@ -1002,13 +1002,13 @@
// here, because unsigned integers will still be parsed correctly (unless their top bit is
// set, which is never true in practice because sample offsets are always small).
int sampleOffset = trun.readInt();
- sampleCompositionTimeOffsetTable[i] =
- (int) ((sampleOffset * C.MILLIS_PER_SECOND) / timescale);
+ sampleCompositionTimeOffsetUsTable[i] =
+ (int) ((sampleOffset * C.MICROS_PER_SECOND) / timescale);
} else {
- sampleCompositionTimeOffsetTable[i] = 0;
+ sampleCompositionTimeOffsetUsTable[i] = 0;
}
- sampleDecodingTimeTable[i] =
- Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset;
+ sampleDecodingTimeUsTable[i] =
+ Util.scaleLargeTimestamp(cumulativeTime, C.MICROS_PER_SECOND, timescale) - edtsOffsetUs;
sampleSizeTable[i] = sampleSize;
sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0
&& (!workaroundEveryVideoFrameIsSyncFrame || i == 0);
@@ -1297,7 +1297,7 @@
Track track = currentTrackBundle.track;
TrackOutput output = currentTrackBundle.output;
int sampleIndex = currentTrackBundle.currentSampleIndex;
- long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L;
+ long sampleTimeUs = fragment.getSamplePresentationTimeUs(sampleIndex);
if (timestampAdjuster != null) {
sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
}
@@ -1545,10 +1545,9 @@
* @param timeUs The seek time, in microseconds.
*/
public void seek(long timeUs) {
- long timeMs = C.usToMs(timeUs);
int searchIndex = currentSampleIndex;
while (searchIndex < fragment.sampleCount
- && fragment.getSamplePresentationTime(searchIndex) < timeMs) {
+ && fragment.getSamplePresentationTimeUs(searchIndex) < timeUs) {
if (fragment.sampleIsSyncFrameTable[searchIndex]) {
firstSampleToOutputIndex = searchIndex;
}
@@ -1611,9 +1610,10 @@
encryptionSignalByte.data[0] =
(byte) (vectorSize | (writeSubsampleEncryptionData ? 0x80 : 0));
encryptionSignalByte.setPosition(0);
- output.sampleData(encryptionSignalByte, 1);
+ output.sampleData(encryptionSignalByte, 1, TrackOutput.SAMPLE_DATA_PART_ENCRYPTION);
// Write the vector.
- output.sampleData(initializationVectorData, vectorSize);
+ output.sampleData(
+ initializationVectorData, vectorSize, TrackOutput.SAMPLE_DATA_PART_ENCRYPTION);
if (!writeSubsampleEncryptionData) {
return 1 + vectorSize;
@@ -1635,7 +1635,10 @@
scratch.data[5] = (byte) ((sampleSize >> 16) & 0xFF);
scratch.data[6] = (byte) ((sampleSize >> 8) & 0xFF);
scratch.data[7] = (byte) (sampleSize & 0xFF);
- output.sampleData(scratch, SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH);
+ output.sampleData(
+ scratch,
+ SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH,
+ TrackOutput.SAMPLE_DATA_PART_ENCRYPTION);
return 1 + vectorSize + SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH;
}
@@ -1658,7 +1661,8 @@
subsampleEncryptionData = scratch;
}
- output.sampleData(subsampleEncryptionData, subsampleDataLength);
+ output.sampleData(
+ subsampleEncryptionData, subsampleDataLength, TrackOutput.SAMPLE_DATA_PART_ENCRYPTION);
return 1 + vectorSize + subsampleDataLength;
}
diff --git a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
index 456cd50..b214114 100644
--- a/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
+++ b/tree/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
@@ -60,14 +60,10 @@
* The size of each sample in the fragment.
*/
public int[] sampleSizeTable;
- /**
- * The composition time offset of each sample in the fragment.
- */
- public int[] sampleCompositionTimeOffsetTable;
- /**
- * The decoding time of each sample in the fragment.
- */
- public long[] sampleDecodingTimeTable;
+ /** The composition time offset of each sample in the fragment, in microseconds. */
+ public int[] sampleCompositionTimeOffsetUsTable;
+ /** The decoding time of each sample in the fragment, in microseconds. */
+ public long[] sampleDecodingTimeUsTable;
/**
* Indicates which samples are sync frames.
*/
@@ -101,8 +97,8 @@
trunDataPosition = new long[0];
trunLength = new int[0];
sampleSizeTable = new int[0];
- sampleCompositionTimeOffsetTable = new int[0];
- sampleDecodingTimeTable = new long[0];
+ sampleCompositionTimeOffsetUsTable = new int[0];
+ sampleDecodingTimeUsTable = new long[0];
sampleIsSyncFrameTable = new boolean[0];
sampleHasSubsampleEncryptionTable = new boolean[0];
sampleEncryptionData = new ParsableByteArray();
@@ -143,8 +139,8 @@
// likely. The choice of 25% is relatively arbitrary.
int tableSize = (sampleCount * 125) / 100;
sampleSizeTable = new int[tableSize];
- sampleCompositionTimeOffsetTable = new int[tableSize];
- sampleDecodingTimeTable = new long[tableSize];
+ sampleCompositionTimeOffsetUsTable = new int[tableSize];
+ sampleDecodingTimeUsTable = new long[tableSize];
sampleIsSyncFrameTable = new boolean[tableSize];
sampleHasSubsampleEncryptionTable = new boolean[tableSize];
}
@@ -186,8 +182,14 @@
sampleEncryptionDataNeedsFill = false;
}
- public long getSamplePresentationTime(int index) {
- return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index];
+ /**
+ * Returns the sample presentation timestamp in microseconds.
+ *
+ * @param index The sample index.
+ * @return The presentation timestamps of this sample in microseconds.
+ */
+ public long getSamplePresentationTimeUs(int index) {
+ return sampleDecodingTimeUsTable[index] + sampleCompositionTimeOffsetUsTable[index];
}
/** Returns whether the sample at the given index has a subsample encryption table. */
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 dcf64d9..c356b1c 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
@@ -47,6 +47,7 @@
private static final int VPS_NUT = 32;
private static final int SPS_NUT = 33;
private static final int PPS_NUT = 34;
+ private static final int AUD_NUT = 35;
private static final int PREFIX_SEI_NUT = 39;
private static final int SUFFIX_SEI_NUT = 40;
@@ -65,7 +66,7 @@
private final NalUnitTargetBuffer sps;
private final NalUnitTargetBuffer pps;
private final NalUnitTargetBuffer prefixSei;
- private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed?
+ private final NalUnitTargetBuffer suffixSei;
private long totalBytesWritten;
// Per packet state that gets reset at the start of each packet.
@@ -424,17 +425,17 @@
private final TrackOutput output;
// Per NAL unit state. A sample consists of one or more NAL units.
- private long nalUnitStartPosition;
+ private long nalUnitPosition;
private boolean nalUnitHasKeyframeData;
private int nalUnitBytesRead;
private long nalUnitTimeUs;
private boolean lookingForFirstSliceFlag;
private boolean isFirstSlice;
- private boolean isFirstParameterSet;
+ private boolean isFirstPrefixNalUnit;
// Per sample state that gets reset at the start of each sample.
private boolean readingSample;
- private boolean writingParameterSets;
+ private boolean readingPrefix;
private long samplePosition;
private long sampleTimeUs;
private boolean sampleIsKeyframe;
@@ -446,35 +447,33 @@
public void reset() {
lookingForFirstSliceFlag = false;
isFirstSlice = false;
- isFirstParameterSet = false;
+ isFirstPrefixNalUnit = false;
readingSample = false;
- writingParameterSets = false;
+ readingPrefix = false;
}
public void startNalUnit(
long position, int offset, int nalUnitType, long pesTimeUs, boolean hasOutputFormat) {
isFirstSlice = false;
- isFirstParameterSet = false;
+ isFirstPrefixNalUnit = false;
nalUnitTimeUs = pesTimeUs;
nalUnitBytesRead = 0;
- nalUnitStartPosition = position;
+ nalUnitPosition = position;
- if (nalUnitType >= VPS_NUT) {
- if (!writingParameterSets && readingSample) {
- // This is a non-VCL NAL unit, so flush the previous sample.
+ if (!isVclBodyNalUnit(nalUnitType)) {
+ if (readingSample && !readingPrefix) {
if (hasOutputFormat) {
outputSample(offset);
}
readingSample = false;
}
- if (nalUnitType <= PPS_NUT) {
- // This sample will have parameter sets at the start.
- isFirstParameterSet = !writingParameterSets;
- writingParameterSets = true;
+ if (isPrefixNalUnit(nalUnitType)) {
+ isFirstPrefixNalUnit = !readingPrefix;
+ readingPrefix = true;
}
}
- // Look for the flag if this NAL unit contains a slice_segment_layer_rbsp.
+ // Look for the first slice flag if this NAL unit contains a slice_segment_layer_rbsp.
nalUnitHasKeyframeData = (nalUnitType >= BLA_W_LP && nalUnitType <= CRA_NUT);
lookingForFirstSliceFlag = nalUnitHasKeyframeData || nalUnitType <= RASL_R;
}
@@ -492,30 +491,38 @@
}
public void endNalUnit(long position, int offset, boolean hasOutputFormat) {
- if (writingParameterSets && isFirstSlice) {
+ if (readingPrefix && isFirstSlice) {
// This sample has parameter sets. Reset the key-frame flag based on the first slice.
sampleIsKeyframe = nalUnitHasKeyframeData;
- writingParameterSets = false;
- } else if (isFirstParameterSet || isFirstSlice) {
+ readingPrefix = false;
+ } else if (isFirstPrefixNalUnit || isFirstSlice) {
// This NAL unit is at the start of a new sample (access unit).
if (hasOutputFormat && readingSample) {
// Output the sample ending before this NAL unit.
- int nalUnitLength = (int) (position - nalUnitStartPosition);
+ int nalUnitLength = (int) (position - nalUnitPosition);
outputSample(offset + nalUnitLength);
}
- samplePosition = nalUnitStartPosition;
+ samplePosition = nalUnitPosition;
sampleTimeUs = nalUnitTimeUs;
- readingSample = true;
sampleIsKeyframe = nalUnitHasKeyframeData;
+ readingSample = true;
}
}
private void outputSample(int offset) {
@C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
- int size = (int) (nalUnitStartPosition - samplePosition);
+ int size = (int) (nalUnitPosition - samplePosition);
output.sampleMetadata(sampleTimeUs, flags, size, offset, null);
}
- }
+ /** Returns whether a NAL unit type is one that occurs before any VCL NAL units in a sample. */
+ private static boolean isPrefixNalUnit(int nalUnitType) {
+ return (VPS_NUT <= nalUnitType && nalUnitType <= AUD_NUT) || nalUnitType == PREFIX_SEI_NUT;
+ }
+ /** Returns whether a NAL unit type is one that occurs in the VLC body of a sample. */
+ private static boolean isVclBodyNalUnit(int nalUnitType) {
+ return nalUnitType < VPS_NUT || nalUnitType == SUFFIX_SEI_NUT;
+ }
+ }
}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorNonParameterizedTest.java
similarity index 85%
rename from tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java
rename to tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorNonParameterizedTest.java
index 03b6fcb..65b8122 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorNonParameterizedTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright (C) 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.
@@ -12,6 +12,7 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
+ *
*/
package com.google.android.exoplayer2.extractor.amr;
@@ -35,9 +36,14 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Unit test for {@link AmrExtractor}. */
+/**
+ * Tests for {@link AmrExtractor} that test specific behaviours and don't need to be parameterized.
+ *
+ * <p>For parameterized tests using {@link ExtractorAsserts} see {@link
+ * AmrExtractorParameterizedTest}.
+ */
@RunWith(AndroidJUnit4.class)
-public final class AmrExtractorTest {
+public final class AmrExtractorNonParameterizedTest {
private static final Random RANDOM = new Random(1234);
@@ -169,30 +175,6 @@
}
}
- @Test
- public void extractingNarrowBandSamples() throws Exception {
- ExtractorAsserts.assertBehavior(
- createAmrExtractorFactory(/* withSeeking= */ false), "amr/sample_nb.amr");
- }
-
- @Test
- public void extractingWideBandSamples() throws Exception {
- ExtractorAsserts.assertBehavior(
- createAmrExtractorFactory(/* withSeeking= */ false), "amr/sample_wb.amr");
- }
-
- @Test
- public void extractingNarrowBandSamples_withSeeking() throws Exception {
- ExtractorAsserts.assertBehavior(
- createAmrExtractorFactory(/* withSeeking= */ true), "amr/sample_nb_cbr.amr");
- }
-
- @Test
- public void extractingWideBandSamples_withSeeking() throws Exception {
- ExtractorAsserts.assertBehavior(
- createAmrExtractorFactory(/* withSeeking= */ true), "amr/sample_wb_cbr.amr");
- }
-
private byte[] newWideBandAmrFrameWithType(int frameType) {
byte frameHeader = (byte) ((frameType << 3) & (0b01111100));
int frameContentInBytes = frameSizeBytesByTypeWb(frameType) - 1;
@@ -237,14 +219,4 @@
private static FakeExtractorInput fakeExtractorInputWithData(byte[] data) {
return new FakeExtractorInput.Builder().setData(data).build();
}
-
- private static ExtractorAsserts.ExtractorFactory createAmrExtractorFactory(boolean withSeeking) {
- return () -> {
- if (!withSeeking) {
- return new AmrExtractor();
- } else {
- return new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
- }
- };
- }
}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorParameterizedTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorParameterizedTest.java
new file mode 100644
index 0000000..833567c
--- /dev/null
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorParameterizedTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.amr;
+
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
+
+/**
+ * Unit tests for {@link AmrExtractor} that use parameterization to test a range of behaviours.
+ *
+ * <p>For non-parameterized tests see {@link AmrExtractorSeekTest} and {@link
+ * AmrExtractorNonParameterizedTest}.
+ */
+@RunWith(ParameterizedRobolectricTestRunner.class)
+public final class AmrExtractorParameterizedTest {
+
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
+ @Test
+ public void extractingNarrowBandSamples() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ createAmrExtractorFactory(/* withSeeking= */ false), "amr/sample_nb.amr", assertionConfig);
+ }
+
+ @Test
+ public void extractingWideBandSamples() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ createAmrExtractorFactory(/* withSeeking= */ false), "amr/sample_wb.amr", assertionConfig);
+ }
+
+ @Test
+ public void extractingNarrowBandSamples_withSeeking() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ createAmrExtractorFactory(/* withSeeking= */ true),
+ "amr/sample_nb_cbr.amr",
+ assertionConfig);
+ }
+
+ @Test
+ public void extractingWideBandSamples_withSeeking() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ createAmrExtractorFactory(/* withSeeking= */ true),
+ "amr/sample_wb_cbr.amr",
+ assertionConfig);
+ }
+
+
+ private static ExtractorAsserts.ExtractorFactory createAmrExtractorFactory(boolean withSeeking) {
+ return () -> {
+ if (!withSeeking) {
+ return new AmrExtractor();
+ } else {
+ return new AmrExtractor(AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING);
+ }
+ };
+ }
+}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java
index 850321e..42e9f93 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java
@@ -33,7 +33,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Unit test for {@link AmrExtractor}. */
+/** Unit tests for {@link AmrExtractor} seeking behaviour. */
@RunWith(AndroidJUnit4.class)
public final class AmrExtractorSeekTest {
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java
index fab950a..a620ee2 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorTest.java
@@ -15,22 +15,32 @@
*/
package com.google.android.exoplayer2.extractor.flac;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Unit tests for {@link FlacExtractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public class FlacExtractorTest {
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void sample() throws Exception {
ExtractorAsserts.assertBehavior(
FlacExtractor::new,
/* file= */ "flac/bear.flac",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "flac/bear_flac");
}
@@ -39,7 +49,7 @@
ExtractorAsserts.assertBehavior(
FlacExtractor::new,
/* file= */ "flac/bear_with_id3.flac",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "flac/bear_with_id3_enabled_flac");
}
@@ -48,7 +58,7 @@
ExtractorAsserts.assertBehavior(
() -> new FlacExtractor(FlacExtractor.FLAG_DISABLE_ID3_METADATA),
/* file= */ "flac/bear_with_id3.flac",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "flac/bear_with_id3_disabled_flac");
}
@@ -57,7 +67,7 @@
ExtractorAsserts.assertBehavior(
FlacExtractor::new,
/* file= */ "flac/bear_no_seek_table_no_num_samples.flac",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "flac/bear_no_seek_table_no_num_samples_flac");
}
@@ -66,7 +76,7 @@
ExtractorAsserts.assertBehavior(
FlacExtractor::new,
/* file= */ "flac/bear_with_vorbis_comments.flac",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "flac/bear_with_vorbis_comments_flac");
}
@@ -75,7 +85,7 @@
ExtractorAsserts.assertBehavior(
FlacExtractor::new,
/* file= */ "flac/bear_with_picture.flac",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "flac/bear_with_picture_flac");
}
@@ -84,7 +94,7 @@
ExtractorAsserts.assertBehavior(
FlacExtractor::new,
/* file= */ "flac/bear_one_metadata_block.flac",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "flac/bear_one_metadata_block_flac");
}
@@ -93,7 +103,7 @@
ExtractorAsserts.assertBehavior(
FlacExtractor::new,
/* file= */ "flac/bear_no_min_max_frame_size.flac",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "flac/bear_no_min_max_frame_size_flac");
}
@@ -102,7 +112,7 @@
ExtractorAsserts.assertBehavior(
FlacExtractor::new,
/* file= */ "flac/bear_no_num_samples.flac",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "flac/bear_no_num_samples_flac");
}
@@ -111,7 +121,7 @@
ExtractorAsserts.assertBehavior(
FlacExtractor::new,
/* file= */ "flac/bear_uncommon_sample_rate.flac",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "flac/bear_uncommon_sample_rate_flac");
}
}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java
index 52b6a04..6481658 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java
@@ -15,17 +15,28 @@
*/
package com.google.android.exoplayer2.extractor.flv;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Unit test for {@link FlvExtractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public final class FlvExtractorTest {
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void sample() throws Exception {
- ExtractorAsserts.assertBehavior(FlvExtractor::new, "flv/sample.flv");
+ ExtractorAsserts.assertBehavior(FlvExtractor::new, "flv/sample.flv", assertionConfig);
}
}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java
index 761815c..70c0ec4 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java
@@ -15,33 +15,57 @@
*/
package com.google.android.exoplayer2.extractor.mkv;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Tests for {@link MatroskaExtractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public final class MatroskaExtractorTest {
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void mkvSample() throws Exception {
- ExtractorAsserts.assertBehavior(MatroskaExtractor::new, "mkv/sample.mkv");
+ ExtractorAsserts.assertBehavior(MatroskaExtractor::new, "mkv/sample.mkv", assertionConfig);
+ }
+
+ @Test
+ public void mkvSample_withSubripSubtitles() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ MatroskaExtractor::new, "mkv/sample_with_srt.mkv", assertionConfig);
+ }
+
+ @Test
+ public void mkvSample_withHtcRotationInfoInTrackName() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ MatroskaExtractor::new, "mkv/sample_with_htc_rotation_track_name.mkv", assertionConfig);
}
@Test
public void mkvFullBlocksSample() throws Exception {
- ExtractorAsserts.assertBehavior(MatroskaExtractor::new, "mkv/full_blocks.mkv");
+ ExtractorAsserts.assertBehavior(MatroskaExtractor::new, "mkv/full_blocks.mkv", assertionConfig);
}
@Test
public void webmSubsampleEncryption() throws Exception {
ExtractorAsserts.assertBehavior(
- MatroskaExtractor::new, "mkv/subsample_encrypted_noaltref.webm");
+ MatroskaExtractor::new, "mkv/subsample_encrypted_noaltref.webm", assertionConfig);
}
@Test
public void webmSubsampleEncryptionWithAltrefFrames() throws Exception {
- ExtractorAsserts.assertBehavior(MatroskaExtractor::new, "mkv/subsample_encrypted_altref.webm");
+ ExtractorAsserts.assertBehavior(
+ MatroskaExtractor::new, "mkv/subsample_encrypted_altref.webm", assertionConfig);
}
}
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 5622137..611c8f7 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,37 +15,49 @@
*/
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 java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Unit test for {@link Mp3Extractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public final class Mp3ExtractorTest {
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void mp3SampleWithXingHeader() throws Exception {
- ExtractorAsserts.assertBehavior(Mp3Extractor::new, "mp3/bear-vbr-xing-header.mp3");
+ ExtractorAsserts.assertBehavior(
+ Mp3Extractor::new, "mp3/bear-vbr-xing-header.mp3", assertionConfig);
}
@Test
public void mp3SampleWithCbrSeeker() throws Exception {
ExtractorAsserts.assertBehavior(
- Mp3Extractor::new, "mp3/bear-cbr-variable-frame-size-no-seek-table.mp3");
+ Mp3Extractor::new, "mp3/bear-cbr-variable-frame-size-no-seek-table.mp3", assertionConfig);
}
@Test
public void mp3SampleWithIndexSeeker() throws Exception {
ExtractorAsserts.assertBehavior(
() -> new Mp3Extractor(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING),
- "mp3/bear-vbr-no-seek-table.mp3");
+ "mp3/bear-vbr-no-seek-table.mp3",
+ assertionConfig);
}
@Test
public void trimmedMp3Sample() throws Exception {
- ExtractorAsserts.assertBehavior(Mp3Extractor::new, "mp3/play-trimmed.mp3");
+ ExtractorAsserts.assertBehavior(Mp3Extractor::new, "mp3/play-trimmed.mp3", assertionConfig);
}
@Test
@@ -53,7 +65,7 @@
ExtractorAsserts.assertBehavior(
Mp3Extractor::new,
/* file= */ "mp3/bear-id3.mp3",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "mp3/bear-id3-enabled");
}
@@ -62,7 +74,7 @@
ExtractorAsserts.assertBehavior(
() -> new Mp3Extractor(Mp3Extractor.FLAG_DISABLE_ID3_METADATA),
/* file= */ "mp3/bear-id3.mp3",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "mp3/bear-id3-disabled");
}
}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java
index 86f8e84..d983caa 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java
@@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2.extractor.mp4;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
@@ -25,21 +24,34 @@
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Unit test for {@link FragmentedMp4Extractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public final class FragmentedMp4ExtractorTest {
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void sample() throws Exception {
ExtractorAsserts.assertBehavior(
- getExtractorFactory(ImmutableList.of()), "mp4/sample_fragmented.mp4");
+ getExtractorFactory(ImmutableList.of()), "mp4/sample_fragmented.mp4", assertionConfig);
}
@Test
public void sampleSeekable() throws Exception {
ExtractorAsserts.assertBehavior(
- getExtractorFactory(ImmutableList.of()), "mp4/sample_fragmented_seekable.mp4");
+ getExtractorFactory(ImmutableList.of()),
+ "mp4/sample_fragmented_seekable.mp4",
+ assertionConfig);
}
@Test
@@ -49,37 +61,40 @@
getExtractorFactory(
Collections.singletonList(
new Format.Builder().setSampleMimeType(MimeTypes.APPLICATION_CEA608).build()));
- ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4");
+ ExtractorAsserts.assertBehavior(
+ extractorFactory, "mp4/sample_fragmented_sei.mp4", assertionConfig);
}
@Test
public void sampleWithAc3Track() throws Exception {
ExtractorAsserts.assertBehavior(
- getExtractorFactory(ImmutableList.of()), "mp4/sample_ac3_fragmented.mp4");
+ getExtractorFactory(ImmutableList.of()), "mp4/sample_ac3_fragmented.mp4", assertionConfig);
}
@Test
public void sampleWithAc4Track() throws Exception {
ExtractorAsserts.assertBehavior(
- getExtractorFactory(ImmutableList.of()), "mp4/sample_ac4_fragmented.mp4");
+ getExtractorFactory(ImmutableList.of()), "mp4/sample_ac4_fragmented.mp4", assertionConfig);
}
@Test
public void sampleWithProtectedAc4Track() throws Exception {
ExtractorAsserts.assertBehavior(
- getExtractorFactory(ImmutableList.of()), "mp4/sample_ac4_protected.mp4");
+ getExtractorFactory(ImmutableList.of()), "mp4/sample_ac4_protected.mp4", assertionConfig);
}
@Test
public void sampleWithEac3Track() throws Exception {
ExtractorAsserts.assertBehavior(
- getExtractorFactory(ImmutableList.of()), "mp4/sample_eac3_fragmented.mp4");
+ getExtractorFactory(ImmutableList.of()), "mp4/sample_eac3_fragmented.mp4", assertionConfig);
}
@Test
public void sampleWithEac3jocTrack() throws Exception {
ExtractorAsserts.assertBehavior(
- getExtractorFactory(ImmutableList.of()), "mp4/sample_eac3joc_fragmented.mp4");
+ getExtractorFactory(ImmutableList.of()),
+ "mp4/sample_eac3joc_fragmented.mp4",
+ assertionConfig);
}
private static ExtractorFactory getExtractorFactory(final List<Format> closedCaptionFormats) {
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java
index 5c1e8e1..d2b54e7 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java
@@ -15,23 +15,35 @@
*/
package com.google.android.exoplayer2.extractor.mp4;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Tests for {@link Mp4Extractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public final class Mp4ExtractorTest {
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void mp4Sample() throws Exception {
- ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample.mp4");
+ ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample.mp4", assertionConfig);
}
@Test
public void mp4SampleWithSlowMotionMetadata() throws Exception {
- ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_android_slow_motion.mp4");
+ ExtractorAsserts.assertBehavior(
+ Mp4Extractor::new, "mp4/sample_android_slow_motion.mp4", assertionConfig);
}
/**
@@ -40,26 +52,27 @@
*/
@Test
public void mp4SampleWithMdatTooLong() throws Exception {
- ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_mdat_too_long.mp4");
+ ExtractorAsserts.assertBehavior(
+ Mp4Extractor::new, "mp4/sample_mdat_too_long.mp4", assertionConfig);
}
@Test
public void mp4SampleWithAc3Track() throws Exception {
- ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_ac3.mp4");
+ ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_ac3.mp4", assertionConfig);
}
@Test
public void mp4SampleWithAc4Track() throws Exception {
- ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_ac4.mp4");
+ ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_ac4.mp4", assertionConfig);
}
@Test
public void mp4SampleWithEac3Track() throws Exception {
- ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_eac3.mp4");
+ ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_eac3.mp4", assertionConfig);
}
@Test
public void mp4SampleWithEac3jocTrack() throws Exception {
- ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_eac3joc.mp4");
+ ExtractorAsserts.assertBehavior(Mp4Extractor::new, "mp4/sample_eac3joc.mp4", assertionConfig);
}
}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorNonParameterizedTest.java
similarity index 75%
rename from tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java
rename to tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorNonParameterizedTest.java
index bffaa58..bf2a350 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorNonParameterizedTest.java
@@ -20,37 +20,19 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
-import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
-/** Unit test for {@link OggExtractor}. */
+/**
+ * Tests for {@link OggExtractor} that test specific behaviours and don't need to be parameterized.
+ *
+ * <p>For parameterized tests using {@link ExtractorAsserts} see {@link
+ * OggExtractorParameterizedTest}.
+ */
@RunWith(AndroidJUnit4.class)
-public final class OggExtractorTest {
-
- private static final ExtractorFactory OGG_EXTRACTOR_FACTORY = OggExtractor::new;
-
- @Test
- public void opus() throws Exception {
- ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus");
- }
-
- @Test
- public void flac() throws Exception {
- ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg");
- }
-
- @Test
- public void flacNoSeektable() throws Exception {
- ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg");
- }
-
- @Test
- public void vorbis() throws Exception {
- ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg");
- }
+public final class OggExtractorNonParameterizedTest {
@Test
public void sniffVorbis() throws Exception {
@@ -97,6 +79,6 @@
.setSimulateUnknownLength(true)
.setSimulatePartialReads(true)
.build();
- ExtractorAsserts.assertSniff(OGG_EXTRACTOR_FACTORY.create(), input, expectedResult);
+ ExtractorAsserts.assertSniff(new OggExtractor(), input, expectedResult);
}
}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java
new file mode 100644
index 0000000..2235539
--- /dev/null
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
+
+/**
+ * Unit tests for {@link OggExtractor} that use parameterization to test a range of behaviours.
+ *
+ * <p>For non-parameterized tests see {@link OggExtractorNonParameterizedTest}.
+ */
+@RunWith(ParameterizedRobolectricTestRunner.class)
+public final class OggExtractorParameterizedTest {
+
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
+ @Test
+ public void opus() throws Exception {
+ ExtractorAsserts.assertBehavior(OggExtractor::new, "ogg/bear.opus", assertionConfig);
+ }
+
+ @Test
+ public void flac() throws Exception {
+ ExtractorAsserts.assertBehavior(OggExtractor::new, "ogg/bear_flac.ogg", assertionConfig);
+ }
+
+ @Test
+ public void flacNoSeektable() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ OggExtractor::new, "ogg/bear_flac_noseektable.ogg", assertionConfig);
+ }
+
+ @Test
+ public void vorbis() throws Exception {
+ ExtractorAsserts.assertBehavior(OggExtractor::new, "ogg/bear_vorbis.ogg", assertionConfig);
+ }
+}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java
index ca7e30a..1cae51a 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java
@@ -15,17 +15,26 @@
*/
package com.google.android.exoplayer2.extractor.rawcc;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.util.MimeTypes;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
/** Tests for {@link RawCcExtractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public final class RawCcExtractorTest {
+ @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @ParameterizedRobolectricTestRunner.Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void rawCcSample() throws Exception {
Format format =
@@ -34,6 +43,7 @@
.setCodecs("cea608")
.setAccessibilityChannel(1)
.build();
- ExtractorAsserts.assertBehavior(() -> new RawCcExtractor(format), "rawcc/sample.rawcc");
+ ExtractorAsserts.assertBehavior(
+ () -> new RawCcExtractor(format), "rawcc/sample.rawcc", assertionConfig);
}
}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java
index df216c7..8e46999 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java
@@ -15,27 +15,38 @@
*/
package com.google.android.exoplayer2.extractor.ts;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Unit test for {@link Ac3Extractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public final class Ac3ExtractorTest {
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void ac3Sample() throws Exception {
- ExtractorAsserts.assertBehavior(Ac3Extractor::new, "ts/sample.ac3");
+ ExtractorAsserts.assertBehavior(Ac3Extractor::new, "ts/sample.ac3", assertionConfig);
}
@Test
public void eAc3Sample() throws Exception {
- ExtractorAsserts.assertBehavior(Ac3Extractor::new, "ts/sample.eac3");
+ ExtractorAsserts.assertBehavior(Ac3Extractor::new, "ts/sample.eac3", assertionConfig);
}
@Test
public void eAc3jocSample() throws Exception {
- ExtractorAsserts.assertBehavior(Ac3Extractor::new, "ts/sample_eac3joc.ec3");
+ ExtractorAsserts.assertBehavior(Ac3Extractor::new, "ts/sample_eac3joc.ec3", assertionConfig);
}
}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java
index 8ddd6f5..36c8dd8 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac4ExtractorTest.java
@@ -15,17 +15,28 @@
*/
package com.google.android.exoplayer2.extractor.ts;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Unit test for {@link Ac4Extractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public final class Ac4ExtractorTest {
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void ac4Sample() throws Exception {
- ExtractorAsserts.assertBehavior(Ac4Extractor::new, "ts/sample.ac4");
+ ExtractorAsserts.assertBehavior(Ac4Extractor::new, "ts/sample.ac4", assertionConfig);
}
}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java
index a06d228..0928f8b 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java
@@ -15,30 +15,42 @@
*/
package com.google.android.exoplayer2.extractor.ts;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Unit test for {@link AdtsExtractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public final class AdtsExtractorTest {
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void sample() throws Exception {
- ExtractorAsserts.assertBehavior(AdtsExtractor::new, "ts/sample.adts");
+ ExtractorAsserts.assertBehavior(AdtsExtractor::new, "ts/sample.adts", assertionConfig);
}
@Test
public void sample_with_id3() throws Exception {
- ExtractorAsserts.assertBehavior(AdtsExtractor::new, "ts/sample_with_id3.adts");
+ ExtractorAsserts.assertBehavior(AdtsExtractor::new, "ts/sample_with_id3.adts", assertionConfig);
}
@Test
public void sample_withSeeking() throws Exception {
ExtractorAsserts.assertBehavior(
() -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING),
- "ts/sample_cbs.adts");
+ "ts/sample_cbs.adts",
+ assertionConfig);
}
// https://github.com/google/ExoPlayer/issues/6700
@@ -46,6 +58,7 @@
public void sample_withSeekingAndTruncatedFile() throws Exception {
ExtractorAsserts.assertBehavior(
() -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING),
- "ts/sample_cbs_truncated.adts");
+ "ts/sample_cbs_truncated.adts",
+ assertionConfig);
}
}
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java
index c9e9dce..de3fbf2 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java
@@ -15,22 +15,34 @@
*/
package com.google.android.exoplayer2.extractor.ts;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Unit test for {@link PsExtractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public final class PsExtractorTest {
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void sampleWithH262AndMpegAudio() throws Exception {
- ExtractorAsserts.assertBehavior(PsExtractor::new, "ts/sample_h262_mpeg_audio.ps");
+ ExtractorAsserts.assertBehavior(
+ PsExtractor::new, "ts/sample_h262_mpeg_audio.ps", assertionConfig);
}
@Test
public void sampleWithAc3() throws Exception {
- ExtractorAsserts.assertBehavior(PsExtractor::new, "ts/sample_ac3.ps");
+ ExtractorAsserts.assertBehavior(PsExtractor::new, "ts/sample_ac3.ps", assertionConfig);
}
}
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 d040c22..68ba0d4 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
@@ -15,12 +15,12 @@
*/
package com.google.android.exoplayer2.extractor.ts;
+import static com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory.FLAG_DETECT_ACCESS_UNITS;
import static com.google.common.truth.Truth.assertThat;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
@@ -36,27 +36,57 @@
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.util.List;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/** Unit test for {@link TsExtractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public final class TsExtractorTest {
+ @Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void sampleWithH262AndMpegAudio() throws Exception {
- ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_h262_mpeg_audio.ts");
+ ExtractorAsserts.assertBehavior(
+ TsExtractor::new, "ts/sample_h262_mpeg_audio.ts", assertionConfig);
}
@Test
public void sampleWithH264AndMpegAudio() throws Exception {
- ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_h264_mpeg_audio.ts");
+ ExtractorAsserts.assertBehavior(
+ TsExtractor::new, "ts/sample_h264_mpeg_audio.ts", assertionConfig);
+ }
+
+ @Test
+ public void sampleWithH264NoAccessUnitDelimiters() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ () -> new TsExtractor(FLAG_DETECT_ACCESS_UNITS),
+ "ts/sample_h264_no_access_unit_delimiters.ts",
+ assertionConfig);
+ }
+
+ @Test
+ public void sampleWithH264AndDtsAudio() throws Exception {
+ ExtractorAsserts.assertBehavior(
+ () -> new TsExtractor(DefaultTsPayloadReaderFactory.FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS),
+ "ts/sample_h264_dts_audio.ts",
+ assertionConfig);
}
@Test
public void sampleWithH265() throws Exception {
- ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_h265.ts");
+ ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_h265.ts", assertionConfig);
}
@Test
@@ -64,7 +94,7 @@
// 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");
+ ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_scte35.ts", assertionConfig);
}
@Test
@@ -72,38 +102,37 @@
// 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");
+ ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_ait.ts", assertionConfig);
}
@Test
public void sampleWithAc3() throws Exception {
- ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_ac3.ts");
+ ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_ac3.ts", assertionConfig);
}
@Test
public void sampleWithAc4() throws Exception {
- ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_ac4.ts");
+ ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_ac4.ts", assertionConfig);
}
@Test
public void sampleWithEac3() throws Exception {
- ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_eac3.ts");
+ ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_eac3.ts", assertionConfig);
}
@Test
public void sampleWithEac3joc() throws Exception {
- ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_eac3joc.ts");
+ ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_eac3joc.ts", assertionConfig);
}
@Test
public void sampleWithLatm() throws Exception {
- ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_latm.ts");
+ ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_latm.ts", assertionConfig);
}
@Test
public void streamWithJunkData() throws Exception {
- ExtractorAsserts.assertBehavior(
- TsExtractor::new, "ts/sample_with_junk", ApplicationProvider.getApplicationContext());
+ ExtractorAsserts.assertBehavior(TsExtractor::new, "ts/sample_with_junk", assertionConfig);
}
@Test
diff --git a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java
index 9287c68..cbcc847 100644
--- a/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java
+++ b/tree/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java
@@ -15,19 +15,27 @@
*/
package com.google.android.exoplayer2.extractor.wav;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
+import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
/** Unit test for {@link WavExtractor}. */
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedRobolectricTestRunner.class)
public final class WavExtractorTest {
+ @ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
+ public static List<Object[]> params() {
+ return ExtractorAsserts.configs();
+ }
+
+ @ParameterizedRobolectricTestRunner.Parameter(0)
+ public ExtractorAsserts.Config assertionConfig;
+
@Test
public void sample() throws Exception {
- ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample.wav");
+ ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample.wav", assertionConfig);
}
@Test
@@ -35,12 +43,12 @@
ExtractorAsserts.assertBehavior(
WavExtractor::new,
"wav/sample_with_trailing_bytes.wav",
- ApplicationProvider.getApplicationContext(),
+ assertionConfig,
/* dumpFilesPrefix= */ "wav/sample.wav");
}
@Test
public void sample_imaAdpcm() throws Exception {
- ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample_ima_adpcm.wav");
+ ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample_ima_adpcm.wav", assertionConfig);
}
}
diff --git a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
index 41dc652..20d8ed3 100644
--- a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
+++ b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
@@ -17,6 +17,7 @@
import android.net.Uri;
import android.os.Handler;
+import android.os.Looper;
import android.util.SparseIntArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
@@ -36,6 +37,7 @@
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
+import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleQueue;
import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
@@ -674,15 +676,14 @@
loader.startLoading(
loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));
eventDispatcher.loadStarted(
- loadable.dataSpec,
+ new LoadEventInfo(loadable.dataSpec, elapsedRealtimeMs),
loadable.type,
trackType,
loadable.trackFormat,
loadable.trackSelectionReason,
loadable.trackSelectionData,
loadable.startTimeUs,
- loadable.endTimeUs,
- elapsedRealtimeMs);
+ loadable.endTimeUs);
return true;
}
@@ -702,19 +703,20 @@
public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {
chunkSource.onChunkLoadCompleted(loadable);
eventDispatcher.loadCompleted(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
loadable.type,
trackType,
loadable.trackFormat,
loadable.trackSelectionReason,
loadable.trackSelectionData,
loadable.startTimeUs,
- loadable.endTimeUs,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ loadable.endTimeUs);
if (!prepared) {
continueLoading(lastSeekPositionUs);
} else {
@@ -726,19 +728,20 @@
public void onLoadCanceled(
Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) {
eventDispatcher.loadCanceled(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
loadable.type,
trackType,
loadable.trackFormat,
loadable.trackSelectionReason,
loadable.trackSelectionData,
loadable.startTimeUs,
- loadable.endTimeUs,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ loadable.endTimeUs);
if (!released) {
resetSampleQueues();
if (enabledTrackGroupCount > 0) {
@@ -786,9 +789,13 @@
}
eventDispatcher.loadError(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ bytesLoaded),
loadable.type,
trackType,
loadable.trackFormat,
@@ -796,9 +803,6 @@
loadable.trackSelectionData,
loadable.startTimeUs,
loadable.endTimeUs,
- elapsedRealtimeMs,
- loadDurationMs,
- bytesLoaded,
error,
/* wasCanceled= */ !loadErrorAction.isRetry());
@@ -907,7 +911,12 @@
boolean isAudioVideo = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO;
HlsSampleQueue sampleQueue =
- new HlsSampleQueue(allocator, drmSessionManager, eventDispatcher, overridingDrmInitData);
+ new HlsSampleQueue(
+ allocator,
+ /* playbackLooper= */ handler.getLooper(),
+ drmSessionManager,
+ eventDispatcher,
+ overridingDrmInitData);
if (isAudioVideo) {
sampleQueue.setDrmInitData(drmInitData);
}
@@ -1377,10 +1386,11 @@
private HlsSampleQueue(
Allocator allocator,
+ Looper playbackLooper,
DrmSessionManager drmSessionManager,
MediaSourceEventDispatcher eventDispatcher,
Map<String, DrmInitData> overridingDrmInitData) {
- super(allocator, drmSessionManager, eventDispatcher);
+ super(allocator, playbackLooper, drmSessionManager, eventDispatcher);
this.overridingDrmInitData = overridingDrmInitData;
}
@@ -1521,7 +1531,8 @@
}
@Override
- public int sampleData(DataReader input, int length, boolean allowEndOfInput)
+ public int sampleData(
+ DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart)
throws IOException {
ensureBufferCapacity(bufferPosition + length);
int numBytesRead = input.read(buffer, bufferPosition, length);
@@ -1537,7 +1548,8 @@
}
@Override
- public void sampleData(ParsableByteArray buffer, int length) {
+ public void sampleData(
+ ParsableByteArray buffer, int length, @SampleDataPart int sampleDataPart) {
ensureBufferCapacity(bufferPosition + length);
buffer.readBytes(this.buffer, bufferPosition, length);
bufferPosition += length;
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 d172aa2..cd8651d 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
@@ -17,7 +17,6 @@
import android.net.Uri;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.SegmentDownloader;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
@@ -27,11 +26,13 @@
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.util.UriUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.concurrent.Executor;
/**
* A downloader for HLS streams.
@@ -40,20 +41,20 @@
*
* <pre>{@code
* SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
- * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
- * DownloaderConstructorHelper constructorHelper =
- * new DownloaderConstructorHelper(cache, factory);
+ * CacheDataSource.Factory cacheDataSourceFactory =
+ * new CacheDataSource.Factory()
+ * .setCache(cache)
+ * .setUpstreamDataSourceFactory(new DefaultHttpDataSourceFactory(userAgent));
* // Create a downloader for the first variant in a master playlist.
* HlsDownloader hlsDownloader =
* new HlsDownloader(
* playlistUri,
- * Collections.singletonList(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0)),
- * constructorHelper);
+ * Collections.singletonList(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, 0));
* // Perform the download.
* hlsDownloader.download(progressListener);
- * // Access downloaded data using CacheDataSource
- * CacheDataSource cacheDataSource =
- * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
+ * // Use the downloaded data for playback.
+ * HlsMediaSource mediaSource =
+ * new HlsMediaSource.Factory(cacheDataSourceFactory).createMediaSource(mediaItem);
* }</pre>
*/
public final class HlsDownloader extends SegmentDownloader<HlsPlaylist> {
@@ -62,11 +63,30 @@
* @param playlistUri The {@link Uri} of the playlist to be downloaded.
* @param streamKeys Keys defining which renditions in the playlist should be selected for
* download. If empty, all renditions are downloaded.
- * @param constructorHelper A {@link DownloaderConstructorHelper} instance.
+ * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
+ * download will be written.
*/
public HlsDownloader(
- Uri playlistUri, List<StreamKey> streamKeys, DownloaderConstructorHelper constructorHelper) {
- super(playlistUri, streamKeys, constructorHelper);
+ Uri playlistUri, List<StreamKey> streamKeys, CacheDataSource.Factory cacheDataSourceFactory) {
+ this(playlistUri, streamKeys, cacheDataSourceFactory, Runnable::run);
+ }
+
+ /**
+ * @param playlistUri The {@link Uri} of the playlist to be downloaded.
+ * @param streamKeys Keys defining which renditions in the playlist should be selected for
+ * download. If empty, all renditions are downloaded.
+ * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
+ * download will be written.
+ * @param executor An {@link Executor} used to make requests for the media being downloaded.
+ * Providing an {@link Executor} that uses multiple threads will speed up the download by
+ * allowing parts of it to be executed in parallel.
+ */
+ public HlsDownloader(
+ Uri playlistUri,
+ List<StreamKey> streamKeys,
+ CacheDataSource.Factory cacheDataSourceFactory,
+ Executor executor) {
+ super(playlistUri, streamKeys, cacheDataSourceFactory, executor);
}
@Override
diff --git a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java
index e624027..9178c8a 100644
--- a/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java
+++ b/tree/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java
@@ -21,6 +21,7 @@
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
@@ -135,9 +136,8 @@
this,
loadErrorHandlingPolicy.getMinimumLoadableRetryCount(masterPlaylistLoadable.type));
eventDispatcher.loadStarted(
- masterPlaylistLoadable.dataSpec,
- masterPlaylistLoadable.type,
- elapsedRealtime);
+ new LoadEventInfo(masterPlaylistLoadable.dataSpec, elapsedRealtime),
+ masterPlaylistLoadable.type);
}
@Override
@@ -242,13 +242,14 @@
primaryBundle.loadPlaylist();
}
eventDispatcher.loadCompleted(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
- C.DATA_TYPE_MANIFEST,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
+ C.DATA_TYPE_MANIFEST);
}
@Override
@@ -258,13 +259,14 @@
long loadDurationMs,
boolean released) {
eventDispatcher.loadCanceled(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
- C.DATA_TYPE_MANIFEST,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
+ C.DATA_TYPE_MANIFEST);
}
@Override
@@ -279,13 +281,14 @@
loadable.type, loadDurationMs, error, errorCount);
boolean isFatal = retryDelayMs == C.TIME_UNSET;
eventDispatcher.loadError(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
C.DATA_TYPE_MANIFEST,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded(),
error,
isFatal);
return isFatal
@@ -521,13 +524,14 @@
if (result instanceof HlsMediaPlaylist) {
processLoadedPlaylist((HlsMediaPlaylist) result, loadDurationMs);
eventDispatcher.loadCompleted(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
- C.DATA_TYPE_MANIFEST,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
+ C.DATA_TYPE_MANIFEST);
} else {
playlistError = new ParserException("Loaded playlist has unexpected type.");
}
@@ -540,13 +544,14 @@
long loadDurationMs,
boolean released) {
eventDispatcher.loadCanceled(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
- C.DATA_TYPE_MANIFEST,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
+ C.DATA_TYPE_MANIFEST);
}
@Override
@@ -582,13 +587,14 @@
}
eventDispatcher.loadError(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
C.DATA_TYPE_MANIFEST,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded(),
error,
/* wasCanceled= */ !loadErrorAction.isRetry());
@@ -612,9 +618,8 @@
this,
loadErrorHandlingPolicy.getMinimumLoadableRetryCount(mediaPlaylistLoadable.type));
eventDispatcher.loadStarted(
- mediaPlaylistLoadable.dataSpec,
- mediaPlaylistLoadable.type,
- elapsedRealtime);
+ new LoadEventInfo(mediaPlaylistLoadable.dataSpec, elapsedRealtime),
+ mediaPlaylistLoadable.type);
}
private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist, long loadDurationMs) {
diff --git a/tree/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/tree/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java
index a8473b8..0dcae17 100644
--- a/tree/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java
+++ b/tree/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java
@@ -40,15 +40,15 @@
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.Downloader;
-import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.DownloaderFactory;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
import com.google.android.exoplayer2.testutil.CacheAsserts.RequestSet;
import com.google.android.exoplayer2.testutil.FakeDataSet;
-import com.google.android.exoplayer2.testutil.FakeDataSource.Factory;
+import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util;
@@ -97,9 +97,11 @@
@Test
public void createWithDefaultDownloaderFactory() {
- DownloaderConstructorHelper constructorHelper =
- new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
- DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
+ CacheDataSource.Factory cacheDataSourceFactory =
+ new CacheDataSource.Factory()
+ .setCache(Mockito.mock(Cache.class))
+ .setUpstreamDataSourceFactory(DummyDataSource.FACTORY);
+ DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory);
Downloader downloader =
factory.createDownloader(
@@ -213,9 +215,11 @@
}
private HlsDownloader getHlsDownloader(String mediaPlaylistUri, List<StreamKey> keys) {
- Factory factory = new Factory().setFakeDataSet(fakeDataSet);
- return new HlsDownloader(
- Uri.parse(mediaPlaylistUri), keys, new DownloaderConstructorHelper(cache, factory));
+ CacheDataSource.Factory cacheDataSourceFactory =
+ new CacheDataSource.Factory()
+ .setCache(cache)
+ .setUpstreamDataSourceFactory(new FakeDataSource.Factory().setFakeDataSet(fakeDataSet));
+ return new HlsDownloader(Uri.parse(mediaPlaylistUri), keys, cacheDataSourceFactory);
}
private static ArrayList<StreamKey> getKeys(int... variantIndices) {
diff --git a/tree/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/tree/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
index ac3085c..418fb17 100644
--- a/tree/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
+++ b/tree/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
@@ -30,6 +30,7 @@
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
+import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
@@ -621,13 +622,14 @@
public void onLoadCompleted(
ParsingLoadable<SsManifest> loadable, long elapsedRealtimeMs, long loadDurationMs) {
manifestEventDispatcher.loadCompleted(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
- loadable.type,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
+ loadable.type);
manifest = loadable.getResult();
manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs;
processManifest();
@@ -641,13 +643,14 @@
long loadDurationMs,
boolean released) {
manifestEventDispatcher.loadCanceled(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
- loadable.type,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded());
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
+ loadable.type);
}
@Override
@@ -665,13 +668,14 @@
? Loader.DONT_RETRY_FATAL
: Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs);
manifestEventDispatcher.loadError(
- loadable.dataSpec,
- loadable.getUri(),
- loadable.getResponseHeaders(),
+ new LoadEventInfo(
+ loadable.dataSpec,
+ loadable.getUri(),
+ loadable.getResponseHeaders(),
+ elapsedRealtimeMs,
+ loadDurationMs,
+ loadable.bytesLoaded()),
loadable.type,
- elapsedRealtimeMs,
- loadDurationMs,
- loadable.bytesLoaded(),
error,
!loadErrorAction.isRetry());
return loadErrorAction;
@@ -767,7 +771,8 @@
long elapsedRealtimeMs =
manifestLoader.startLoading(
loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type));
- manifestEventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);
+ manifestEventDispatcher.loadStarted(
+ new LoadEventInfo(loadable.dataSpec, elapsedRealtimeMs), loadable.type);
}
}
diff --git a/tree/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/tree/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java
index 1331fe4..dc141a2 100644
--- a/tree/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java
+++ b/tree/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java
@@ -17,7 +17,6 @@
import android.net.Uri;
import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.SegmentDownloader;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
@@ -27,9 +26,11 @@
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
/**
* A downloader for SmoothStreaming streams.
@@ -38,20 +39,21 @@
*
* <pre>{@code
* SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor(), databaseProvider);
- * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
- * DownloaderConstructorHelper constructorHelper =
- * new DownloaderConstructorHelper(cache, factory);
+ * CacheDataSource.Factory cacheDataSourceFactory =
+ * new CacheDataSource.Factory()
+ * .setCache(cache)
+ * .setUpstreamDataSourceFactory(new DefaultHttpDataSourceFactory(userAgent));
* // Create a downloader for the first track of the first stream element.
* SsDownloader ssDownloader =
* new SsDownloader(
* manifestUrl,
* Collections.singletonList(new StreamKey(0, 0)),
- * constructorHelper);
+ * cacheDataSourceFactory);
* // Perform the download.
* ssDownloader.download(progressListener);
- * // Access downloaded data using CacheDataSource
- * CacheDataSource cacheDataSource =
- * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);
+ * // Use the downloaded data for playback.
+ * SsMediaSource mediaSource =
+ * new SsMediaSource.Factory(cacheDataSourceFactory).createMediaSource(mediaItem);
* }</pre>
*/
public final class SsDownloader extends SegmentDownloader<SsManifest> {
@@ -60,11 +62,30 @@
* @param manifestUri The {@link Uri} of the manifest to be downloaded.
* @param streamKeys Keys defining which streams in the manifest should be selected for download.
* If empty, all streams are downloaded.
- * @param constructorHelper A {@link DownloaderConstructorHelper} instance.
+ * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
+ * download will be written.
*/
public SsDownloader(
- Uri manifestUri, List<StreamKey> streamKeys, DownloaderConstructorHelper constructorHelper) {
- super(SsUtil.fixManifestUri(manifestUri), streamKeys, constructorHelper);
+ Uri manifestUri, List<StreamKey> streamKeys, CacheDataSource.Factory cacheDataSourceFactory) {
+ this(manifestUri, streamKeys, cacheDataSourceFactory, Runnable::run);
+ }
+
+ /**
+ * @param manifestUri The {@link Uri} of the manifest to be downloaded.
+ * @param streamKeys Keys defining which streams in the manifest should be selected for download.
+ * If empty, all streams are downloaded.
+ * @param cacheDataSourceFactory A {@link CacheDataSource.Factory} for the cache into which the
+ * download will be written.
+ * @param executor An {@link Executor} used to make requests for the media being downloaded.
+ * Providing an {@link Executor} that uses multiple threads will speed up the download by
+ * allowing parts of it to be executed in parallel.
+ */
+ public SsDownloader(
+ Uri manifestUri,
+ List<StreamKey> streamKeys,
+ CacheDataSource.Factory cacheDataSourceFactory,
+ Executor executor) {
+ super(SsUtil.fixManifestUri(manifestUri), streamKeys, cacheDataSourceFactory, executor);
}
@Override
diff --git a/tree/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.java b/tree/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.java
index 5560a72..1bbe0b1 100644
--- a/tree/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.java
+++ b/tree/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloaderTest.java
@@ -22,11 +22,11 @@
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory;
import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.Downloader;
-import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.DownloaderFactory;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -38,9 +38,11 @@
@Test
public void createWithDefaultDownloaderFactory() throws Exception {
- DownloaderConstructorHelper constructorHelper =
- new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY);
- DownloaderFactory factory = new DefaultDownloaderFactory(constructorHelper);
+ CacheDataSource.Factory cacheDataSourceFactory =
+ new CacheDataSource.Factory()
+ .setCache(Mockito.mock(Cache.class))
+ .setUpstreamDataSourceFactory(DummyDataSource.FACTORY);
+ DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory);
Downloader downloader =
factory.createDownloader(
diff --git a/tree/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/tree/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java
index 841d1b6..f0093a2 100644
--- a/tree/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java
+++ b/tree/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java
@@ -65,7 +65,8 @@
private final float spacingAdd;
private final TextPaint textPaint;
- private final Paint paint;
+ private final Paint windowPaint;
+ private final Paint bitmapPaint;
// Previous input variables.
@Nullable private CharSequence cueText;
@@ -124,9 +125,13 @@
textPaint.setAntiAlias(true);
textPaint.setSubpixelText(true);
- paint = new Paint();
- paint.setAntiAlias(true);
- paint.setStyle(Style.FILL);
+ windowPaint = new Paint();
+ windowPaint.setAntiAlias(true);
+ windowPaint.setStyle(Style.FILL);
+
+ bitmapPaint = new Paint();
+ bitmapPaint.setAntiAlias(true);
+ bitmapPaint.setFilterBitmap(true);
}
/**
@@ -442,9 +447,13 @@
canvas.translate(textLeft, textTop);
if (Color.alpha(windowColor) > 0) {
- paint.setColor(windowColor);
+ windowPaint.setColor(windowColor);
canvas.drawRect(
- -textPaddingX, 0, textLayout.getWidth() + textPaddingX, textLayout.getHeight(), paint);
+ -textPaddingX,
+ 0,
+ textLayout.getWidth() + textPaddingX,
+ textLayout.getHeight(),
+ windowPaint);
}
if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) {
@@ -478,7 +487,7 @@
@RequiresNonNull({"cueBitmap", "bitmapRect"})
private void drawBitmapLayout(Canvas canvas) {
- canvas.drawBitmap(cueBitmap, /* src= */ null, bitmapRect, /* paint= */ null);
+ canvas.drawBitmap(cueBitmap, /* src= */ null, bitmapRect, bitmapPaint);
}
/**
diff --git a/tree/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java b/tree/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java
index c2e14ab..5e92dfd 100644
--- a/tree/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java
+++ b/tree/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java
@@ -20,7 +20,6 @@
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
-import com.google.android.exoplayer2.offline.DownloaderConstructorHelper;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.dash.DashUtil;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
@@ -28,10 +27,9 @@
import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.source.dash.offline.DashDownloader;
import com.google.android.exoplayer2.testutil.HostActivity;
+import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
-import com.google.android.exoplayer2.upstream.DummyDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
-import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util;
@@ -57,8 +55,8 @@
private DashTestRunner testRunner;
private File tempFolder;
private SimpleCache cache;
- private DefaultHttpDataSourceFactory httpDataSourceFactory;
- private CacheDataSourceFactory offlineDataSourceFactory;
+ private DataSource.Factory httpDataSourceFactory;
+ private DataSource.Factory offlineDataSourceFactory;
@Before
public void setUp() throws Exception {
@@ -72,9 +70,7 @@
tempFolder = Util.createTempDirectory(testRule.getActivity(), "ExoPlayerTest");
cache = new SimpleCache(tempFolder, new NoOpCacheEvictor());
httpDataSourceFactory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
- offlineDataSourceFactory =
- new CacheDataSourceFactory(
- cache, DummyDataSource.FACTORY, CacheDataSource.FLAG_BLOCK_ON_CACHE);
+ offlineDataSourceFactory = new CacheDataSource.Factory().setCache(cache);
}
@After
@@ -120,9 +116,11 @@
}
}
}
- DownloaderConstructorHelper constructorHelper =
- new DownloaderConstructorHelper(cache, httpDataSourceFactory);
- return new DashDownloader(MANIFEST_URI, keys, constructorHelper);
+ CacheDataSource.Factory cacheDataSourceFactory =
+ new CacheDataSource.Factory()
+ .setCache(cache)
+ .setUpstreamDataSourceFactory(httpDataSourceFactory);
+ return new DashDownloader(MANIFEST_URI, keys, cacheDataSourceFactory);
}
}
diff --git a/tree/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java b/tree/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java
index befdc49..40ec1ed 100644
--- a/tree/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java
+++ b/tree/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashWidevineOfflineTest.java
@@ -17,7 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.assertThrows;
import android.media.MediaDrm.MediaDrmStateException;
import android.net.Uri;
@@ -111,29 +111,47 @@
@Test
public void widevineOfflineReleasedLicenseV22() throws Throwable {
- if (Util.SDK_INT < 22) {
+ if (Util.SDK_INT < 22 || Util.SDK_INT > 28) {
return; // Pass.
}
downloadLicense();
releaseLicense(); // keySetId no longer valid.
- try {
- testRunner.run();
- fail("Playback should fail because the license has been released.");
- } catch (Throwable e) {
- // Get the root cause
- while (true) {
- Throwable cause = e.getCause();
- if (cause == null || cause == e) {
- break;
- }
- e = cause;
- }
- // It should be a MediaDrmStateException instance
- if (!(e instanceof MediaDrmStateException)) {
- throw e;
- }
+ Throwable error =
+ assertThrows(
+ "Playback should fail because the license has been released.",
+ Throwable.class,
+ () -> testRunner.run());
+
+ // Get the root cause
+ Throwable cause = error.getCause();
+ while (cause != null && cause != error) {
+ error = cause;
+ cause = error.getCause();
}
+ assertThat(error).isInstanceOf(MediaDrmStateException.class);
+ }
+
+ @Test
+ public void widevineOfflineReleasedLicenseV29() throws Throwable {
+ if (Util.SDK_INT < 29) {
+ return; // Pass.
+ }
+ downloadLicense();
+ releaseLicense(); // keySetId no longer valid.
+
+ Throwable error =
+ assertThrows(
+ "Playback should fail because the license has been released.",
+ Throwable.class,
+ () -> testRunner.run());
+ // Get the root cause
+ Throwable cause = error.getCause();
+ while (cause != null && cause != error) {
+ error = cause;
+ cause = error.getCause();
+ }
+ assertThat(error).isInstanceOf(IllegalArgumentException.class);
}
@Test
diff --git a/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv b/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv
new file mode 100644
index 0000000..02574ff
--- /dev/null
+++ b/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv
Binary files differ
diff --git a/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.0.dump b/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.0.dump
new file mode 100644
index 0000000..258efdc
--- /dev/null
+++ b/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.0.dump
@@ -0,0 +1,269 @@
+seekMap:
+ isSeekable = true
+ duration = 1071000
+ getPosition(0) = [[timeUs=0, position=994]]
+ getPosition(1) = [[timeUs=0, position=994]]
+ getPosition(535500) = [[timeUs=0, position=994]]
+ getPosition(1071000) = [[timeUs=0, position=994]]
+numberOfTracks = 2
+track 1:
+ total output bytes = 89502
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ width = 1080
+ height = 720
+ rotationDegrees = 90
+ selectionFlags = 1
+ language = und
+ initializationData:
+ data = length 30, hash F6F3D010
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 36477, hash F0F36CFE
+ sample 1:
+ time = 67000
+ flags = 0
+ data = length 5341, hash 40B85E2
+ sample 2:
+ time = 33000
+ flags = 0
+ data = length 596, hash 357B4D92
+ sample 3:
+ time = 200000
+ flags = 0
+ data = length 7704, hash A39EDA06
+ sample 4:
+ time = 133000
+ flags = 0
+ data = length 989, hash 2813C72D
+ sample 5:
+ time = 100000
+ flags = 0
+ data = length 721, hash C50D1C73
+ sample 6:
+ time = 167000
+ flags = 0
+ data = length 519, hash 65FE1911
+ sample 7:
+ time = 333000
+ flags = 0
+ data = length 6160, hash E1CAC0EC
+ sample 8:
+ time = 267000
+ flags = 0
+ data = length 953, hash 7160C661
+ sample 9:
+ time = 233000
+ flags = 0
+ data = length 620, hash 7A7AE07C
+ sample 10:
+ time = 300000
+ flags = 0
+ data = length 405, hash 5CC7F4E7
+ sample 11:
+ time = 433000
+ flags = 0
+ data = length 4852, hash 9DB6979D
+ sample 12:
+ time = 400000
+ flags = 0
+ data = length 547, hash E31A6979
+ sample 13:
+ time = 367000
+ flags = 0
+ data = length 570, hash FEC40D00
+ sample 14:
+ time = 567000
+ flags = 0
+ data = length 5525, hash 7C478F7E
+ sample 15:
+ time = 500000
+ flags = 0
+ data = length 1082, hash DA07059A
+ sample 16:
+ time = 467000
+ flags = 0
+ data = length 807, hash 93478E6B
+ sample 17:
+ time = 533000
+ flags = 0
+ data = length 744, hash 9A8E6026
+ sample 18:
+ time = 700000
+ flags = 0
+ data = length 4732, hash C73B23C0
+ sample 19:
+ time = 633000
+ flags = 0
+ data = length 1004, hash 8A19A228
+ sample 20:
+ time = 600000
+ flags = 0
+ data = length 794, hash 8126022C
+ sample 21:
+ time = 667000
+ flags = 0
+ data = length 645, hash F08300E5
+ sample 22:
+ time = 833000
+ flags = 0
+ data = length 2684, hash 727FE378
+ sample 23:
+ time = 767000
+ flags = 0
+ data = length 787, hash 419A7821
+ sample 24:
+ time = 733000
+ flags = 0
+ data = length 649, hash 5C159346
+ sample 25:
+ time = 800000
+ flags = 0
+ data = length 509, hash F912D655
+ sample 26:
+ time = 967000
+ flags = 0
+ data = length 1226, hash 29815C21
+ sample 27:
+ time = 900000
+ flags = 0
+ data = length 898, hash D997AD0A
+ sample 28:
+ time = 867000
+ flags = 0
+ data = length 476, hash A0423645
+ sample 29:
+ time = 933000
+ flags = 0
+ data = length 486, hash DDF32CBB
+track 2:
+ total output bytes = 12120
+ sample count = 29
+ format 0:
+ id = 2
+ sampleMimeType = audio/ac3
+ channelCount = 1
+ sampleRate = 44100
+ selectionFlags = 1
+ language = und
+ sample 0:
+ time = 62000
+ flags = 1
+ data = length 416, hash 211F2286
+ sample 1:
+ time = 97000
+ flags = 1
+ data = length 418, hash 77425A86
+ sample 2:
+ time = 131000
+ flags = 1
+ data = length 418, hash A0FE5CA1
+ sample 3:
+ time = 166000
+ flags = 1
+ data = length 418, hash 2309B066
+ sample 4:
+ time = 201000
+ flags = 1
+ data = length 418, hash 928A653B
+ sample 5:
+ time = 236000
+ flags = 1
+ data = length 418, hash 3422F0CB
+ sample 6:
+ time = 270000
+ flags = 1
+ data = length 418, hash EFF43D5B
+ sample 7:
+ time = 306000
+ flags = 1
+ data = length 418, hash FC8093C7
+ sample 8:
+ time = 341000
+ flags = 1
+ data = length 418, hash CCC08A16
+ sample 9:
+ time = 376000
+ flags = 1
+ data = length 418, hash 2A6EE863
+ sample 10:
+ time = 410000
+ flags = 1
+ data = length 418, hash D69A9251
+ sample 11:
+ time = 445000
+ flags = 1
+ data = length 418, hash BCFB758D
+ sample 12:
+ time = 480000
+ flags = 1
+ data = length 418, hash 11B66799
+ sample 13:
+ time = 514000
+ flags = 1
+ data = length 418, hash C824D392
+ sample 14:
+ time = 550000
+ flags = 1
+ data = length 418, hash C167D872
+ sample 15:
+ time = 585000
+ flags = 1
+ data = length 418, hash 4221C855
+ sample 16:
+ time = 620000
+ flags = 1
+ data = length 418, hash 4D4FF934
+ sample 17:
+ time = 654000
+ flags = 1
+ data = length 418, hash 984AA025
+ sample 18:
+ time = 690000
+ flags = 1
+ data = length 418, hash BB788B46
+ sample 19:
+ time = 724000
+ flags = 1
+ data = length 418, hash 9EFBFD97
+ sample 20:
+ time = 759000
+ flags = 1
+ data = length 418, hash DF1A460C
+ sample 21:
+ time = 793000
+ flags = 1
+ data = length 418, hash 2BDB56A
+ sample 22:
+ time = 829000
+ flags = 1
+ data = length 418, hash CA230060
+ sample 23:
+ time = 864000
+ flags = 1
+ data = length 418, hash D2F19F41
+ sample 24:
+ time = 898000
+ flags = 1
+ data = length 418, hash AF392D79
+ sample 25:
+ time = 932000
+ flags = 1
+ data = length 418, hash C5D7F2A3
+ sample 26:
+ time = 968000
+ flags = 1
+ data = length 418, hash 733A35AE
+ sample 27:
+ time = 1002000
+ flags = 1
+ data = length 418, hash DE46E5D3
+ sample 28:
+ time = 1037000
+ flags = 1
+ data = length 418, hash 56AB8D37
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.1.dump b/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.1.dump
new file mode 100644
index 0000000..258efdc
--- /dev/null
+++ b/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.1.dump
@@ -0,0 +1,269 @@
+seekMap:
+ isSeekable = true
+ duration = 1071000
+ getPosition(0) = [[timeUs=0, position=994]]
+ getPosition(1) = [[timeUs=0, position=994]]
+ getPosition(535500) = [[timeUs=0, position=994]]
+ getPosition(1071000) = [[timeUs=0, position=994]]
+numberOfTracks = 2
+track 1:
+ total output bytes = 89502
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ width = 1080
+ height = 720
+ rotationDegrees = 90
+ selectionFlags = 1
+ language = und
+ initializationData:
+ data = length 30, hash F6F3D010
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 36477, hash F0F36CFE
+ sample 1:
+ time = 67000
+ flags = 0
+ data = length 5341, hash 40B85E2
+ sample 2:
+ time = 33000
+ flags = 0
+ data = length 596, hash 357B4D92
+ sample 3:
+ time = 200000
+ flags = 0
+ data = length 7704, hash A39EDA06
+ sample 4:
+ time = 133000
+ flags = 0
+ data = length 989, hash 2813C72D
+ sample 5:
+ time = 100000
+ flags = 0
+ data = length 721, hash C50D1C73
+ sample 6:
+ time = 167000
+ flags = 0
+ data = length 519, hash 65FE1911
+ sample 7:
+ time = 333000
+ flags = 0
+ data = length 6160, hash E1CAC0EC
+ sample 8:
+ time = 267000
+ flags = 0
+ data = length 953, hash 7160C661
+ sample 9:
+ time = 233000
+ flags = 0
+ data = length 620, hash 7A7AE07C
+ sample 10:
+ time = 300000
+ flags = 0
+ data = length 405, hash 5CC7F4E7
+ sample 11:
+ time = 433000
+ flags = 0
+ data = length 4852, hash 9DB6979D
+ sample 12:
+ time = 400000
+ flags = 0
+ data = length 547, hash E31A6979
+ sample 13:
+ time = 367000
+ flags = 0
+ data = length 570, hash FEC40D00
+ sample 14:
+ time = 567000
+ flags = 0
+ data = length 5525, hash 7C478F7E
+ sample 15:
+ time = 500000
+ flags = 0
+ data = length 1082, hash DA07059A
+ sample 16:
+ time = 467000
+ flags = 0
+ data = length 807, hash 93478E6B
+ sample 17:
+ time = 533000
+ flags = 0
+ data = length 744, hash 9A8E6026
+ sample 18:
+ time = 700000
+ flags = 0
+ data = length 4732, hash C73B23C0
+ sample 19:
+ time = 633000
+ flags = 0
+ data = length 1004, hash 8A19A228
+ sample 20:
+ time = 600000
+ flags = 0
+ data = length 794, hash 8126022C
+ sample 21:
+ time = 667000
+ flags = 0
+ data = length 645, hash F08300E5
+ sample 22:
+ time = 833000
+ flags = 0
+ data = length 2684, hash 727FE378
+ sample 23:
+ time = 767000
+ flags = 0
+ data = length 787, hash 419A7821
+ sample 24:
+ time = 733000
+ flags = 0
+ data = length 649, hash 5C159346
+ sample 25:
+ time = 800000
+ flags = 0
+ data = length 509, hash F912D655
+ sample 26:
+ time = 967000
+ flags = 0
+ data = length 1226, hash 29815C21
+ sample 27:
+ time = 900000
+ flags = 0
+ data = length 898, hash D997AD0A
+ sample 28:
+ time = 867000
+ flags = 0
+ data = length 476, hash A0423645
+ sample 29:
+ time = 933000
+ flags = 0
+ data = length 486, hash DDF32CBB
+track 2:
+ total output bytes = 12120
+ sample count = 29
+ format 0:
+ id = 2
+ sampleMimeType = audio/ac3
+ channelCount = 1
+ sampleRate = 44100
+ selectionFlags = 1
+ language = und
+ sample 0:
+ time = 62000
+ flags = 1
+ data = length 416, hash 211F2286
+ sample 1:
+ time = 97000
+ flags = 1
+ data = length 418, hash 77425A86
+ sample 2:
+ time = 131000
+ flags = 1
+ data = length 418, hash A0FE5CA1
+ sample 3:
+ time = 166000
+ flags = 1
+ data = length 418, hash 2309B066
+ sample 4:
+ time = 201000
+ flags = 1
+ data = length 418, hash 928A653B
+ sample 5:
+ time = 236000
+ flags = 1
+ data = length 418, hash 3422F0CB
+ sample 6:
+ time = 270000
+ flags = 1
+ data = length 418, hash EFF43D5B
+ sample 7:
+ time = 306000
+ flags = 1
+ data = length 418, hash FC8093C7
+ sample 8:
+ time = 341000
+ flags = 1
+ data = length 418, hash CCC08A16
+ sample 9:
+ time = 376000
+ flags = 1
+ data = length 418, hash 2A6EE863
+ sample 10:
+ time = 410000
+ flags = 1
+ data = length 418, hash D69A9251
+ sample 11:
+ time = 445000
+ flags = 1
+ data = length 418, hash BCFB758D
+ sample 12:
+ time = 480000
+ flags = 1
+ data = length 418, hash 11B66799
+ sample 13:
+ time = 514000
+ flags = 1
+ data = length 418, hash C824D392
+ sample 14:
+ time = 550000
+ flags = 1
+ data = length 418, hash C167D872
+ sample 15:
+ time = 585000
+ flags = 1
+ data = length 418, hash 4221C855
+ sample 16:
+ time = 620000
+ flags = 1
+ data = length 418, hash 4D4FF934
+ sample 17:
+ time = 654000
+ flags = 1
+ data = length 418, hash 984AA025
+ sample 18:
+ time = 690000
+ flags = 1
+ data = length 418, hash BB788B46
+ sample 19:
+ time = 724000
+ flags = 1
+ data = length 418, hash 9EFBFD97
+ sample 20:
+ time = 759000
+ flags = 1
+ data = length 418, hash DF1A460C
+ sample 21:
+ time = 793000
+ flags = 1
+ data = length 418, hash 2BDB56A
+ sample 22:
+ time = 829000
+ flags = 1
+ data = length 418, hash CA230060
+ sample 23:
+ time = 864000
+ flags = 1
+ data = length 418, hash D2F19F41
+ sample 24:
+ time = 898000
+ flags = 1
+ data = length 418, hash AF392D79
+ sample 25:
+ time = 932000
+ flags = 1
+ data = length 418, hash C5D7F2A3
+ sample 26:
+ time = 968000
+ flags = 1
+ data = length 418, hash 733A35AE
+ sample 27:
+ time = 1002000
+ flags = 1
+ data = length 418, hash DE46E5D3
+ sample 28:
+ time = 1037000
+ flags = 1
+ data = length 418, hash 56AB8D37
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.2.dump b/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.2.dump
new file mode 100644
index 0000000..258efdc
--- /dev/null
+++ b/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.2.dump
@@ -0,0 +1,269 @@
+seekMap:
+ isSeekable = true
+ duration = 1071000
+ getPosition(0) = [[timeUs=0, position=994]]
+ getPosition(1) = [[timeUs=0, position=994]]
+ getPosition(535500) = [[timeUs=0, position=994]]
+ getPosition(1071000) = [[timeUs=0, position=994]]
+numberOfTracks = 2
+track 1:
+ total output bytes = 89502
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ width = 1080
+ height = 720
+ rotationDegrees = 90
+ selectionFlags = 1
+ language = und
+ initializationData:
+ data = length 30, hash F6F3D010
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 36477, hash F0F36CFE
+ sample 1:
+ time = 67000
+ flags = 0
+ data = length 5341, hash 40B85E2
+ sample 2:
+ time = 33000
+ flags = 0
+ data = length 596, hash 357B4D92
+ sample 3:
+ time = 200000
+ flags = 0
+ data = length 7704, hash A39EDA06
+ sample 4:
+ time = 133000
+ flags = 0
+ data = length 989, hash 2813C72D
+ sample 5:
+ time = 100000
+ flags = 0
+ data = length 721, hash C50D1C73
+ sample 6:
+ time = 167000
+ flags = 0
+ data = length 519, hash 65FE1911
+ sample 7:
+ time = 333000
+ flags = 0
+ data = length 6160, hash E1CAC0EC
+ sample 8:
+ time = 267000
+ flags = 0
+ data = length 953, hash 7160C661
+ sample 9:
+ time = 233000
+ flags = 0
+ data = length 620, hash 7A7AE07C
+ sample 10:
+ time = 300000
+ flags = 0
+ data = length 405, hash 5CC7F4E7
+ sample 11:
+ time = 433000
+ flags = 0
+ data = length 4852, hash 9DB6979D
+ sample 12:
+ time = 400000
+ flags = 0
+ data = length 547, hash E31A6979
+ sample 13:
+ time = 367000
+ flags = 0
+ data = length 570, hash FEC40D00
+ sample 14:
+ time = 567000
+ flags = 0
+ data = length 5525, hash 7C478F7E
+ sample 15:
+ time = 500000
+ flags = 0
+ data = length 1082, hash DA07059A
+ sample 16:
+ time = 467000
+ flags = 0
+ data = length 807, hash 93478E6B
+ sample 17:
+ time = 533000
+ flags = 0
+ data = length 744, hash 9A8E6026
+ sample 18:
+ time = 700000
+ flags = 0
+ data = length 4732, hash C73B23C0
+ sample 19:
+ time = 633000
+ flags = 0
+ data = length 1004, hash 8A19A228
+ sample 20:
+ time = 600000
+ flags = 0
+ data = length 794, hash 8126022C
+ sample 21:
+ time = 667000
+ flags = 0
+ data = length 645, hash F08300E5
+ sample 22:
+ time = 833000
+ flags = 0
+ data = length 2684, hash 727FE378
+ sample 23:
+ time = 767000
+ flags = 0
+ data = length 787, hash 419A7821
+ sample 24:
+ time = 733000
+ flags = 0
+ data = length 649, hash 5C159346
+ sample 25:
+ time = 800000
+ flags = 0
+ data = length 509, hash F912D655
+ sample 26:
+ time = 967000
+ flags = 0
+ data = length 1226, hash 29815C21
+ sample 27:
+ time = 900000
+ flags = 0
+ data = length 898, hash D997AD0A
+ sample 28:
+ time = 867000
+ flags = 0
+ data = length 476, hash A0423645
+ sample 29:
+ time = 933000
+ flags = 0
+ data = length 486, hash DDF32CBB
+track 2:
+ total output bytes = 12120
+ sample count = 29
+ format 0:
+ id = 2
+ sampleMimeType = audio/ac3
+ channelCount = 1
+ sampleRate = 44100
+ selectionFlags = 1
+ language = und
+ sample 0:
+ time = 62000
+ flags = 1
+ data = length 416, hash 211F2286
+ sample 1:
+ time = 97000
+ flags = 1
+ data = length 418, hash 77425A86
+ sample 2:
+ time = 131000
+ flags = 1
+ data = length 418, hash A0FE5CA1
+ sample 3:
+ time = 166000
+ flags = 1
+ data = length 418, hash 2309B066
+ sample 4:
+ time = 201000
+ flags = 1
+ data = length 418, hash 928A653B
+ sample 5:
+ time = 236000
+ flags = 1
+ data = length 418, hash 3422F0CB
+ sample 6:
+ time = 270000
+ flags = 1
+ data = length 418, hash EFF43D5B
+ sample 7:
+ time = 306000
+ flags = 1
+ data = length 418, hash FC8093C7
+ sample 8:
+ time = 341000
+ flags = 1
+ data = length 418, hash CCC08A16
+ sample 9:
+ time = 376000
+ flags = 1
+ data = length 418, hash 2A6EE863
+ sample 10:
+ time = 410000
+ flags = 1
+ data = length 418, hash D69A9251
+ sample 11:
+ time = 445000
+ flags = 1
+ data = length 418, hash BCFB758D
+ sample 12:
+ time = 480000
+ flags = 1
+ data = length 418, hash 11B66799
+ sample 13:
+ time = 514000
+ flags = 1
+ data = length 418, hash C824D392
+ sample 14:
+ time = 550000
+ flags = 1
+ data = length 418, hash C167D872
+ sample 15:
+ time = 585000
+ flags = 1
+ data = length 418, hash 4221C855
+ sample 16:
+ time = 620000
+ flags = 1
+ data = length 418, hash 4D4FF934
+ sample 17:
+ time = 654000
+ flags = 1
+ data = length 418, hash 984AA025
+ sample 18:
+ time = 690000
+ flags = 1
+ data = length 418, hash BB788B46
+ sample 19:
+ time = 724000
+ flags = 1
+ data = length 418, hash 9EFBFD97
+ sample 20:
+ time = 759000
+ flags = 1
+ data = length 418, hash DF1A460C
+ sample 21:
+ time = 793000
+ flags = 1
+ data = length 418, hash 2BDB56A
+ sample 22:
+ time = 829000
+ flags = 1
+ data = length 418, hash CA230060
+ sample 23:
+ time = 864000
+ flags = 1
+ data = length 418, hash D2F19F41
+ sample 24:
+ time = 898000
+ flags = 1
+ data = length 418, hash AF392D79
+ sample 25:
+ time = 932000
+ flags = 1
+ data = length 418, hash C5D7F2A3
+ sample 26:
+ time = 968000
+ flags = 1
+ data = length 418, hash 733A35AE
+ sample 27:
+ time = 1002000
+ flags = 1
+ data = length 418, hash DE46E5D3
+ sample 28:
+ time = 1037000
+ flags = 1
+ data = length 418, hash 56AB8D37
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.3.dump b/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.3.dump
new file mode 100644
index 0000000..258efdc
--- /dev/null
+++ b/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.3.dump
@@ -0,0 +1,269 @@
+seekMap:
+ isSeekable = true
+ duration = 1071000
+ getPosition(0) = [[timeUs=0, position=994]]
+ getPosition(1) = [[timeUs=0, position=994]]
+ getPosition(535500) = [[timeUs=0, position=994]]
+ getPosition(1071000) = [[timeUs=0, position=994]]
+numberOfTracks = 2
+track 1:
+ total output bytes = 89502
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ width = 1080
+ height = 720
+ rotationDegrees = 90
+ selectionFlags = 1
+ language = und
+ initializationData:
+ data = length 30, hash F6F3D010
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 36477, hash F0F36CFE
+ sample 1:
+ time = 67000
+ flags = 0
+ data = length 5341, hash 40B85E2
+ sample 2:
+ time = 33000
+ flags = 0
+ data = length 596, hash 357B4D92
+ sample 3:
+ time = 200000
+ flags = 0
+ data = length 7704, hash A39EDA06
+ sample 4:
+ time = 133000
+ flags = 0
+ data = length 989, hash 2813C72D
+ sample 5:
+ time = 100000
+ flags = 0
+ data = length 721, hash C50D1C73
+ sample 6:
+ time = 167000
+ flags = 0
+ data = length 519, hash 65FE1911
+ sample 7:
+ time = 333000
+ flags = 0
+ data = length 6160, hash E1CAC0EC
+ sample 8:
+ time = 267000
+ flags = 0
+ data = length 953, hash 7160C661
+ sample 9:
+ time = 233000
+ flags = 0
+ data = length 620, hash 7A7AE07C
+ sample 10:
+ time = 300000
+ flags = 0
+ data = length 405, hash 5CC7F4E7
+ sample 11:
+ time = 433000
+ flags = 0
+ data = length 4852, hash 9DB6979D
+ sample 12:
+ time = 400000
+ flags = 0
+ data = length 547, hash E31A6979
+ sample 13:
+ time = 367000
+ flags = 0
+ data = length 570, hash FEC40D00
+ sample 14:
+ time = 567000
+ flags = 0
+ data = length 5525, hash 7C478F7E
+ sample 15:
+ time = 500000
+ flags = 0
+ data = length 1082, hash DA07059A
+ sample 16:
+ time = 467000
+ flags = 0
+ data = length 807, hash 93478E6B
+ sample 17:
+ time = 533000
+ flags = 0
+ data = length 744, hash 9A8E6026
+ sample 18:
+ time = 700000
+ flags = 0
+ data = length 4732, hash C73B23C0
+ sample 19:
+ time = 633000
+ flags = 0
+ data = length 1004, hash 8A19A228
+ sample 20:
+ time = 600000
+ flags = 0
+ data = length 794, hash 8126022C
+ sample 21:
+ time = 667000
+ flags = 0
+ data = length 645, hash F08300E5
+ sample 22:
+ time = 833000
+ flags = 0
+ data = length 2684, hash 727FE378
+ sample 23:
+ time = 767000
+ flags = 0
+ data = length 787, hash 419A7821
+ sample 24:
+ time = 733000
+ flags = 0
+ data = length 649, hash 5C159346
+ sample 25:
+ time = 800000
+ flags = 0
+ data = length 509, hash F912D655
+ sample 26:
+ time = 967000
+ flags = 0
+ data = length 1226, hash 29815C21
+ sample 27:
+ time = 900000
+ flags = 0
+ data = length 898, hash D997AD0A
+ sample 28:
+ time = 867000
+ flags = 0
+ data = length 476, hash A0423645
+ sample 29:
+ time = 933000
+ flags = 0
+ data = length 486, hash DDF32CBB
+track 2:
+ total output bytes = 12120
+ sample count = 29
+ format 0:
+ id = 2
+ sampleMimeType = audio/ac3
+ channelCount = 1
+ sampleRate = 44100
+ selectionFlags = 1
+ language = und
+ sample 0:
+ time = 62000
+ flags = 1
+ data = length 416, hash 211F2286
+ sample 1:
+ time = 97000
+ flags = 1
+ data = length 418, hash 77425A86
+ sample 2:
+ time = 131000
+ flags = 1
+ data = length 418, hash A0FE5CA1
+ sample 3:
+ time = 166000
+ flags = 1
+ data = length 418, hash 2309B066
+ sample 4:
+ time = 201000
+ flags = 1
+ data = length 418, hash 928A653B
+ sample 5:
+ time = 236000
+ flags = 1
+ data = length 418, hash 3422F0CB
+ sample 6:
+ time = 270000
+ flags = 1
+ data = length 418, hash EFF43D5B
+ sample 7:
+ time = 306000
+ flags = 1
+ data = length 418, hash FC8093C7
+ sample 8:
+ time = 341000
+ flags = 1
+ data = length 418, hash CCC08A16
+ sample 9:
+ time = 376000
+ flags = 1
+ data = length 418, hash 2A6EE863
+ sample 10:
+ time = 410000
+ flags = 1
+ data = length 418, hash D69A9251
+ sample 11:
+ time = 445000
+ flags = 1
+ data = length 418, hash BCFB758D
+ sample 12:
+ time = 480000
+ flags = 1
+ data = length 418, hash 11B66799
+ sample 13:
+ time = 514000
+ flags = 1
+ data = length 418, hash C824D392
+ sample 14:
+ time = 550000
+ flags = 1
+ data = length 418, hash C167D872
+ sample 15:
+ time = 585000
+ flags = 1
+ data = length 418, hash 4221C855
+ sample 16:
+ time = 620000
+ flags = 1
+ data = length 418, hash 4D4FF934
+ sample 17:
+ time = 654000
+ flags = 1
+ data = length 418, hash 984AA025
+ sample 18:
+ time = 690000
+ flags = 1
+ data = length 418, hash BB788B46
+ sample 19:
+ time = 724000
+ flags = 1
+ data = length 418, hash 9EFBFD97
+ sample 20:
+ time = 759000
+ flags = 1
+ data = length 418, hash DF1A460C
+ sample 21:
+ time = 793000
+ flags = 1
+ data = length 418, hash 2BDB56A
+ sample 22:
+ time = 829000
+ flags = 1
+ data = length 418, hash CA230060
+ sample 23:
+ time = 864000
+ flags = 1
+ data = length 418, hash D2F19F41
+ sample 24:
+ time = 898000
+ flags = 1
+ data = length 418, hash AF392D79
+ sample 25:
+ time = 932000
+ flags = 1
+ data = length 418, hash C5D7F2A3
+ sample 26:
+ time = 968000
+ flags = 1
+ data = length 418, hash 733A35AE
+ sample 27:
+ time = 1002000
+ flags = 1
+ data = length 418, hash DE46E5D3
+ sample 28:
+ time = 1037000
+ flags = 1
+ data = length 418, hash 56AB8D37
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.unknown_length.dump b/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.unknown_length.dump
new file mode 100644
index 0000000..258efdc
--- /dev/null
+++ b/tree/testdata/src/test/assets/mkv/sample_with_htc_rotation_track_name.mkv.unknown_length.dump
@@ -0,0 +1,269 @@
+seekMap:
+ isSeekable = true
+ duration = 1071000
+ getPosition(0) = [[timeUs=0, position=994]]
+ getPosition(1) = [[timeUs=0, position=994]]
+ getPosition(535500) = [[timeUs=0, position=994]]
+ getPosition(1071000) = [[timeUs=0, position=994]]
+numberOfTracks = 2
+track 1:
+ total output bytes = 89502
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ width = 1080
+ height = 720
+ rotationDegrees = 90
+ selectionFlags = 1
+ language = und
+ initializationData:
+ data = length 30, hash F6F3D010
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 36477, hash F0F36CFE
+ sample 1:
+ time = 67000
+ flags = 0
+ data = length 5341, hash 40B85E2
+ sample 2:
+ time = 33000
+ flags = 0
+ data = length 596, hash 357B4D92
+ sample 3:
+ time = 200000
+ flags = 0
+ data = length 7704, hash A39EDA06
+ sample 4:
+ time = 133000
+ flags = 0
+ data = length 989, hash 2813C72D
+ sample 5:
+ time = 100000
+ flags = 0
+ data = length 721, hash C50D1C73
+ sample 6:
+ time = 167000
+ flags = 0
+ data = length 519, hash 65FE1911
+ sample 7:
+ time = 333000
+ flags = 0
+ data = length 6160, hash E1CAC0EC
+ sample 8:
+ time = 267000
+ flags = 0
+ data = length 953, hash 7160C661
+ sample 9:
+ time = 233000
+ flags = 0
+ data = length 620, hash 7A7AE07C
+ sample 10:
+ time = 300000
+ flags = 0
+ data = length 405, hash 5CC7F4E7
+ sample 11:
+ time = 433000
+ flags = 0
+ data = length 4852, hash 9DB6979D
+ sample 12:
+ time = 400000
+ flags = 0
+ data = length 547, hash E31A6979
+ sample 13:
+ time = 367000
+ flags = 0
+ data = length 570, hash FEC40D00
+ sample 14:
+ time = 567000
+ flags = 0
+ data = length 5525, hash 7C478F7E
+ sample 15:
+ time = 500000
+ flags = 0
+ data = length 1082, hash DA07059A
+ sample 16:
+ time = 467000
+ flags = 0
+ data = length 807, hash 93478E6B
+ sample 17:
+ time = 533000
+ flags = 0
+ data = length 744, hash 9A8E6026
+ sample 18:
+ time = 700000
+ flags = 0
+ data = length 4732, hash C73B23C0
+ sample 19:
+ time = 633000
+ flags = 0
+ data = length 1004, hash 8A19A228
+ sample 20:
+ time = 600000
+ flags = 0
+ data = length 794, hash 8126022C
+ sample 21:
+ time = 667000
+ flags = 0
+ data = length 645, hash F08300E5
+ sample 22:
+ time = 833000
+ flags = 0
+ data = length 2684, hash 727FE378
+ sample 23:
+ time = 767000
+ flags = 0
+ data = length 787, hash 419A7821
+ sample 24:
+ time = 733000
+ flags = 0
+ data = length 649, hash 5C159346
+ sample 25:
+ time = 800000
+ flags = 0
+ data = length 509, hash F912D655
+ sample 26:
+ time = 967000
+ flags = 0
+ data = length 1226, hash 29815C21
+ sample 27:
+ time = 900000
+ flags = 0
+ data = length 898, hash D997AD0A
+ sample 28:
+ time = 867000
+ flags = 0
+ data = length 476, hash A0423645
+ sample 29:
+ time = 933000
+ flags = 0
+ data = length 486, hash DDF32CBB
+track 2:
+ total output bytes = 12120
+ sample count = 29
+ format 0:
+ id = 2
+ sampleMimeType = audio/ac3
+ channelCount = 1
+ sampleRate = 44100
+ selectionFlags = 1
+ language = und
+ sample 0:
+ time = 62000
+ flags = 1
+ data = length 416, hash 211F2286
+ sample 1:
+ time = 97000
+ flags = 1
+ data = length 418, hash 77425A86
+ sample 2:
+ time = 131000
+ flags = 1
+ data = length 418, hash A0FE5CA1
+ sample 3:
+ time = 166000
+ flags = 1
+ data = length 418, hash 2309B066
+ sample 4:
+ time = 201000
+ flags = 1
+ data = length 418, hash 928A653B
+ sample 5:
+ time = 236000
+ flags = 1
+ data = length 418, hash 3422F0CB
+ sample 6:
+ time = 270000
+ flags = 1
+ data = length 418, hash EFF43D5B
+ sample 7:
+ time = 306000
+ flags = 1
+ data = length 418, hash FC8093C7
+ sample 8:
+ time = 341000
+ flags = 1
+ data = length 418, hash CCC08A16
+ sample 9:
+ time = 376000
+ flags = 1
+ data = length 418, hash 2A6EE863
+ sample 10:
+ time = 410000
+ flags = 1
+ data = length 418, hash D69A9251
+ sample 11:
+ time = 445000
+ flags = 1
+ data = length 418, hash BCFB758D
+ sample 12:
+ time = 480000
+ flags = 1
+ data = length 418, hash 11B66799
+ sample 13:
+ time = 514000
+ flags = 1
+ data = length 418, hash C824D392
+ sample 14:
+ time = 550000
+ flags = 1
+ data = length 418, hash C167D872
+ sample 15:
+ time = 585000
+ flags = 1
+ data = length 418, hash 4221C855
+ sample 16:
+ time = 620000
+ flags = 1
+ data = length 418, hash 4D4FF934
+ sample 17:
+ time = 654000
+ flags = 1
+ data = length 418, hash 984AA025
+ sample 18:
+ time = 690000
+ flags = 1
+ data = length 418, hash BB788B46
+ sample 19:
+ time = 724000
+ flags = 1
+ data = length 418, hash 9EFBFD97
+ sample 20:
+ time = 759000
+ flags = 1
+ data = length 418, hash DF1A460C
+ sample 21:
+ time = 793000
+ flags = 1
+ data = length 418, hash 2BDB56A
+ sample 22:
+ time = 829000
+ flags = 1
+ data = length 418, hash CA230060
+ sample 23:
+ time = 864000
+ flags = 1
+ data = length 418, hash D2F19F41
+ sample 24:
+ time = 898000
+ flags = 1
+ data = length 418, hash AF392D79
+ sample 25:
+ time = 932000
+ flags = 1
+ data = length 418, hash C5D7F2A3
+ sample 26:
+ time = 968000
+ flags = 1
+ data = length 418, hash 733A35AE
+ sample 27:
+ time = 1002000
+ flags = 1
+ data = length 418, hash DE46E5D3
+ sample 28:
+ time = 1037000
+ flags = 1
+ data = length 418, hash 56AB8D37
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv b/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv
new file mode 100644
index 0000000..bdd56d8
--- /dev/null
+++ b/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv
Binary files differ
diff --git a/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.0.dump b/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.0.dump
new file mode 100644
index 0000000..1b6f763
--- /dev/null
+++ b/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.0.dump
@@ -0,0 +1,280 @@
+seekMap:
+ isSeekable = true
+ duration = 1234000
+ getPosition(0) = [[timeUs=0, position=1163]]
+ getPosition(1) = [[timeUs=0, position=1163]]
+ getPosition(617000) = [[timeUs=0, position=1163]]
+ getPosition(1234000) = [[timeUs=0, position=1163]]
+numberOfTracks = 3
+track 1:
+ total output bytes = 89502
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ width = 1080
+ height = 720
+ selectionFlags = 1
+ language = und
+ initializationData:
+ data = length 30, hash F6F3D010
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 36477, hash F0F36CFE
+ sample 1:
+ time = 67000
+ flags = 0
+ data = length 5341, hash 40B85E2
+ sample 2:
+ time = 33000
+ flags = 0
+ data = length 596, hash 357B4D92
+ sample 3:
+ time = 200000
+ flags = 0
+ data = length 7704, hash A39EDA06
+ sample 4:
+ time = 133000
+ flags = 0
+ data = length 989, hash 2813C72D
+ sample 5:
+ time = 100000
+ flags = 0
+ data = length 721, hash C50D1C73
+ sample 6:
+ time = 167000
+ flags = 0
+ data = length 519, hash 65FE1911
+ sample 7:
+ time = 333000
+ flags = 0
+ data = length 6160, hash E1CAC0EC
+ sample 8:
+ time = 267000
+ flags = 0
+ data = length 953, hash 7160C661
+ sample 9:
+ time = 233000
+ flags = 0
+ data = length 620, hash 7A7AE07C
+ sample 10:
+ time = 300000
+ flags = 0
+ data = length 405, hash 5CC7F4E7
+ sample 11:
+ time = 433000
+ flags = 0
+ data = length 4852, hash 9DB6979D
+ sample 12:
+ time = 400000
+ flags = 0
+ data = length 547, hash E31A6979
+ sample 13:
+ time = 367000
+ flags = 0
+ data = length 570, hash FEC40D00
+ sample 14:
+ time = 567000
+ flags = 0
+ data = length 5525, hash 7C478F7E
+ sample 15:
+ time = 500000
+ flags = 0
+ data = length 1082, hash DA07059A
+ sample 16:
+ time = 467000
+ flags = 0
+ data = length 807, hash 93478E6B
+ sample 17:
+ time = 533000
+ flags = 0
+ data = length 744, hash 9A8E6026
+ sample 18:
+ time = 700000
+ flags = 0
+ data = length 4732, hash C73B23C0
+ sample 19:
+ time = 633000
+ flags = 0
+ data = length 1004, hash 8A19A228
+ sample 20:
+ time = 600000
+ flags = 0
+ data = length 794, hash 8126022C
+ sample 21:
+ time = 667000
+ flags = 0
+ data = length 645, hash F08300E5
+ sample 22:
+ time = 833000
+ flags = 0
+ data = length 2684, hash 727FE378
+ sample 23:
+ time = 767000
+ flags = 0
+ data = length 787, hash 419A7821
+ sample 24:
+ time = 733000
+ flags = 0
+ data = length 649, hash 5C159346
+ sample 25:
+ time = 800000
+ flags = 0
+ data = length 509, hash F912D655
+ sample 26:
+ time = 967000
+ flags = 0
+ data = length 1226, hash 29815C21
+ sample 27:
+ time = 900000
+ flags = 0
+ data = length 898, hash D997AD0A
+ sample 28:
+ time = 867000
+ flags = 0
+ data = length 476, hash A0423645
+ sample 29:
+ time = 933000
+ flags = 0
+ data = length 486, hash DDF32CBB
+track 2:
+ total output bytes = 12120
+ sample count = 29
+ format 0:
+ id = 2
+ sampleMimeType = audio/ac3
+ channelCount = 1
+ sampleRate = 44100
+ selectionFlags = 1
+ language = und
+ sample 0:
+ time = 62000
+ flags = 1
+ data = length 416, hash 211F2286
+ sample 1:
+ time = 97000
+ flags = 1
+ data = length 418, hash 77425A86
+ sample 2:
+ time = 131000
+ flags = 1
+ data = length 418, hash A0FE5CA1
+ sample 3:
+ time = 166000
+ flags = 1
+ data = length 418, hash 2309B066
+ sample 4:
+ time = 201000
+ flags = 1
+ data = length 418, hash 928A653B
+ sample 5:
+ time = 236000
+ flags = 1
+ data = length 418, hash 3422F0CB
+ sample 6:
+ time = 270000
+ flags = 1
+ data = length 418, hash EFF43D5B
+ sample 7:
+ time = 306000
+ flags = 1
+ data = length 418, hash FC8093C7
+ sample 8:
+ time = 341000
+ flags = 1
+ data = length 418, hash CCC08A16
+ sample 9:
+ time = 376000
+ flags = 1
+ data = length 418, hash 2A6EE863
+ sample 10:
+ time = 410000
+ flags = 1
+ data = length 418, hash D69A9251
+ sample 11:
+ time = 445000
+ flags = 1
+ data = length 418, hash BCFB758D
+ sample 12:
+ time = 480000
+ flags = 1
+ data = length 418, hash 11B66799
+ sample 13:
+ time = 514000
+ flags = 1
+ data = length 418, hash C824D392
+ sample 14:
+ time = 550000
+ flags = 1
+ data = length 418, hash C167D872
+ sample 15:
+ time = 585000
+ flags = 1
+ data = length 418, hash 4221C855
+ sample 16:
+ time = 620000
+ flags = 1
+ data = length 418, hash 4D4FF934
+ sample 17:
+ time = 654000
+ flags = 1
+ data = length 418, hash 984AA025
+ sample 18:
+ time = 690000
+ flags = 1
+ data = length 418, hash BB788B46
+ sample 19:
+ time = 724000
+ flags = 1
+ data = length 418, hash 9EFBFD97
+ sample 20:
+ time = 759000
+ flags = 1
+ data = length 418, hash DF1A460C
+ sample 21:
+ time = 793000
+ flags = 1
+ data = length 418, hash 2BDB56A
+ sample 22:
+ time = 829000
+ flags = 1
+ data = length 418, hash CA230060
+ sample 23:
+ time = 864000
+ flags = 1
+ data = length 418, hash D2F19F41
+ sample 24:
+ time = 898000
+ flags = 1
+ data = length 418, hash AF392D79
+ sample 25:
+ time = 932000
+ flags = 1
+ data = length 418, hash C5D7F2A3
+ sample 26:
+ time = 968000
+ flags = 1
+ data = length 418, hash 733A35AE
+ sample 27:
+ time = 1002000
+ flags = 1
+ data = length 418, hash DE46E5D3
+ sample 28:
+ time = 1037000
+ flags = 1
+ data = length 418, hash 56AB8D37
+track 3:
+ total output bytes = 59
+ sample count = 1
+ format 0:
+ id = 3
+ sampleMimeType = application/x-subrip
+ language = en
+ label = Subs Label
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 59, hash 1AD38625
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.1.dump b/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.1.dump
new file mode 100644
index 0000000..1b6f763
--- /dev/null
+++ b/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.1.dump
@@ -0,0 +1,280 @@
+seekMap:
+ isSeekable = true
+ duration = 1234000
+ getPosition(0) = [[timeUs=0, position=1163]]
+ getPosition(1) = [[timeUs=0, position=1163]]
+ getPosition(617000) = [[timeUs=0, position=1163]]
+ getPosition(1234000) = [[timeUs=0, position=1163]]
+numberOfTracks = 3
+track 1:
+ total output bytes = 89502
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ width = 1080
+ height = 720
+ selectionFlags = 1
+ language = und
+ initializationData:
+ data = length 30, hash F6F3D010
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 36477, hash F0F36CFE
+ sample 1:
+ time = 67000
+ flags = 0
+ data = length 5341, hash 40B85E2
+ sample 2:
+ time = 33000
+ flags = 0
+ data = length 596, hash 357B4D92
+ sample 3:
+ time = 200000
+ flags = 0
+ data = length 7704, hash A39EDA06
+ sample 4:
+ time = 133000
+ flags = 0
+ data = length 989, hash 2813C72D
+ sample 5:
+ time = 100000
+ flags = 0
+ data = length 721, hash C50D1C73
+ sample 6:
+ time = 167000
+ flags = 0
+ data = length 519, hash 65FE1911
+ sample 7:
+ time = 333000
+ flags = 0
+ data = length 6160, hash E1CAC0EC
+ sample 8:
+ time = 267000
+ flags = 0
+ data = length 953, hash 7160C661
+ sample 9:
+ time = 233000
+ flags = 0
+ data = length 620, hash 7A7AE07C
+ sample 10:
+ time = 300000
+ flags = 0
+ data = length 405, hash 5CC7F4E7
+ sample 11:
+ time = 433000
+ flags = 0
+ data = length 4852, hash 9DB6979D
+ sample 12:
+ time = 400000
+ flags = 0
+ data = length 547, hash E31A6979
+ sample 13:
+ time = 367000
+ flags = 0
+ data = length 570, hash FEC40D00
+ sample 14:
+ time = 567000
+ flags = 0
+ data = length 5525, hash 7C478F7E
+ sample 15:
+ time = 500000
+ flags = 0
+ data = length 1082, hash DA07059A
+ sample 16:
+ time = 467000
+ flags = 0
+ data = length 807, hash 93478E6B
+ sample 17:
+ time = 533000
+ flags = 0
+ data = length 744, hash 9A8E6026
+ sample 18:
+ time = 700000
+ flags = 0
+ data = length 4732, hash C73B23C0
+ sample 19:
+ time = 633000
+ flags = 0
+ data = length 1004, hash 8A19A228
+ sample 20:
+ time = 600000
+ flags = 0
+ data = length 794, hash 8126022C
+ sample 21:
+ time = 667000
+ flags = 0
+ data = length 645, hash F08300E5
+ sample 22:
+ time = 833000
+ flags = 0
+ data = length 2684, hash 727FE378
+ sample 23:
+ time = 767000
+ flags = 0
+ data = length 787, hash 419A7821
+ sample 24:
+ time = 733000
+ flags = 0
+ data = length 649, hash 5C159346
+ sample 25:
+ time = 800000
+ flags = 0
+ data = length 509, hash F912D655
+ sample 26:
+ time = 967000
+ flags = 0
+ data = length 1226, hash 29815C21
+ sample 27:
+ time = 900000
+ flags = 0
+ data = length 898, hash D997AD0A
+ sample 28:
+ time = 867000
+ flags = 0
+ data = length 476, hash A0423645
+ sample 29:
+ time = 933000
+ flags = 0
+ data = length 486, hash DDF32CBB
+track 2:
+ total output bytes = 12120
+ sample count = 29
+ format 0:
+ id = 2
+ sampleMimeType = audio/ac3
+ channelCount = 1
+ sampleRate = 44100
+ selectionFlags = 1
+ language = und
+ sample 0:
+ time = 62000
+ flags = 1
+ data = length 416, hash 211F2286
+ sample 1:
+ time = 97000
+ flags = 1
+ data = length 418, hash 77425A86
+ sample 2:
+ time = 131000
+ flags = 1
+ data = length 418, hash A0FE5CA1
+ sample 3:
+ time = 166000
+ flags = 1
+ data = length 418, hash 2309B066
+ sample 4:
+ time = 201000
+ flags = 1
+ data = length 418, hash 928A653B
+ sample 5:
+ time = 236000
+ flags = 1
+ data = length 418, hash 3422F0CB
+ sample 6:
+ time = 270000
+ flags = 1
+ data = length 418, hash EFF43D5B
+ sample 7:
+ time = 306000
+ flags = 1
+ data = length 418, hash FC8093C7
+ sample 8:
+ time = 341000
+ flags = 1
+ data = length 418, hash CCC08A16
+ sample 9:
+ time = 376000
+ flags = 1
+ data = length 418, hash 2A6EE863
+ sample 10:
+ time = 410000
+ flags = 1
+ data = length 418, hash D69A9251
+ sample 11:
+ time = 445000
+ flags = 1
+ data = length 418, hash BCFB758D
+ sample 12:
+ time = 480000
+ flags = 1
+ data = length 418, hash 11B66799
+ sample 13:
+ time = 514000
+ flags = 1
+ data = length 418, hash C824D392
+ sample 14:
+ time = 550000
+ flags = 1
+ data = length 418, hash C167D872
+ sample 15:
+ time = 585000
+ flags = 1
+ data = length 418, hash 4221C855
+ sample 16:
+ time = 620000
+ flags = 1
+ data = length 418, hash 4D4FF934
+ sample 17:
+ time = 654000
+ flags = 1
+ data = length 418, hash 984AA025
+ sample 18:
+ time = 690000
+ flags = 1
+ data = length 418, hash BB788B46
+ sample 19:
+ time = 724000
+ flags = 1
+ data = length 418, hash 9EFBFD97
+ sample 20:
+ time = 759000
+ flags = 1
+ data = length 418, hash DF1A460C
+ sample 21:
+ time = 793000
+ flags = 1
+ data = length 418, hash 2BDB56A
+ sample 22:
+ time = 829000
+ flags = 1
+ data = length 418, hash CA230060
+ sample 23:
+ time = 864000
+ flags = 1
+ data = length 418, hash D2F19F41
+ sample 24:
+ time = 898000
+ flags = 1
+ data = length 418, hash AF392D79
+ sample 25:
+ time = 932000
+ flags = 1
+ data = length 418, hash C5D7F2A3
+ sample 26:
+ time = 968000
+ flags = 1
+ data = length 418, hash 733A35AE
+ sample 27:
+ time = 1002000
+ flags = 1
+ data = length 418, hash DE46E5D3
+ sample 28:
+ time = 1037000
+ flags = 1
+ data = length 418, hash 56AB8D37
+track 3:
+ total output bytes = 59
+ sample count = 1
+ format 0:
+ id = 3
+ sampleMimeType = application/x-subrip
+ language = en
+ label = Subs Label
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 59, hash 1AD38625
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.2.dump b/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.2.dump
new file mode 100644
index 0000000..1b6f763
--- /dev/null
+++ b/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.2.dump
@@ -0,0 +1,280 @@
+seekMap:
+ isSeekable = true
+ duration = 1234000
+ getPosition(0) = [[timeUs=0, position=1163]]
+ getPosition(1) = [[timeUs=0, position=1163]]
+ getPosition(617000) = [[timeUs=0, position=1163]]
+ getPosition(1234000) = [[timeUs=0, position=1163]]
+numberOfTracks = 3
+track 1:
+ total output bytes = 89502
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ width = 1080
+ height = 720
+ selectionFlags = 1
+ language = und
+ initializationData:
+ data = length 30, hash F6F3D010
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 36477, hash F0F36CFE
+ sample 1:
+ time = 67000
+ flags = 0
+ data = length 5341, hash 40B85E2
+ sample 2:
+ time = 33000
+ flags = 0
+ data = length 596, hash 357B4D92
+ sample 3:
+ time = 200000
+ flags = 0
+ data = length 7704, hash A39EDA06
+ sample 4:
+ time = 133000
+ flags = 0
+ data = length 989, hash 2813C72D
+ sample 5:
+ time = 100000
+ flags = 0
+ data = length 721, hash C50D1C73
+ sample 6:
+ time = 167000
+ flags = 0
+ data = length 519, hash 65FE1911
+ sample 7:
+ time = 333000
+ flags = 0
+ data = length 6160, hash E1CAC0EC
+ sample 8:
+ time = 267000
+ flags = 0
+ data = length 953, hash 7160C661
+ sample 9:
+ time = 233000
+ flags = 0
+ data = length 620, hash 7A7AE07C
+ sample 10:
+ time = 300000
+ flags = 0
+ data = length 405, hash 5CC7F4E7
+ sample 11:
+ time = 433000
+ flags = 0
+ data = length 4852, hash 9DB6979D
+ sample 12:
+ time = 400000
+ flags = 0
+ data = length 547, hash E31A6979
+ sample 13:
+ time = 367000
+ flags = 0
+ data = length 570, hash FEC40D00
+ sample 14:
+ time = 567000
+ flags = 0
+ data = length 5525, hash 7C478F7E
+ sample 15:
+ time = 500000
+ flags = 0
+ data = length 1082, hash DA07059A
+ sample 16:
+ time = 467000
+ flags = 0
+ data = length 807, hash 93478E6B
+ sample 17:
+ time = 533000
+ flags = 0
+ data = length 744, hash 9A8E6026
+ sample 18:
+ time = 700000
+ flags = 0
+ data = length 4732, hash C73B23C0
+ sample 19:
+ time = 633000
+ flags = 0
+ data = length 1004, hash 8A19A228
+ sample 20:
+ time = 600000
+ flags = 0
+ data = length 794, hash 8126022C
+ sample 21:
+ time = 667000
+ flags = 0
+ data = length 645, hash F08300E5
+ sample 22:
+ time = 833000
+ flags = 0
+ data = length 2684, hash 727FE378
+ sample 23:
+ time = 767000
+ flags = 0
+ data = length 787, hash 419A7821
+ sample 24:
+ time = 733000
+ flags = 0
+ data = length 649, hash 5C159346
+ sample 25:
+ time = 800000
+ flags = 0
+ data = length 509, hash F912D655
+ sample 26:
+ time = 967000
+ flags = 0
+ data = length 1226, hash 29815C21
+ sample 27:
+ time = 900000
+ flags = 0
+ data = length 898, hash D997AD0A
+ sample 28:
+ time = 867000
+ flags = 0
+ data = length 476, hash A0423645
+ sample 29:
+ time = 933000
+ flags = 0
+ data = length 486, hash DDF32CBB
+track 2:
+ total output bytes = 12120
+ sample count = 29
+ format 0:
+ id = 2
+ sampleMimeType = audio/ac3
+ channelCount = 1
+ sampleRate = 44100
+ selectionFlags = 1
+ language = und
+ sample 0:
+ time = 62000
+ flags = 1
+ data = length 416, hash 211F2286
+ sample 1:
+ time = 97000
+ flags = 1
+ data = length 418, hash 77425A86
+ sample 2:
+ time = 131000
+ flags = 1
+ data = length 418, hash A0FE5CA1
+ sample 3:
+ time = 166000
+ flags = 1
+ data = length 418, hash 2309B066
+ sample 4:
+ time = 201000
+ flags = 1
+ data = length 418, hash 928A653B
+ sample 5:
+ time = 236000
+ flags = 1
+ data = length 418, hash 3422F0CB
+ sample 6:
+ time = 270000
+ flags = 1
+ data = length 418, hash EFF43D5B
+ sample 7:
+ time = 306000
+ flags = 1
+ data = length 418, hash FC8093C7
+ sample 8:
+ time = 341000
+ flags = 1
+ data = length 418, hash CCC08A16
+ sample 9:
+ time = 376000
+ flags = 1
+ data = length 418, hash 2A6EE863
+ sample 10:
+ time = 410000
+ flags = 1
+ data = length 418, hash D69A9251
+ sample 11:
+ time = 445000
+ flags = 1
+ data = length 418, hash BCFB758D
+ sample 12:
+ time = 480000
+ flags = 1
+ data = length 418, hash 11B66799
+ sample 13:
+ time = 514000
+ flags = 1
+ data = length 418, hash C824D392
+ sample 14:
+ time = 550000
+ flags = 1
+ data = length 418, hash C167D872
+ sample 15:
+ time = 585000
+ flags = 1
+ data = length 418, hash 4221C855
+ sample 16:
+ time = 620000
+ flags = 1
+ data = length 418, hash 4D4FF934
+ sample 17:
+ time = 654000
+ flags = 1
+ data = length 418, hash 984AA025
+ sample 18:
+ time = 690000
+ flags = 1
+ data = length 418, hash BB788B46
+ sample 19:
+ time = 724000
+ flags = 1
+ data = length 418, hash 9EFBFD97
+ sample 20:
+ time = 759000
+ flags = 1
+ data = length 418, hash DF1A460C
+ sample 21:
+ time = 793000
+ flags = 1
+ data = length 418, hash 2BDB56A
+ sample 22:
+ time = 829000
+ flags = 1
+ data = length 418, hash CA230060
+ sample 23:
+ time = 864000
+ flags = 1
+ data = length 418, hash D2F19F41
+ sample 24:
+ time = 898000
+ flags = 1
+ data = length 418, hash AF392D79
+ sample 25:
+ time = 932000
+ flags = 1
+ data = length 418, hash C5D7F2A3
+ sample 26:
+ time = 968000
+ flags = 1
+ data = length 418, hash 733A35AE
+ sample 27:
+ time = 1002000
+ flags = 1
+ data = length 418, hash DE46E5D3
+ sample 28:
+ time = 1037000
+ flags = 1
+ data = length 418, hash 56AB8D37
+track 3:
+ total output bytes = 59
+ sample count = 1
+ format 0:
+ id = 3
+ sampleMimeType = application/x-subrip
+ language = en
+ label = Subs Label
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 59, hash 1AD38625
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.3.dump b/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.3.dump
new file mode 100644
index 0000000..1b6f763
--- /dev/null
+++ b/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.3.dump
@@ -0,0 +1,280 @@
+seekMap:
+ isSeekable = true
+ duration = 1234000
+ getPosition(0) = [[timeUs=0, position=1163]]
+ getPosition(1) = [[timeUs=0, position=1163]]
+ getPosition(617000) = [[timeUs=0, position=1163]]
+ getPosition(1234000) = [[timeUs=0, position=1163]]
+numberOfTracks = 3
+track 1:
+ total output bytes = 89502
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ width = 1080
+ height = 720
+ selectionFlags = 1
+ language = und
+ initializationData:
+ data = length 30, hash F6F3D010
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 36477, hash F0F36CFE
+ sample 1:
+ time = 67000
+ flags = 0
+ data = length 5341, hash 40B85E2
+ sample 2:
+ time = 33000
+ flags = 0
+ data = length 596, hash 357B4D92
+ sample 3:
+ time = 200000
+ flags = 0
+ data = length 7704, hash A39EDA06
+ sample 4:
+ time = 133000
+ flags = 0
+ data = length 989, hash 2813C72D
+ sample 5:
+ time = 100000
+ flags = 0
+ data = length 721, hash C50D1C73
+ sample 6:
+ time = 167000
+ flags = 0
+ data = length 519, hash 65FE1911
+ sample 7:
+ time = 333000
+ flags = 0
+ data = length 6160, hash E1CAC0EC
+ sample 8:
+ time = 267000
+ flags = 0
+ data = length 953, hash 7160C661
+ sample 9:
+ time = 233000
+ flags = 0
+ data = length 620, hash 7A7AE07C
+ sample 10:
+ time = 300000
+ flags = 0
+ data = length 405, hash 5CC7F4E7
+ sample 11:
+ time = 433000
+ flags = 0
+ data = length 4852, hash 9DB6979D
+ sample 12:
+ time = 400000
+ flags = 0
+ data = length 547, hash E31A6979
+ sample 13:
+ time = 367000
+ flags = 0
+ data = length 570, hash FEC40D00
+ sample 14:
+ time = 567000
+ flags = 0
+ data = length 5525, hash 7C478F7E
+ sample 15:
+ time = 500000
+ flags = 0
+ data = length 1082, hash DA07059A
+ sample 16:
+ time = 467000
+ flags = 0
+ data = length 807, hash 93478E6B
+ sample 17:
+ time = 533000
+ flags = 0
+ data = length 744, hash 9A8E6026
+ sample 18:
+ time = 700000
+ flags = 0
+ data = length 4732, hash C73B23C0
+ sample 19:
+ time = 633000
+ flags = 0
+ data = length 1004, hash 8A19A228
+ sample 20:
+ time = 600000
+ flags = 0
+ data = length 794, hash 8126022C
+ sample 21:
+ time = 667000
+ flags = 0
+ data = length 645, hash F08300E5
+ sample 22:
+ time = 833000
+ flags = 0
+ data = length 2684, hash 727FE378
+ sample 23:
+ time = 767000
+ flags = 0
+ data = length 787, hash 419A7821
+ sample 24:
+ time = 733000
+ flags = 0
+ data = length 649, hash 5C159346
+ sample 25:
+ time = 800000
+ flags = 0
+ data = length 509, hash F912D655
+ sample 26:
+ time = 967000
+ flags = 0
+ data = length 1226, hash 29815C21
+ sample 27:
+ time = 900000
+ flags = 0
+ data = length 898, hash D997AD0A
+ sample 28:
+ time = 867000
+ flags = 0
+ data = length 476, hash A0423645
+ sample 29:
+ time = 933000
+ flags = 0
+ data = length 486, hash DDF32CBB
+track 2:
+ total output bytes = 12120
+ sample count = 29
+ format 0:
+ id = 2
+ sampleMimeType = audio/ac3
+ channelCount = 1
+ sampleRate = 44100
+ selectionFlags = 1
+ language = und
+ sample 0:
+ time = 62000
+ flags = 1
+ data = length 416, hash 211F2286
+ sample 1:
+ time = 97000
+ flags = 1
+ data = length 418, hash 77425A86
+ sample 2:
+ time = 131000
+ flags = 1
+ data = length 418, hash A0FE5CA1
+ sample 3:
+ time = 166000
+ flags = 1
+ data = length 418, hash 2309B066
+ sample 4:
+ time = 201000
+ flags = 1
+ data = length 418, hash 928A653B
+ sample 5:
+ time = 236000
+ flags = 1
+ data = length 418, hash 3422F0CB
+ sample 6:
+ time = 270000
+ flags = 1
+ data = length 418, hash EFF43D5B
+ sample 7:
+ time = 306000
+ flags = 1
+ data = length 418, hash FC8093C7
+ sample 8:
+ time = 341000
+ flags = 1
+ data = length 418, hash CCC08A16
+ sample 9:
+ time = 376000
+ flags = 1
+ data = length 418, hash 2A6EE863
+ sample 10:
+ time = 410000
+ flags = 1
+ data = length 418, hash D69A9251
+ sample 11:
+ time = 445000
+ flags = 1
+ data = length 418, hash BCFB758D
+ sample 12:
+ time = 480000
+ flags = 1
+ data = length 418, hash 11B66799
+ sample 13:
+ time = 514000
+ flags = 1
+ data = length 418, hash C824D392
+ sample 14:
+ time = 550000
+ flags = 1
+ data = length 418, hash C167D872
+ sample 15:
+ time = 585000
+ flags = 1
+ data = length 418, hash 4221C855
+ sample 16:
+ time = 620000
+ flags = 1
+ data = length 418, hash 4D4FF934
+ sample 17:
+ time = 654000
+ flags = 1
+ data = length 418, hash 984AA025
+ sample 18:
+ time = 690000
+ flags = 1
+ data = length 418, hash BB788B46
+ sample 19:
+ time = 724000
+ flags = 1
+ data = length 418, hash 9EFBFD97
+ sample 20:
+ time = 759000
+ flags = 1
+ data = length 418, hash DF1A460C
+ sample 21:
+ time = 793000
+ flags = 1
+ data = length 418, hash 2BDB56A
+ sample 22:
+ time = 829000
+ flags = 1
+ data = length 418, hash CA230060
+ sample 23:
+ time = 864000
+ flags = 1
+ data = length 418, hash D2F19F41
+ sample 24:
+ time = 898000
+ flags = 1
+ data = length 418, hash AF392D79
+ sample 25:
+ time = 932000
+ flags = 1
+ data = length 418, hash C5D7F2A3
+ sample 26:
+ time = 968000
+ flags = 1
+ data = length 418, hash 733A35AE
+ sample 27:
+ time = 1002000
+ flags = 1
+ data = length 418, hash DE46E5D3
+ sample 28:
+ time = 1037000
+ flags = 1
+ data = length 418, hash 56AB8D37
+track 3:
+ total output bytes = 59
+ sample count = 1
+ format 0:
+ id = 3
+ sampleMimeType = application/x-subrip
+ language = en
+ label = Subs Label
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 59, hash 1AD38625
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.unknown_length.dump b/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.unknown_length.dump
new file mode 100644
index 0000000..1b6f763
--- /dev/null
+++ b/tree/testdata/src/test/assets/mkv/sample_with_srt.mkv.unknown_length.dump
@@ -0,0 +1,280 @@
+seekMap:
+ isSeekable = true
+ duration = 1234000
+ getPosition(0) = [[timeUs=0, position=1163]]
+ getPosition(1) = [[timeUs=0, position=1163]]
+ getPosition(617000) = [[timeUs=0, position=1163]]
+ getPosition(1234000) = [[timeUs=0, position=1163]]
+numberOfTracks = 3
+track 1:
+ total output bytes = 89502
+ sample count = 30
+ format 0:
+ id = 1
+ sampleMimeType = video/avc
+ width = 1080
+ height = 720
+ selectionFlags = 1
+ language = und
+ initializationData:
+ data = length 30, hash F6F3D010
+ data = length 10, hash 7A0D0F2B
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 36477, hash F0F36CFE
+ sample 1:
+ time = 67000
+ flags = 0
+ data = length 5341, hash 40B85E2
+ sample 2:
+ time = 33000
+ flags = 0
+ data = length 596, hash 357B4D92
+ sample 3:
+ time = 200000
+ flags = 0
+ data = length 7704, hash A39EDA06
+ sample 4:
+ time = 133000
+ flags = 0
+ data = length 989, hash 2813C72D
+ sample 5:
+ time = 100000
+ flags = 0
+ data = length 721, hash C50D1C73
+ sample 6:
+ time = 167000
+ flags = 0
+ data = length 519, hash 65FE1911
+ sample 7:
+ time = 333000
+ flags = 0
+ data = length 6160, hash E1CAC0EC
+ sample 8:
+ time = 267000
+ flags = 0
+ data = length 953, hash 7160C661
+ sample 9:
+ time = 233000
+ flags = 0
+ data = length 620, hash 7A7AE07C
+ sample 10:
+ time = 300000
+ flags = 0
+ data = length 405, hash 5CC7F4E7
+ sample 11:
+ time = 433000
+ flags = 0
+ data = length 4852, hash 9DB6979D
+ sample 12:
+ time = 400000
+ flags = 0
+ data = length 547, hash E31A6979
+ sample 13:
+ time = 367000
+ flags = 0
+ data = length 570, hash FEC40D00
+ sample 14:
+ time = 567000
+ flags = 0
+ data = length 5525, hash 7C478F7E
+ sample 15:
+ time = 500000
+ flags = 0
+ data = length 1082, hash DA07059A
+ sample 16:
+ time = 467000
+ flags = 0
+ data = length 807, hash 93478E6B
+ sample 17:
+ time = 533000
+ flags = 0
+ data = length 744, hash 9A8E6026
+ sample 18:
+ time = 700000
+ flags = 0
+ data = length 4732, hash C73B23C0
+ sample 19:
+ time = 633000
+ flags = 0
+ data = length 1004, hash 8A19A228
+ sample 20:
+ time = 600000
+ flags = 0
+ data = length 794, hash 8126022C
+ sample 21:
+ time = 667000
+ flags = 0
+ data = length 645, hash F08300E5
+ sample 22:
+ time = 833000
+ flags = 0
+ data = length 2684, hash 727FE378
+ sample 23:
+ time = 767000
+ flags = 0
+ data = length 787, hash 419A7821
+ sample 24:
+ time = 733000
+ flags = 0
+ data = length 649, hash 5C159346
+ sample 25:
+ time = 800000
+ flags = 0
+ data = length 509, hash F912D655
+ sample 26:
+ time = 967000
+ flags = 0
+ data = length 1226, hash 29815C21
+ sample 27:
+ time = 900000
+ flags = 0
+ data = length 898, hash D997AD0A
+ sample 28:
+ time = 867000
+ flags = 0
+ data = length 476, hash A0423645
+ sample 29:
+ time = 933000
+ flags = 0
+ data = length 486, hash DDF32CBB
+track 2:
+ total output bytes = 12120
+ sample count = 29
+ format 0:
+ id = 2
+ sampleMimeType = audio/ac3
+ channelCount = 1
+ sampleRate = 44100
+ selectionFlags = 1
+ language = und
+ sample 0:
+ time = 62000
+ flags = 1
+ data = length 416, hash 211F2286
+ sample 1:
+ time = 97000
+ flags = 1
+ data = length 418, hash 77425A86
+ sample 2:
+ time = 131000
+ flags = 1
+ data = length 418, hash A0FE5CA1
+ sample 3:
+ time = 166000
+ flags = 1
+ data = length 418, hash 2309B066
+ sample 4:
+ time = 201000
+ flags = 1
+ data = length 418, hash 928A653B
+ sample 5:
+ time = 236000
+ flags = 1
+ data = length 418, hash 3422F0CB
+ sample 6:
+ time = 270000
+ flags = 1
+ data = length 418, hash EFF43D5B
+ sample 7:
+ time = 306000
+ flags = 1
+ data = length 418, hash FC8093C7
+ sample 8:
+ time = 341000
+ flags = 1
+ data = length 418, hash CCC08A16
+ sample 9:
+ time = 376000
+ flags = 1
+ data = length 418, hash 2A6EE863
+ sample 10:
+ time = 410000
+ flags = 1
+ data = length 418, hash D69A9251
+ sample 11:
+ time = 445000
+ flags = 1
+ data = length 418, hash BCFB758D
+ sample 12:
+ time = 480000
+ flags = 1
+ data = length 418, hash 11B66799
+ sample 13:
+ time = 514000
+ flags = 1
+ data = length 418, hash C824D392
+ sample 14:
+ time = 550000
+ flags = 1
+ data = length 418, hash C167D872
+ sample 15:
+ time = 585000
+ flags = 1
+ data = length 418, hash 4221C855
+ sample 16:
+ time = 620000
+ flags = 1
+ data = length 418, hash 4D4FF934
+ sample 17:
+ time = 654000
+ flags = 1
+ data = length 418, hash 984AA025
+ sample 18:
+ time = 690000
+ flags = 1
+ data = length 418, hash BB788B46
+ sample 19:
+ time = 724000
+ flags = 1
+ data = length 418, hash 9EFBFD97
+ sample 20:
+ time = 759000
+ flags = 1
+ data = length 418, hash DF1A460C
+ sample 21:
+ time = 793000
+ flags = 1
+ data = length 418, hash 2BDB56A
+ sample 22:
+ time = 829000
+ flags = 1
+ data = length 418, hash CA230060
+ sample 23:
+ time = 864000
+ flags = 1
+ data = length 418, hash D2F19F41
+ sample 24:
+ time = 898000
+ flags = 1
+ data = length 418, hash AF392D79
+ sample 25:
+ time = 932000
+ flags = 1
+ data = length 418, hash C5D7F2A3
+ sample 26:
+ time = 968000
+ flags = 1
+ data = length 418, hash 733A35AE
+ sample 27:
+ time = 1002000
+ flags = 1
+ data = length 418, hash DE46E5D3
+ sample 28:
+ time = 1037000
+ flags = 1
+ data = length 418, hash 56AB8D37
+track 3:
+ total output bytes = 59
+ sample count = 1
+ format 0:
+ id = 3
+ sampleMimeType = application/x-subrip
+ language = en
+ label = Subs Label
+ sample 0:
+ time = 0
+ flags = 1
+ data = length 59, hash 1AD38625
+tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump b/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump
index b2c54a9..8868166 100644
--- a/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.0.dump
@@ -68,7 +68,7 @@
flags = 1
data = length 520, hash FEE56928
sample 13:
- time = 520000
+ time = 519999
flags = 1
data = length 599, hash 41F496C5
sample 14:
diff --git a/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump b/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump
index f8d4804..e23156e 100644
--- a/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.1.dump
@@ -44,7 +44,7 @@
flags = 1
data = length 520, hash FEE56928
sample 7:
- time = 520000
+ time = 519999
flags = 1
data = length 599, hash 41F496C5
sample 8:
diff --git a/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump b/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump
index 4375173..82a11a4 100644
--- a/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.2.dump
@@ -20,7 +20,7 @@
flags = 1
data = length 520, hash FEE56928
sample 1:
- time = 520000
+ time = 519999
flags = 1
data = length 599, hash 41F496C5
sample 2:
diff --git a/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.unknown_length.dump b/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.unknown_length.dump
index b2c54a9..8868166 100644
--- a/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.unknown_length.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_ac4_fragmented.mp4.unknown_length.dump
@@ -68,7 +68,7 @@
flags = 1
data = length 520, hash FEE56928
sample 13:
- time = 520000
+ time = 519999
flags = 1
data = length 599, hash 41F496C5
sample 14:
diff --git a/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump b/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump
index 425e0f6..0d0b831 100644
--- a/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.0.dump
@@ -95,7 +95,7 @@
crypto mode = 1
encryption key = length 16, hash 9FDDEA52
sample 13:
- time = 520000
+ time = 519999
flags = 1073741825
data = length 616, hash 3F657E23
crypto mode = 1
diff --git a/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump b/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump
index 64935a8..aeffcab 100644
--- a/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.1.dump
@@ -59,7 +59,7 @@
crypto mode = 1
encryption key = length 16, hash 9FDDEA52
sample 7:
- time = 520000
+ time = 519999
flags = 1073741825
data = length 616, hash 3F657E23
crypto mode = 1
diff --git a/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump b/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump
index 5fad73c..ce0badc 100644
--- a/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.2.dump
@@ -23,7 +23,7 @@
crypto mode = 1
encryption key = length 16, hash 9FDDEA52
sample 1:
- time = 520000
+ time = 519999
flags = 1073741825
data = length 616, hash 3F657E23
crypto mode = 1
diff --git a/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.unknown_length.dump b/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.unknown_length.dump
index 425e0f6..0d0b831 100644
--- a/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.unknown_length.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_ac4_protected.mp4.unknown_length.dump
@@ -95,7 +95,7 @@
crypto mode = 1
encryption key = length 16, hash 9FDDEA52
sample 13:
- time = 520000
+ time = 519999
flags = 1073741825
data = length 616, hash 3F657E23
crypto mode = 1
diff --git a/tree/testdata/src/test/assets/mp4/sample_fragmented.mp4.0.dump b/tree/testdata/src/test/assets/mp4/sample_fragmented.mp4.0.dump
index a736207..2a5848f 100644
--- a/tree/testdata/src/test/assets/mp4/sample_fragmented.mp4.0.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_fragmented.mp4.0.dump
@@ -15,123 +15,123 @@
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
- time = 66000
+ time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
- time = 199000
+ time = 200199
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
- time = 132000
+ time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
- time = 100000
+ time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
- time = 166000
+ time = 166832
flags = 0
data = length 564, hash E5F56C5B
sample 5:
- time = 332000
+ time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
- time = 266000
+ time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
- time = 233000
+ time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
- time = 299000
+ time = 300299
flags = 0
data = length 467, hash 69806D94
sample 9:
- time = 466000
+ time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
- time = 399000
+ time = 400399
flags = 0
data = length 1087, hash 491BF106
sample 11:
- time = 367000
+ time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
- time = 433000
+ time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
- time = 599000
+ time = 600599
flags = 0
data = length 5190, hash B9031D8
sample 14:
- time = 533000
+ time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
- time = 500000
+ time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
- time = 566000
+ time = 567232
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
- time = 733000
+ time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
- time = 666000
+ time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
- time = 633000
+ time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
- time = 700000
+ time = 700699
flags = 0
data = length 491, hash B5930C7C
sample 21:
- time = 866000
+ time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
- time = 800000
+ time = 800799
flags = 0
data = length 838, hash 294A3451
sample 23:
- time = 767000
+ time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
- time = 833000
+ time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
- time = 1000000
+ time = 1000999
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
- time = 933000
+ time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
- time = 900000
+ time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
- time = 967000
+ time = 967632
flags = 0
data = length 415, hash 850DFEA3
sample 29:
- time = 1033000
+ time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
@@ -151,183 +151,183 @@
flags = 1
data = length 18, hash 96519432
sample 1:
- time = 23000
+ time = 23219
flags = 1
data = length 4, hash EE9DF
sample 2:
- time = 46000
+ time = 46439
flags = 1
data = length 4, hash EEDBF
sample 3:
- time = 69000
+ time = 69659
flags = 1
data = length 157, hash E2F078F4
sample 4:
- time = 92000
+ time = 92879
flags = 1
data = length 371, hash B9471F94
sample 5:
- time = 116000
+ time = 116099
flags = 1
data = length 373, hash 2AB265CB
sample 6:
- time = 139000
+ time = 139319
flags = 1
data = length 402, hash 1295477C
sample 7:
- time = 162000
+ time = 162539
flags = 1
data = length 455, hash 2D8146C8
sample 8:
- time = 185000
+ time = 185759
flags = 1
data = length 434, hash F2C5D287
sample 9:
- time = 208000
+ time = 208979
flags = 1
data = length 450, hash 84143FCD
sample 10:
- time = 232000
+ time = 232199
flags = 1
data = length 429, hash EF769D50
sample 11:
- time = 255000
+ time = 255419
flags = 1
data = length 450, hash EC3DE692
sample 12:
- time = 278000
+ time = 278639
flags = 1
data = length 447, hash 3E519E13
sample 13:
- time = 301000
+ time = 301859
flags = 1
data = length 457, hash 1E4F23A0
sample 14:
- time = 325000
+ time = 325079
flags = 1
data = length 447, hash A439EA97
sample 15:
- time = 348000
+ time = 348299
flags = 1
data = length 456, hash 1E9034C6
sample 16:
- time = 371000
+ time = 371519
flags = 1
data = length 398, hash 99DB7345
sample 17:
- time = 394000
+ time = 394739
flags = 1
data = length 474, hash 3F05F10A
sample 18:
- time = 417000
+ time = 417959
flags = 1
data = length 416, hash C105EE09
sample 19:
- time = 441000
+ time = 441179
flags = 1
data = length 454, hash 5FDBE458
sample 20:
- time = 464000
+ time = 464399
flags = 1
data = length 438, hash 41A93AC3
sample 21:
- time = 487000
+ time = 487619
flags = 1
data = length 443, hash 10FDA652
sample 22:
- time = 510000
+ time = 510839
flags = 1
data = length 412, hash 1F791E25
sample 23:
- time = 534000
+ time = 534058
flags = 1
data = length 482, hash A6D983D
sample 24:
- time = 557000
+ time = 557278
flags = 1
data = length 386, hash BED7392F
sample 25:
- time = 580000
+ time = 580498
flags = 1
data = length 463, hash 5309F8C9
sample 26:
- time = 603000
+ time = 603718
flags = 1
data = length 394, hash 21C7321F
sample 27:
- time = 626000
+ time = 626938
flags = 1
data = length 489, hash 71B4730D
sample 28:
- time = 650000
+ time = 650158
flags = 1
data = length 403, hash D9C6DE89
sample 29:
- time = 673000
+ time = 673378
flags = 1
data = length 447, hash 9B14B73B
sample 30:
- time = 696000
+ time = 696598
flags = 1
data = length 439, hash 4760D35B
sample 31:
- time = 719000
+ time = 719818
flags = 1
data = length 463, hash 1601F88D
sample 32:
- time = 743000
+ time = 743038
flags = 1
data = length 423, hash D4AE6773
sample 33:
- time = 766000
+ time = 766258
flags = 1
data = length 497, hash A3C674D3
sample 34:
- time = 789000
+ time = 789478
flags = 1
data = length 419, hash D3734A1F
sample 35:
- time = 812000
+ time = 812698
flags = 1
data = length 474, hash DFB41F9
sample 36:
- time = 835000
+ time = 835918
flags = 1
data = length 413, hash 53E7CB9F
sample 37:
- time = 859000
+ time = 859138
flags = 1
data = length 445, hash D15B0E39
sample 38:
- time = 882000
+ time = 882358
flags = 1
data = length 453, hash 77ED81E4
sample 39:
- time = 905000
+ time = 905578
flags = 1
data = length 545, hash 3321AEB9
sample 40:
- time = 928000
+ time = 928798
flags = 1
data = length 317, hash F557D0E
sample 41:
- time = 952000
+ time = 952018
flags = 1
data = length 537, hash ED58CF7B
sample 42:
- time = 975000
+ time = 975238
flags = 1
data = length 458, hash 51CDAA10
sample 43:
- time = 998000
+ time = 998458
flags = 1
data = length 465, hash CBA1EFD7
sample 44:
- time = 1021000
+ time = 1021678
flags = 1
data = length 446, hash D6735B8A
sample 45:
- time = 1044000
+ time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp4/sample_fragmented.mp4.unknown_length.dump b/tree/testdata/src/test/assets/mp4/sample_fragmented.mp4.unknown_length.dump
index a736207..2a5848f 100644
--- a/tree/testdata/src/test/assets/mp4/sample_fragmented.mp4.unknown_length.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_fragmented.mp4.unknown_length.dump
@@ -15,123 +15,123 @@
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
- time = 66000
+ time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
- time = 199000
+ time = 200199
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
- time = 132000
+ time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
- time = 100000
+ time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
- time = 166000
+ time = 166832
flags = 0
data = length 564, hash E5F56C5B
sample 5:
- time = 332000
+ time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
- time = 266000
+ time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
- time = 233000
+ time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
- time = 299000
+ time = 300299
flags = 0
data = length 467, hash 69806D94
sample 9:
- time = 466000
+ time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
- time = 399000
+ time = 400399
flags = 0
data = length 1087, hash 491BF106
sample 11:
- time = 367000
+ time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
- time = 433000
+ time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
- time = 599000
+ time = 600599
flags = 0
data = length 5190, hash B9031D8
sample 14:
- time = 533000
+ time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
- time = 500000
+ time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
- time = 566000
+ time = 567232
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
- time = 733000
+ time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
- time = 666000
+ time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
- time = 633000
+ time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
- time = 700000
+ time = 700699
flags = 0
data = length 491, hash B5930C7C
sample 21:
- time = 866000
+ time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
- time = 800000
+ time = 800799
flags = 0
data = length 838, hash 294A3451
sample 23:
- time = 767000
+ time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
- time = 833000
+ time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
- time = 1000000
+ time = 1000999
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
- time = 933000
+ time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
- time = 900000
+ time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
- time = 967000
+ time = 967632
flags = 0
data = length 415, hash 850DFEA3
sample 29:
- time = 1033000
+ time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
@@ -151,183 +151,183 @@
flags = 1
data = length 18, hash 96519432
sample 1:
- time = 23000
+ time = 23219
flags = 1
data = length 4, hash EE9DF
sample 2:
- time = 46000
+ time = 46439
flags = 1
data = length 4, hash EEDBF
sample 3:
- time = 69000
+ time = 69659
flags = 1
data = length 157, hash E2F078F4
sample 4:
- time = 92000
+ time = 92879
flags = 1
data = length 371, hash B9471F94
sample 5:
- time = 116000
+ time = 116099
flags = 1
data = length 373, hash 2AB265CB
sample 6:
- time = 139000
+ time = 139319
flags = 1
data = length 402, hash 1295477C
sample 7:
- time = 162000
+ time = 162539
flags = 1
data = length 455, hash 2D8146C8
sample 8:
- time = 185000
+ time = 185759
flags = 1
data = length 434, hash F2C5D287
sample 9:
- time = 208000
+ time = 208979
flags = 1
data = length 450, hash 84143FCD
sample 10:
- time = 232000
+ time = 232199
flags = 1
data = length 429, hash EF769D50
sample 11:
- time = 255000
+ time = 255419
flags = 1
data = length 450, hash EC3DE692
sample 12:
- time = 278000
+ time = 278639
flags = 1
data = length 447, hash 3E519E13
sample 13:
- time = 301000
+ time = 301859
flags = 1
data = length 457, hash 1E4F23A0
sample 14:
- time = 325000
+ time = 325079
flags = 1
data = length 447, hash A439EA97
sample 15:
- time = 348000
+ time = 348299
flags = 1
data = length 456, hash 1E9034C6
sample 16:
- time = 371000
+ time = 371519
flags = 1
data = length 398, hash 99DB7345
sample 17:
- time = 394000
+ time = 394739
flags = 1
data = length 474, hash 3F05F10A
sample 18:
- time = 417000
+ time = 417959
flags = 1
data = length 416, hash C105EE09
sample 19:
- time = 441000
+ time = 441179
flags = 1
data = length 454, hash 5FDBE458
sample 20:
- time = 464000
+ time = 464399
flags = 1
data = length 438, hash 41A93AC3
sample 21:
- time = 487000
+ time = 487619
flags = 1
data = length 443, hash 10FDA652
sample 22:
- time = 510000
+ time = 510839
flags = 1
data = length 412, hash 1F791E25
sample 23:
- time = 534000
+ time = 534058
flags = 1
data = length 482, hash A6D983D
sample 24:
- time = 557000
+ time = 557278
flags = 1
data = length 386, hash BED7392F
sample 25:
- time = 580000
+ time = 580498
flags = 1
data = length 463, hash 5309F8C9
sample 26:
- time = 603000
+ time = 603718
flags = 1
data = length 394, hash 21C7321F
sample 27:
- time = 626000
+ time = 626938
flags = 1
data = length 489, hash 71B4730D
sample 28:
- time = 650000
+ time = 650158
flags = 1
data = length 403, hash D9C6DE89
sample 29:
- time = 673000
+ time = 673378
flags = 1
data = length 447, hash 9B14B73B
sample 30:
- time = 696000
+ time = 696598
flags = 1
data = length 439, hash 4760D35B
sample 31:
- time = 719000
+ time = 719818
flags = 1
data = length 463, hash 1601F88D
sample 32:
- time = 743000
+ time = 743038
flags = 1
data = length 423, hash D4AE6773
sample 33:
- time = 766000
+ time = 766258
flags = 1
data = length 497, hash A3C674D3
sample 34:
- time = 789000
+ time = 789478
flags = 1
data = length 419, hash D3734A1F
sample 35:
- time = 812000
+ time = 812698
flags = 1
data = length 474, hash DFB41F9
sample 36:
- time = 835000
+ time = 835918
flags = 1
data = length 413, hash 53E7CB9F
sample 37:
- time = 859000
+ time = 859138
flags = 1
data = length 445, hash D15B0E39
sample 38:
- time = 882000
+ time = 882358
flags = 1
data = length 453, hash 77ED81E4
sample 39:
- time = 905000
+ time = 905578
flags = 1
data = length 545, hash 3321AEB9
sample 40:
- time = 928000
+ time = 928798
flags = 1
data = length 317, hash F557D0E
sample 41:
- time = 952000
+ time = 952018
flags = 1
data = length 537, hash ED58CF7B
sample 42:
- time = 975000
+ time = 975238
flags = 1
data = length 458, hash 51CDAA10
sample 43:
- time = 998000
+ time = 998458
flags = 1
data = length 465, hash CBA1EFD7
sample 44:
- time = 1021000
+ time = 1021678
flags = 1
data = length 446, hash D6735B8A
sample 45:
- time = 1044000
+ time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump b/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump
index c879cde..5dc1f1d 100644
--- a/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.0.dump
@@ -18,123 +18,123 @@
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
- time = 66000
+ time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
- time = 199000
+ time = 200199
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
- time = 132000
+ time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
- time = 100000
+ time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
- time = 166000
+ time = 166832
flags = 0
data = length 564, hash E5F56C5B
sample 5:
- time = 332000
+ time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
- time = 266000
+ time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
- time = 233000
+ time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
- time = 299000
+ time = 300299
flags = 0
data = length 467, hash 69806D94
sample 9:
- time = 466000
+ time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
- time = 399000
+ time = 400399
flags = 0
data = length 1087, hash 491BF106
sample 11:
- time = 367000
+ time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
- time = 433000
+ time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
- time = 599000
+ time = 600599
flags = 0
data = length 5190, hash B9031D8
sample 14:
- time = 533000
+ time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
- time = 500000
+ time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
- time = 566000
+ time = 567232
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
- time = 733000
+ time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
- time = 666000
+ time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
- time = 633000
+ time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
- time = 700000
+ time = 700699
flags = 0
data = length 491, hash B5930C7C
sample 21:
- time = 866000
+ time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
- time = 800000
+ time = 800799
flags = 0
data = length 838, hash 294A3451
sample 23:
- time = 767000
+ time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
- time = 833000
+ time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
- time = 1000000
+ time = 1000999
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
- time = 933000
+ time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
- time = 900000
+ time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
- time = 967000
+ time = 967632
flags = 0
data = length 415, hash 850DFEA3
sample 29:
- time = 1033000
+ time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
@@ -154,183 +154,183 @@
flags = 1
data = length 18, hash 96519432
sample 1:
- time = 23000
+ time = 23219
flags = 1
data = length 4, hash EE9DF
sample 2:
- time = 46000
+ time = 46439
flags = 1
data = length 4, hash EEDBF
sample 3:
- time = 69000
+ time = 69659
flags = 1
data = length 157, hash E2F078F4
sample 4:
- time = 92000
+ time = 92879
flags = 1
data = length 371, hash B9471F94
sample 5:
- time = 116000
+ time = 116099
flags = 1
data = length 373, hash 2AB265CB
sample 6:
- time = 139000
+ time = 139319
flags = 1
data = length 402, hash 1295477C
sample 7:
- time = 162000
+ time = 162539
flags = 1
data = length 455, hash 2D8146C8
sample 8:
- time = 185000
+ time = 185759
flags = 1
data = length 434, hash F2C5D287
sample 9:
- time = 208000
+ time = 208979
flags = 1
data = length 450, hash 84143FCD
sample 10:
- time = 232000
+ time = 232199
flags = 1
data = length 429, hash EF769D50
sample 11:
- time = 255000
+ time = 255419
flags = 1
data = length 450, hash EC3DE692
sample 12:
- time = 278000
+ time = 278639
flags = 1
data = length 447, hash 3E519E13
sample 13:
- time = 301000
+ time = 301859
flags = 1
data = length 457, hash 1E4F23A0
sample 14:
- time = 325000
+ time = 325079
flags = 1
data = length 447, hash A439EA97
sample 15:
- time = 348000
+ time = 348299
flags = 1
data = length 456, hash 1E9034C6
sample 16:
- time = 371000
+ time = 371519
flags = 1
data = length 398, hash 99DB7345
sample 17:
- time = 394000
+ time = 394739
flags = 1
data = length 474, hash 3F05F10A
sample 18:
- time = 417000
+ time = 417959
flags = 1
data = length 416, hash C105EE09
sample 19:
- time = 441000
+ time = 441179
flags = 1
data = length 454, hash 5FDBE458
sample 20:
- time = 464000
+ time = 464399
flags = 1
data = length 438, hash 41A93AC3
sample 21:
- time = 487000
+ time = 487619
flags = 1
data = length 443, hash 10FDA652
sample 22:
- time = 510000
+ time = 510839
flags = 1
data = length 412, hash 1F791E25
sample 23:
- time = 534000
+ time = 534058
flags = 1
data = length 482, hash A6D983D
sample 24:
- time = 557000
+ time = 557278
flags = 1
data = length 386, hash BED7392F
sample 25:
- time = 580000
+ time = 580498
flags = 1
data = length 463, hash 5309F8C9
sample 26:
- time = 603000
+ time = 603718
flags = 1
data = length 394, hash 21C7321F
sample 27:
- time = 626000
+ time = 626938
flags = 1
data = length 489, hash 71B4730D
sample 28:
- time = 650000
+ time = 650158
flags = 1
data = length 403, hash D9C6DE89
sample 29:
- time = 673000
+ time = 673378
flags = 1
data = length 447, hash 9B14B73B
sample 30:
- time = 696000
+ time = 696598
flags = 1
data = length 439, hash 4760D35B
sample 31:
- time = 719000
+ time = 719818
flags = 1
data = length 463, hash 1601F88D
sample 32:
- time = 743000
+ time = 743038
flags = 1
data = length 423, hash D4AE6773
sample 33:
- time = 766000
+ time = 766258
flags = 1
data = length 497, hash A3C674D3
sample 34:
- time = 789000
+ time = 789478
flags = 1
data = length 419, hash D3734A1F
sample 35:
- time = 812000
+ time = 812698
flags = 1
data = length 474, hash DFB41F9
sample 36:
- time = 835000
+ time = 835918
flags = 1
data = length 413, hash 53E7CB9F
sample 37:
- time = 859000
+ time = 859138
flags = 1
data = length 445, hash D15B0E39
sample 38:
- time = 882000
+ time = 882358
flags = 1
data = length 453, hash 77ED81E4
sample 39:
- time = 905000
+ time = 905578
flags = 1
data = length 545, hash 3321AEB9
sample 40:
- time = 928000
+ time = 928798
flags = 1
data = length 317, hash F557D0E
sample 41:
- time = 952000
+ time = 952018
flags = 1
data = length 537, hash ED58CF7B
sample 42:
- time = 975000
+ time = 975238
flags = 1
data = length 458, hash 51CDAA10
sample 43:
- time = 998000
+ time = 998458
flags = 1
data = length 465, hash CBA1EFD7
sample 44:
- time = 1021000
+ time = 1021678
flags = 1
data = length 446, hash D6735B8A
sample 45:
- time = 1044000
+ time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump b/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump
index b264cf1..aab2beb 100644
--- a/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.1.dump
@@ -18,123 +18,123 @@
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
- time = 66000
+ time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
- time = 199000
+ time = 200199
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
- time = 132000
+ time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
- time = 100000
+ time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
- time = 166000
+ time = 166832
flags = 0
data = length 564, hash E5F56C5B
sample 5:
- time = 332000
+ time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
- time = 266000
+ time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
- time = 233000
+ time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
- time = 299000
+ time = 300299
flags = 0
data = length 467, hash 69806D94
sample 9:
- time = 466000
+ time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
- time = 399000
+ time = 400399
flags = 0
data = length 1087, hash 491BF106
sample 11:
- time = 367000
+ time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
- time = 433000
+ time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
- time = 599000
+ time = 600599
flags = 0
data = length 5190, hash B9031D8
sample 14:
- time = 533000
+ time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
- time = 500000
+ time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
- time = 566000
+ time = 567232
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
- time = 733000
+ time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
- time = 666000
+ time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
- time = 633000
+ time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
- time = 700000
+ time = 700699
flags = 0
data = length 491, hash B5930C7C
sample 21:
- time = 866000
+ time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
- time = 800000
+ time = 800799
flags = 0
data = length 838, hash 294A3451
sample 23:
- time = 767000
+ time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
- time = 833000
+ time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
- time = 1000000
+ time = 1000999
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
- time = 933000
+ time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
- time = 900000
+ time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
- time = 967000
+ time = 967632
flags = 0
data = length 415, hash 850DFEA3
sample 29:
- time = 1033000
+ time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
@@ -150,127 +150,127 @@
initializationData:
data = length 5, hash 2B7623A
sample 0:
- time = 348000
+ time = 348299
flags = 1
data = length 456, hash 1E9034C6
sample 1:
- time = 371000
+ time = 371519
flags = 1
data = length 398, hash 99DB7345
sample 2:
- time = 394000
+ time = 394739
flags = 1
data = length 474, hash 3F05F10A
sample 3:
- time = 417000
+ time = 417959
flags = 1
data = length 416, hash C105EE09
sample 4:
- time = 441000
+ time = 441179
flags = 1
data = length 454, hash 5FDBE458
sample 5:
- time = 464000
+ time = 464399
flags = 1
data = length 438, hash 41A93AC3
sample 6:
- time = 487000
+ time = 487619
flags = 1
data = length 443, hash 10FDA652
sample 7:
- time = 510000
+ time = 510839
flags = 1
data = length 412, hash 1F791E25
sample 8:
- time = 534000
+ time = 534058
flags = 1
data = length 482, hash A6D983D
sample 9:
- time = 557000
+ time = 557278
flags = 1
data = length 386, hash BED7392F
sample 10:
- time = 580000
+ time = 580498
flags = 1
data = length 463, hash 5309F8C9
sample 11:
- time = 603000
+ time = 603718
flags = 1
data = length 394, hash 21C7321F
sample 12:
- time = 626000
+ time = 626938
flags = 1
data = length 489, hash 71B4730D
sample 13:
- time = 650000
+ time = 650158
flags = 1
data = length 403, hash D9C6DE89
sample 14:
- time = 673000
+ time = 673378
flags = 1
data = length 447, hash 9B14B73B
sample 15:
- time = 696000
+ time = 696598
flags = 1
data = length 439, hash 4760D35B
sample 16:
- time = 719000
+ time = 719818
flags = 1
data = length 463, hash 1601F88D
sample 17:
- time = 743000
+ time = 743038
flags = 1
data = length 423, hash D4AE6773
sample 18:
- time = 766000
+ time = 766258
flags = 1
data = length 497, hash A3C674D3
sample 19:
- time = 789000
+ time = 789478
flags = 1
data = length 419, hash D3734A1F
sample 20:
- time = 812000
+ time = 812698
flags = 1
data = length 474, hash DFB41F9
sample 21:
- time = 835000
+ time = 835918
flags = 1
data = length 413, hash 53E7CB9F
sample 22:
- time = 859000
+ time = 859138
flags = 1
data = length 445, hash D15B0E39
sample 23:
- time = 882000
+ time = 882358
flags = 1
data = length 453, hash 77ED81E4
sample 24:
- time = 905000
+ time = 905578
flags = 1
data = length 545, hash 3321AEB9
sample 25:
- time = 928000
+ time = 928798
flags = 1
data = length 317, hash F557D0E
sample 26:
- time = 952000
+ time = 952018
flags = 1
data = length 537, hash ED58CF7B
sample 27:
- time = 975000
+ time = 975238
flags = 1
data = length 458, hash 51CDAA10
sample 28:
- time = 998000
+ time = 998458
flags = 1
data = length 465, hash CBA1EFD7
sample 29:
- time = 1021000
+ time = 1021678
flags = 1
data = length 446, hash D6735B8A
sample 30:
- time = 1044000
+ time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump b/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump
index 4b10cca..c1d569c 100644
--- a/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.2.dump
@@ -18,123 +18,123 @@
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
- time = 66000
+ time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
- time = 199000
+ time = 200199
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
- time = 132000
+ time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
- time = 100000
+ time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
- time = 166000
+ time = 166832
flags = 0
data = length 564, hash E5F56C5B
sample 5:
- time = 332000
+ time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
- time = 266000
+ time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
- time = 233000
+ time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
- time = 299000
+ time = 300299
flags = 0
data = length 467, hash 69806D94
sample 9:
- time = 466000
+ time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
- time = 399000
+ time = 400399
flags = 0
data = length 1087, hash 491BF106
sample 11:
- time = 367000
+ time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
- time = 433000
+ time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
- time = 599000
+ time = 600599
flags = 0
data = length 5190, hash B9031D8
sample 14:
- time = 533000
+ time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
- time = 500000
+ time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
- time = 566000
+ time = 567232
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
- time = 733000
+ time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
- time = 666000
+ time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
- time = 633000
+ time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
- time = 700000
+ time = 700699
flags = 0
data = length 491, hash B5930C7C
sample 21:
- time = 866000
+ time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
- time = 800000
+ time = 800799
flags = 0
data = length 838, hash 294A3451
sample 23:
- time = 767000
+ time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
- time = 833000
+ time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
- time = 1000000
+ time = 1000999
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
- time = 933000
+ time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
- time = 900000
+ time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
- time = 967000
+ time = 967632
flags = 0
data = length 415, hash 850DFEA3
sample 29:
- time = 1033000
+ time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
@@ -150,67 +150,67 @@
initializationData:
data = length 5, hash 2B7623A
sample 0:
- time = 696000
+ time = 696598
flags = 1
data = length 439, hash 4760D35B
sample 1:
- time = 719000
+ time = 719818
flags = 1
data = length 463, hash 1601F88D
sample 2:
- time = 743000
+ time = 743038
flags = 1
data = length 423, hash D4AE6773
sample 3:
- time = 766000
+ time = 766258
flags = 1
data = length 497, hash A3C674D3
sample 4:
- time = 789000
+ time = 789478
flags = 1
data = length 419, hash D3734A1F
sample 5:
- time = 812000
+ time = 812698
flags = 1
data = length 474, hash DFB41F9
sample 6:
- time = 835000
+ time = 835918
flags = 1
data = length 413, hash 53E7CB9F
sample 7:
- time = 859000
+ time = 859138
flags = 1
data = length 445, hash D15B0E39
sample 8:
- time = 882000
+ time = 882358
flags = 1
data = length 453, hash 77ED81E4
sample 9:
- time = 905000
+ time = 905578
flags = 1
data = length 545, hash 3321AEB9
sample 10:
- time = 928000
+ time = 928798
flags = 1
data = length 317, hash F557D0E
sample 11:
- time = 952000
+ time = 952018
flags = 1
data = length 537, hash ED58CF7B
sample 12:
- time = 975000
+ time = 975238
flags = 1
data = length 458, hash 51CDAA10
sample 13:
- time = 998000
+ time = 998458
flags = 1
data = length 465, hash CBA1EFD7
sample 14:
- time = 1021000
+ time = 1021678
flags = 1
data = length 446, hash D6735B8A
sample 15:
- time = 1044000
+ time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump b/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump
index 4477d48..dd915bc 100644
--- a/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.3.dump
@@ -18,123 +18,123 @@
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
- time = 66000
+ time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
- time = 199000
+ time = 200199
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
- time = 132000
+ time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
- time = 100000
+ time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
- time = 166000
+ time = 166832
flags = 0
data = length 564, hash E5F56C5B
sample 5:
- time = 332000
+ time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
- time = 266000
+ time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
- time = 233000
+ time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
- time = 299000
+ time = 300299
flags = 0
data = length 467, hash 69806D94
sample 9:
- time = 466000
+ time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
- time = 399000
+ time = 400399
flags = 0
data = length 1087, hash 491BF106
sample 11:
- time = 367000
+ time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
- time = 433000
+ time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
- time = 599000
+ time = 600599
flags = 0
data = length 5190, hash B9031D8
sample 14:
- time = 533000
+ time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
- time = 500000
+ time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
- time = 566000
+ time = 567232
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
- time = 733000
+ time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
- time = 666000
+ time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
- time = 633000
+ time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
- time = 700000
+ time = 700699
flags = 0
data = length 491, hash B5930C7C
sample 21:
- time = 866000
+ time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
- time = 800000
+ time = 800799
flags = 0
data = length 838, hash 294A3451
sample 23:
- time = 767000
+ time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
- time = 833000
+ time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
- time = 1000000
+ time = 1000999
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
- time = 933000
+ time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
- time = 900000
+ time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
- time = 967000
+ time = 967632
flags = 0
data = length 415, hash 850DFEA3
sample 29:
- time = 1033000
+ time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
@@ -150,7 +150,7 @@
initializationData:
data = length 5, hash 2B7623A
sample 0:
- time = 1044000
+ time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.unknown_length.dump b/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.unknown_length.dump
index c879cde..5dc1f1d 100644
--- a/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.unknown_length.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_fragmented_seekable.mp4.unknown_length.dump
@@ -18,123 +18,123 @@
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
- time = 66000
+ time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
- time = 199000
+ time = 200199
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
- time = 132000
+ time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
- time = 100000
+ time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
- time = 166000
+ time = 166832
flags = 0
data = length 564, hash E5F56C5B
sample 5:
- time = 332000
+ time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
- time = 266000
+ time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
- time = 233000
+ time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
- time = 299000
+ time = 300299
flags = 0
data = length 467, hash 69806D94
sample 9:
- time = 466000
+ time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
- time = 399000
+ time = 400399
flags = 0
data = length 1087, hash 491BF106
sample 11:
- time = 367000
+ time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
- time = 433000
+ time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
- time = 599000
+ time = 600599
flags = 0
data = length 5190, hash B9031D8
sample 14:
- time = 533000
+ time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
- time = 500000
+ time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
- time = 566000
+ time = 567232
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
- time = 733000
+ time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
- time = 666000
+ time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
- time = 633000
+ time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
- time = 700000
+ time = 700699
flags = 0
data = length 491, hash B5930C7C
sample 21:
- time = 866000
+ time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
- time = 800000
+ time = 800799
flags = 0
data = length 838, hash 294A3451
sample 23:
- time = 767000
+ time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
- time = 833000
+ time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
- time = 1000000
+ time = 1000999
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
- time = 933000
+ time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
- time = 900000
+ time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
- time = 967000
+ time = 967632
flags = 0
data = length 415, hash 850DFEA3
sample 29:
- time = 1033000
+ time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
@@ -154,183 +154,183 @@
flags = 1
data = length 18, hash 96519432
sample 1:
- time = 23000
+ time = 23219
flags = 1
data = length 4, hash EE9DF
sample 2:
- time = 46000
+ time = 46439
flags = 1
data = length 4, hash EEDBF
sample 3:
- time = 69000
+ time = 69659
flags = 1
data = length 157, hash E2F078F4
sample 4:
- time = 92000
+ time = 92879
flags = 1
data = length 371, hash B9471F94
sample 5:
- time = 116000
+ time = 116099
flags = 1
data = length 373, hash 2AB265CB
sample 6:
- time = 139000
+ time = 139319
flags = 1
data = length 402, hash 1295477C
sample 7:
- time = 162000
+ time = 162539
flags = 1
data = length 455, hash 2D8146C8
sample 8:
- time = 185000
+ time = 185759
flags = 1
data = length 434, hash F2C5D287
sample 9:
- time = 208000
+ time = 208979
flags = 1
data = length 450, hash 84143FCD
sample 10:
- time = 232000
+ time = 232199
flags = 1
data = length 429, hash EF769D50
sample 11:
- time = 255000
+ time = 255419
flags = 1
data = length 450, hash EC3DE692
sample 12:
- time = 278000
+ time = 278639
flags = 1
data = length 447, hash 3E519E13
sample 13:
- time = 301000
+ time = 301859
flags = 1
data = length 457, hash 1E4F23A0
sample 14:
- time = 325000
+ time = 325079
flags = 1
data = length 447, hash A439EA97
sample 15:
- time = 348000
+ time = 348299
flags = 1
data = length 456, hash 1E9034C6
sample 16:
- time = 371000
+ time = 371519
flags = 1
data = length 398, hash 99DB7345
sample 17:
- time = 394000
+ time = 394739
flags = 1
data = length 474, hash 3F05F10A
sample 18:
- time = 417000
+ time = 417959
flags = 1
data = length 416, hash C105EE09
sample 19:
- time = 441000
+ time = 441179
flags = 1
data = length 454, hash 5FDBE458
sample 20:
- time = 464000
+ time = 464399
flags = 1
data = length 438, hash 41A93AC3
sample 21:
- time = 487000
+ time = 487619
flags = 1
data = length 443, hash 10FDA652
sample 22:
- time = 510000
+ time = 510839
flags = 1
data = length 412, hash 1F791E25
sample 23:
- time = 534000
+ time = 534058
flags = 1
data = length 482, hash A6D983D
sample 24:
- time = 557000
+ time = 557278
flags = 1
data = length 386, hash BED7392F
sample 25:
- time = 580000
+ time = 580498
flags = 1
data = length 463, hash 5309F8C9
sample 26:
- time = 603000
+ time = 603718
flags = 1
data = length 394, hash 21C7321F
sample 27:
- time = 626000
+ time = 626938
flags = 1
data = length 489, hash 71B4730D
sample 28:
- time = 650000
+ time = 650158
flags = 1
data = length 403, hash D9C6DE89
sample 29:
- time = 673000
+ time = 673378
flags = 1
data = length 447, hash 9B14B73B
sample 30:
- time = 696000
+ time = 696598
flags = 1
data = length 439, hash 4760D35B
sample 31:
- time = 719000
+ time = 719818
flags = 1
data = length 463, hash 1601F88D
sample 32:
- time = 743000
+ time = 743038
flags = 1
data = length 423, hash D4AE6773
sample 33:
- time = 766000
+ time = 766258
flags = 1
data = length 497, hash A3C674D3
sample 34:
- time = 789000
+ time = 789478
flags = 1
data = length 419, hash D3734A1F
sample 35:
- time = 812000
+ time = 812698
flags = 1
data = length 474, hash DFB41F9
sample 36:
- time = 835000
+ time = 835918
flags = 1
data = length 413, hash 53E7CB9F
sample 37:
- time = 859000
+ time = 859138
flags = 1
data = length 445, hash D15B0E39
sample 38:
- time = 882000
+ time = 882358
flags = 1
data = length 453, hash 77ED81E4
sample 39:
- time = 905000
+ time = 905578
flags = 1
data = length 545, hash 3321AEB9
sample 40:
- time = 928000
+ time = 928798
flags = 1
data = length 317, hash F557D0E
sample 41:
- time = 952000
+ time = 952018
flags = 1
data = length 537, hash ED58CF7B
sample 42:
- time = 975000
+ time = 975238
flags = 1
data = length 458, hash 51CDAA10
sample 43:
- time = 998000
+ time = 998458
flags = 1
data = length 465, hash CBA1EFD7
sample 44:
- time = 1021000
+ time = 1021678
flags = 1
data = length 446, hash D6735B8A
sample 45:
- time = 1044000
+ time = 1044897
flags = 1
data = length 10, hash A453EEBE
tracksEnded = true
diff --git a/tree/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump b/tree/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump
index 95fe467..341fba4 100644
--- a/tree/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.0.dump
@@ -15,123 +15,123 @@
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
- time = 66000
+ time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
- time = 199000
+ time = 200199
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
- time = 132000
+ time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
- time = 100000
+ time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
- time = 166000
+ time = 166832
flags = 0
data = length 564, hash E5F56C5B
sample 5:
- time = 332000
+ time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
- time = 266000
+ time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
- time = 233000
+ time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
- time = 299000
+ time = 300299
flags = 0
data = length 467, hash 69806D94
sample 9:
- time = 466000
+ time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
- time = 399000
+ time = 400399
flags = 0
data = length 1087, hash 491BF106
sample 11:
- time = 367000
+ time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
- time = 433000
+ time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
- time = 599000
+ time = 600599
flags = 0
data = length 5190, hash B9031D8
sample 14:
- time = 533000
+ time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
- time = 500000
+ time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
- time = 566000
+ time = 567232
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
- time = 733000
+ time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
- time = 666000
+ time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
- time = 633000
+ time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
- time = 700000
+ time = 700699
flags = 0
data = length 491, hash B5930C7C
sample 21:
- time = 866000
+ time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
- time = 800000
+ time = 800799
flags = 0
data = length 838, hash 294A3451
sample 23:
- time = 767000
+ time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
- time = 833000
+ time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
- time = 1000000
+ time = 1000999
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
- time = 933000
+ time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
- time = 900000
+ time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
- time = 967000
+ time = 967632
flags = 0
data = length 415, hash 850DFEA3
sample 29:
- time = 1033000
+ time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
@@ -151,183 +151,183 @@
flags = 1
data = length 18, hash 96519432
sample 1:
- time = 23000
+ time = 23219
flags = 1
data = length 4, hash EE9DF
sample 2:
- time = 46000
+ time = 46439
flags = 1
data = length 4, hash EEDBF
sample 3:
- time = 69000
+ time = 69659
flags = 1
data = length 157, hash E2F078F4
sample 4:
- time = 92000
+ time = 92879
flags = 1
data = length 371, hash B9471F94
sample 5:
- time = 116000
+ time = 116099
flags = 1
data = length 373, hash 2AB265CB
sample 6:
- time = 139000
+ time = 139319
flags = 1
data = length 402, hash 1295477C
sample 7:
- time = 162000
+ time = 162539
flags = 1
data = length 455, hash 2D8146C8
sample 8:
- time = 185000
+ time = 185759
flags = 1
data = length 434, hash F2C5D287
sample 9:
- time = 208000
+ time = 208979
flags = 1
data = length 450, hash 84143FCD
sample 10:
- time = 232000
+ time = 232199
flags = 1
data = length 429, hash EF769D50
sample 11:
- time = 255000
+ time = 255419
flags = 1
data = length 450, hash EC3DE692
sample 12:
- time = 278000
+ time = 278639
flags = 1
data = length 447, hash 3E519E13
sample 13:
- time = 301000
+ time = 301859
flags = 1
data = length 457, hash 1E4F23A0
sample 14:
- time = 325000
+ time = 325079
flags = 1
data = length 447, hash A439EA97
sample 15:
- time = 348000
+ time = 348299
flags = 1
data = length 456, hash 1E9034C6
sample 16:
- time = 371000
+ time = 371519
flags = 1
data = length 398, hash 99DB7345
sample 17:
- time = 394000
+ time = 394739
flags = 1
data = length 474, hash 3F05F10A
sample 18:
- time = 417000
+ time = 417959
flags = 1
data = length 416, hash C105EE09
sample 19:
- time = 441000
+ time = 441179
flags = 1
data = length 454, hash 5FDBE458
sample 20:
- time = 464000
+ time = 464399
flags = 1
data = length 438, hash 41A93AC3
sample 21:
- time = 487000
+ time = 487619
flags = 1
data = length 443, hash 10FDA652
sample 22:
- time = 510000
+ time = 510839
flags = 1
data = length 412, hash 1F791E25
sample 23:
- time = 534000
+ time = 534058
flags = 1
data = length 482, hash A6D983D
sample 24:
- time = 557000
+ time = 557278
flags = 1
data = length 386, hash BED7392F
sample 25:
- time = 580000
+ time = 580498
flags = 1
data = length 463, hash 5309F8C9
sample 26:
- time = 603000
+ time = 603718
flags = 1
data = length 394, hash 21C7321F
sample 27:
- time = 626000
+ time = 626938
flags = 1
data = length 489, hash 71B4730D
sample 28:
- time = 650000
+ time = 650158
flags = 1
data = length 403, hash D9C6DE89
sample 29:
- time = 673000
+ time = 673378
flags = 1
data = length 447, hash 9B14B73B
sample 30:
- time = 696000
+ time = 696598
flags = 1
data = length 439, hash 4760D35B
sample 31:
- time = 719000
+ time = 719818
flags = 1
data = length 463, hash 1601F88D
sample 32:
- time = 743000
+ time = 743038
flags = 1
data = length 423, hash D4AE6773
sample 33:
- time = 766000
+ time = 766258
flags = 1
data = length 497, hash A3C674D3
sample 34:
- time = 789000
+ time = 789478
flags = 1
data = length 419, hash D3734A1F
sample 35:
- time = 812000
+ time = 812698
flags = 1
data = length 474, hash DFB41F9
sample 36:
- time = 835000
+ time = 835918
flags = 1
data = length 413, hash 53E7CB9F
sample 37:
- time = 859000
+ time = 859138
flags = 1
data = length 445, hash D15B0E39
sample 38:
- time = 882000
+ time = 882358
flags = 1
data = length 453, hash 77ED81E4
sample 39:
- time = 905000
+ time = 905578
flags = 1
data = length 545, hash 3321AEB9
sample 40:
- time = 928000
+ time = 928798
flags = 1
data = length 317, hash F557D0E
sample 41:
- time = 952000
+ time = 952018
flags = 1
data = length 537, hash ED58CF7B
sample 42:
- time = 975000
+ time = 975238
flags = 1
data = length 458, hash 51CDAA10
sample 43:
- time = 998000
+ time = 998458
flags = 1
data = length 465, hash CBA1EFD7
sample 44:
- time = 1021000
+ time = 1021678
flags = 1
data = length 446, hash D6735B8A
sample 45:
- time = 1044000
+ time = 1044897
flags = 1
data = length 10, hash A453EEBE
track 3:
diff --git a/tree/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.unknown_length.dump b/tree/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.unknown_length.dump
index 95fe467..341fba4 100644
--- a/tree/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.unknown_length.dump
+++ b/tree/testdata/src/test/assets/mp4/sample_fragmented_sei.mp4.unknown_length.dump
@@ -15,123 +15,123 @@
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample 0:
- time = 66000
+ time = 66733
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
- time = 199000
+ time = 200199
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
- time = 132000
+ time = 133466
flags = 0
data = length 1295, hash C0DA5090
sample 3:
- time = 100000
+ time = 100100
flags = 0
data = length 469, hash D6E0A200
sample 4:
- time = 166000
+ time = 166832
flags = 0
data = length 564, hash E5F56C5B
sample 5:
- time = 332000
+ time = 333666
flags = 0
data = length 6075, hash 8756E49E
sample 6:
- time = 266000
+ time = 266933
flags = 0
data = length 847, hash DCC2B618
sample 7:
- time = 233000
+ time = 233566
flags = 0
data = length 455, hash B9CCE047
sample 8:
- time = 299000
+ time = 300299
flags = 0
data = length 467, hash 69806D94
sample 9:
- time = 466000
+ time = 467133
flags = 0
data = length 4549, hash 3944F501
sample 10:
- time = 399000
+ time = 400399
flags = 0
data = length 1087, hash 491BF106
sample 11:
- time = 367000
+ time = 367033
flags = 0
data = length 380, hash 5FED016A
sample 12:
- time = 433000
+ time = 433766
flags = 0
data = length 455, hash 8A0610
sample 13:
- time = 599000
+ time = 600599
flags = 0
data = length 5190, hash B9031D8
sample 14:
- time = 533000
+ time = 533866
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
- time = 500000
+ time = 500500
flags = 0
data = length 653, hash 8494F326
sample 16:
- time = 566000
+ time = 567232
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
- time = 733000
+ time = 734066
flags = 0
data = length 4884, hash D16B6A96
sample 18:
- time = 666000
+ time = 667333
flags = 0
data = length 997, hash 164FF210
sample 19:
- time = 633000
+ time = 633966
flags = 0
data = length 640, hash F664125B
sample 20:
- time = 700000
+ time = 700699
flags = 0
data = length 491, hash B5930C7C
sample 21:
- time = 866000
+ time = 867533
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
- time = 800000
+ time = 800799
flags = 0
data = length 838, hash 294A3451
sample 23:
- time = 767000
+ time = 767433
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
- time = 833000
+ time = 834166
flags = 0
data = length 329, hash A654FFA1
sample 25:
- time = 1000000
+ time = 1000999
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
- time = 933000
+ time = 934266
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
- time = 900000
+ time = 900900
flags = 0
data = length 415, hash B31BBC3B
sample 28:
- time = 967000
+ time = 967632
flags = 0
data = length 415, hash 850DFEA3
sample 29:
- time = 1033000
+ time = 1034366
flags = 0
data = length 619, hash AB5E56CA
track 1:
@@ -151,183 +151,183 @@
flags = 1
data = length 18, hash 96519432
sample 1:
- time = 23000
+ time = 23219
flags = 1
data = length 4, hash EE9DF
sample 2:
- time = 46000
+ time = 46439
flags = 1
data = length 4, hash EEDBF
sample 3:
- time = 69000
+ time = 69659
flags = 1
data = length 157, hash E2F078F4
sample 4:
- time = 92000
+ time = 92879
flags = 1
data = length 371, hash B9471F94
sample 5:
- time = 116000
+ time = 116099
flags = 1
data = length 373, hash 2AB265CB
sample 6:
- time = 139000
+ time = 139319
flags = 1
data = length 402, hash 1295477C
sample 7:
- time = 162000
+ time = 162539
flags = 1
data = length 455, hash 2D8146C8
sample 8:
- time = 185000
+ time = 185759
flags = 1
data = length 434, hash F2C5D287
sample 9:
- time = 208000
+ time = 208979
flags = 1
data = length 450, hash 84143FCD
sample 10:
- time = 232000
+ time = 232199
flags = 1
data = length 429, hash EF769D50
sample 11:
- time = 255000
+ time = 255419
flags = 1
data = length 450, hash EC3DE692
sample 12:
- time = 278000
+ time = 278639
flags = 1
data = length 447, hash 3E519E13
sample 13:
- time = 301000
+ time = 301859
flags = 1
data = length 457, hash 1E4F23A0
sample 14:
- time = 325000
+ time = 325079
flags = 1
data = length 447, hash A439EA97
sample 15:
- time = 348000
+ time = 348299
flags = 1
data = length 456, hash 1E9034C6
sample 16:
- time = 371000
+ time = 371519
flags = 1
data = length 398, hash 99DB7345
sample 17:
- time = 394000
+ time = 394739
flags = 1
data = length 474, hash 3F05F10A
sample 18:
- time = 417000
+ time = 417959
flags = 1
data = length 416, hash C105EE09
sample 19:
- time = 441000
+ time = 441179
flags = 1
data = length 454, hash 5FDBE458
sample 20:
- time = 464000
+ time = 464399
flags = 1
data = length 438, hash 41A93AC3
sample 21:
- time = 487000
+ time = 487619
flags = 1
data = length 443, hash 10FDA652
sample 22:
- time = 510000
+ time = 510839
flags = 1
data = length 412, hash 1F791E25
sample 23:
- time = 534000
+ time = 534058
flags = 1
data = length 482, hash A6D983D
sample 24:
- time = 557000
+ time = 557278
flags = 1
data = length 386, hash BED7392F
sample 25:
- time = 580000
+ time = 580498
flags = 1
data = length 463, hash 5309F8C9
sample 26:
- time = 603000
+ time = 603718
flags = 1
data = length 394, hash 21C7321F
sample 27:
- time = 626000
+ time = 626938
flags = 1
data = length 489, hash 71B4730D
sample 28:
- time = 650000
+ time = 650158
flags = 1
data = length 403, hash D9C6DE89
sample 29:
- time = 673000
+ time = 673378
flags = 1
data = length 447, hash 9B14B73B
sample 30:
- time = 696000
+ time = 696598
flags = 1
data = length 439, hash 4760D35B
sample 31:
- time = 719000
+ time = 719818
flags = 1
data = length 463, hash 1601F88D
sample 32:
- time = 743000
+ time = 743038
flags = 1
data = length 423, hash D4AE6773
sample 33:
- time = 766000
+ time = 766258
flags = 1
data = length 497, hash A3C674D3
sample 34:
- time = 789000
+ time = 789478
flags = 1
data = length 419, hash D3734A1F
sample 35:
- time = 812000
+ time = 812698
flags = 1
data = length 474, hash DFB41F9
sample 36:
- time = 835000
+ time = 835918
flags = 1
data = length 413, hash 53E7CB9F
sample 37:
- time = 859000
+ time = 859138
flags = 1
data = length 445, hash D15B0E39
sample 38:
- time = 882000
+ time = 882358
flags = 1
data = length 453, hash 77ED81E4
sample 39:
- time = 905000
+ time = 905578
flags = 1
data = length 545, hash 3321AEB9
sample 40:
- time = 928000
+ time = 928798
flags = 1
data = length 317, hash F557D0E
sample 41:
- time = 952000
+ time = 952018
flags = 1
data = length 537, hash ED58CF7B
sample 42:
- time = 975000
+ time = 975238
flags = 1
data = length 458, hash 51CDAA10
sample 43:
- time = 998000
+ time = 998458
flags = 1
data = length 465, hash CBA1EFD7
sample 44:
- time = 1021000
+ time = 1021678
flags = 1
data = length 446, hash D6735B8A
sample 45:
- time = 1044000
+ time = 1044897
flags = 1
data = length 10, hash A453EEBE
track 3:
diff --git a/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts b/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts
new file mode 100644
index 0000000..e9aafd7
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts
Binary files differ
diff --git a/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.0.dump b/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.0.dump
new file mode 100644
index 0000000..86b6448
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.0.dump
@@ -0,0 +1,81 @@
+seekMap:
+ isSeekable = true
+ duration = 0
+ getPosition(0) = [[timeUs=0, position=0]]
+ getPosition(1) = [[timeUs=1, position=0]]
+ getPosition(0) = [[timeUs=0, position=0]]
+ getPosition(0) = [[timeUs=0, position=0]]
+numberOfTracks = 3
+track 256:
+ total output bytes = 13650
+ sample count = 2
+ format 0:
+ id = 1/256
+ sampleMimeType = video/avc
+ codecs = avc1.64001E
+ width = 640
+ height = 426
+ initializationData:
+ data = length 29, hash 4C2CAE9C
+ data = length 9, hash D971CD89
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 12394, hash A39F5311
+ sample 1:
+ time = 100100
+ flags = 0
+ data = length 813, hash 99F7B4FA
+track 257:
+ total output bytes = 18432
+ sample count = 9
+ format 0:
+ averageBitrate = 1411500
+ id = 1/257
+ sampleMimeType = audio/vnd.dts
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 1:
+ time = 78344
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 2:
+ time = 89955
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 3:
+ time = 101566
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 4:
+ time = 113177
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 5:
+ time = 124777
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 6:
+ time = 136388
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 7:
+ time = 148000
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 8:
+ time = 159611
+ flags = 1
+ data = length 2048, hash 88866F81
+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_h264_dts_audio.ts.1.dump b/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.1.dump
new file mode 100644
index 0000000..86b6448
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.1.dump
@@ -0,0 +1,81 @@
+seekMap:
+ isSeekable = true
+ duration = 0
+ getPosition(0) = [[timeUs=0, position=0]]
+ getPosition(1) = [[timeUs=1, position=0]]
+ getPosition(0) = [[timeUs=0, position=0]]
+ getPosition(0) = [[timeUs=0, position=0]]
+numberOfTracks = 3
+track 256:
+ total output bytes = 13650
+ sample count = 2
+ format 0:
+ id = 1/256
+ sampleMimeType = video/avc
+ codecs = avc1.64001E
+ width = 640
+ height = 426
+ initializationData:
+ data = length 29, hash 4C2CAE9C
+ data = length 9, hash D971CD89
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 12394, hash A39F5311
+ sample 1:
+ time = 100100
+ flags = 0
+ data = length 813, hash 99F7B4FA
+track 257:
+ total output bytes = 18432
+ sample count = 9
+ format 0:
+ averageBitrate = 1411500
+ id = 1/257
+ sampleMimeType = audio/vnd.dts
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 1:
+ time = 78344
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 2:
+ time = 89955
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 3:
+ time = 101566
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 4:
+ time = 113177
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 5:
+ time = 124777
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 6:
+ time = 136388
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 7:
+ time = 148000
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 8:
+ time = 159611
+ flags = 1
+ data = length 2048, hash 88866F81
+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_h264_dts_audio.ts.2.dump b/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.2.dump
new file mode 100644
index 0000000..86b6448
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.2.dump
@@ -0,0 +1,81 @@
+seekMap:
+ isSeekable = true
+ duration = 0
+ getPosition(0) = [[timeUs=0, position=0]]
+ getPosition(1) = [[timeUs=1, position=0]]
+ getPosition(0) = [[timeUs=0, position=0]]
+ getPosition(0) = [[timeUs=0, position=0]]
+numberOfTracks = 3
+track 256:
+ total output bytes = 13650
+ sample count = 2
+ format 0:
+ id = 1/256
+ sampleMimeType = video/avc
+ codecs = avc1.64001E
+ width = 640
+ height = 426
+ initializationData:
+ data = length 29, hash 4C2CAE9C
+ data = length 9, hash D971CD89
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 12394, hash A39F5311
+ sample 1:
+ time = 100100
+ flags = 0
+ data = length 813, hash 99F7B4FA
+track 257:
+ total output bytes = 18432
+ sample count = 9
+ format 0:
+ averageBitrate = 1411500
+ id = 1/257
+ sampleMimeType = audio/vnd.dts
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 1:
+ time = 78344
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 2:
+ time = 89955
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 3:
+ time = 101566
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 4:
+ time = 113177
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 5:
+ time = 124777
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 6:
+ time = 136388
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 7:
+ time = 148000
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 8:
+ time = 159611
+ flags = 1
+ data = length 2048, hash 88866F81
+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_h264_dts_audio.ts.3.dump b/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.3.dump
new file mode 100644
index 0000000..86b6448
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.3.dump
@@ -0,0 +1,81 @@
+seekMap:
+ isSeekable = true
+ duration = 0
+ getPosition(0) = [[timeUs=0, position=0]]
+ getPosition(1) = [[timeUs=1, position=0]]
+ getPosition(0) = [[timeUs=0, position=0]]
+ getPosition(0) = [[timeUs=0, position=0]]
+numberOfTracks = 3
+track 256:
+ total output bytes = 13650
+ sample count = 2
+ format 0:
+ id = 1/256
+ sampleMimeType = video/avc
+ codecs = avc1.64001E
+ width = 640
+ height = 426
+ initializationData:
+ data = length 29, hash 4C2CAE9C
+ data = length 9, hash D971CD89
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 12394, hash A39F5311
+ sample 1:
+ time = 100100
+ flags = 0
+ data = length 813, hash 99F7B4FA
+track 257:
+ total output bytes = 18432
+ sample count = 9
+ format 0:
+ averageBitrate = 1411500
+ id = 1/257
+ sampleMimeType = audio/vnd.dts
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 1:
+ time = 78344
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 2:
+ time = 89955
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 3:
+ time = 101566
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 4:
+ time = 113177
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 5:
+ time = 124777
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 6:
+ time = 136388
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 7:
+ time = 148000
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 8:
+ time = 159611
+ flags = 1
+ data = length 2048, hash 88866F81
+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_h264_dts_audio.ts.unknown_length.dump b/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.unknown_length.dump
new file mode 100644
index 0000000..0e87b90
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h264_dts_audio.ts.unknown_length.dump
@@ -0,0 +1,78 @@
+seekMap:
+ isSeekable = false
+ duration = UNSET TIME
+ getPosition(0) = [[timeUs=0, position=0]]
+numberOfTracks = 3
+track 256:
+ total output bytes = 13650
+ sample count = 2
+ format 0:
+ id = 1/256
+ sampleMimeType = video/avc
+ codecs = avc1.64001E
+ width = 640
+ height = 426
+ initializationData:
+ data = length 29, hash 4C2CAE9C
+ data = length 9, hash D971CD89
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 12394, hash A39F5311
+ sample 1:
+ time = 100100
+ flags = 0
+ data = length 813, hash 99F7B4FA
+track 257:
+ total output bytes = 18432
+ sample count = 9
+ format 0:
+ averageBitrate = 1411500
+ id = 1/257
+ sampleMimeType = audio/vnd.dts
+ channelCount = 1
+ sampleRate = 44100
+ language = und
+ sample 0:
+ time = 66733
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 1:
+ time = 78344
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 2:
+ time = 89955
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 3:
+ time = 101566
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 4:
+ time = 113177
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 5:
+ time = 124777
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 6:
+ time = 136388
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 7:
+ time = 148000
+ flags = 1
+ data = length 2048, hash 88866F81
+ sample 8:
+ time = 159611
+ flags = 1
+ data = length 2048, hash 88866F81
+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_h264_no_access_unit_delimiters.ts b/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts
new file mode 100644
index 0000000..a1cc40b
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts
Binary files differ
diff --git a/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.0.dump b/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.0.dump
new file mode 100644
index 0000000..f3f3d23
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.0.dump
@@ -0,0 +1,43 @@
+seekMap:
+ isSeekable = true
+ duration = 66733
+ getPosition(0) = [[timeUs=0, position=0]]
+ getPosition(1) = [[timeUs=1, position=0]]
+ getPosition(33366) = [[timeUs=33366, position=6420]]
+ getPosition(66733) = [[timeUs=66733, position=13028]]
+numberOfTracks = 2
+track 256:
+ total output bytes = 12451
+ sample count = 4
+ format 0:
+ id = 1/256
+ sampleMimeType = video/avc
+ codecs = avc1.64001E
+ width = 640
+ height = 426
+ initializationData:
+ data = length 29, hash 4C2CAE9C
+ data = length 9, hash D971CD89
+ sample 0:
+ time = 66733
+ flags = 0
+ data = length 734, hash AF0D9485
+ sample 1:
+ time = 66733
+ flags = 1
+ data = length 10938, hash 68420875
+ sample 2:
+ time = 133466
+ flags = 0
+ data = length 6, hash 34E6CF79
+ sample 3:
+ time = 133466
+ flags = 0
+ data = length 518, hash 546C177
+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_h264_no_access_unit_delimiters.ts.1.dump b/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.1.dump
new file mode 100644
index 0000000..94d3d8d
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.1.dump
@@ -0,0 +1,43 @@
+seekMap:
+ isSeekable = true
+ duration = 66733
+ getPosition(0) = [[timeUs=0, position=0]]
+ getPosition(1) = [[timeUs=1, position=0]]
+ getPosition(33366) = [[timeUs=33366, position=6420]]
+ getPosition(66733) = [[timeUs=66733, position=13028]]
+numberOfTracks = 2
+track 256:
+ total output bytes = 12451
+ sample count = 4
+ format 0:
+ id = 1/256
+ sampleMimeType = video/avc
+ codecs = avc1.64001E
+ width = 640
+ height = 426
+ initializationData:
+ data = length 29, hash 4C2CAE9C
+ data = length 9, hash D971CD89
+ sample 0:
+ time = 88977
+ flags = 0
+ data = length 734, hash AF0D9485
+ sample 1:
+ time = 88977
+ flags = 1
+ data = length 10938, hash 68420875
+ sample 2:
+ time = 155710
+ flags = 0
+ data = length 6, hash 34E6CF79
+ sample 3:
+ time = 155710
+ flags = 0
+ data = length 518, hash 546C177
+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_h264_no_access_unit_delimiters.ts.2.dump b/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.2.dump
new file mode 100644
index 0000000..e6e8332
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.2.dump
@@ -0,0 +1,43 @@
+seekMap:
+ isSeekable = true
+ duration = 66733
+ getPosition(0) = [[timeUs=0, position=0]]
+ getPosition(1) = [[timeUs=1, position=0]]
+ getPosition(33366) = [[timeUs=33366, position=6420]]
+ getPosition(66733) = [[timeUs=66733, position=13028]]
+numberOfTracks = 2
+track 256:
+ total output bytes = 12451
+ sample count = 4
+ format 0:
+ id = 1/256
+ sampleMimeType = video/avc
+ codecs = avc1.64001E
+ width = 640
+ height = 426
+ initializationData:
+ data = length 29, hash 4C2CAE9C
+ data = length 9, hash D971CD89
+ sample 0:
+ time = 111221
+ flags = 0
+ data = length 734, hash AF0D9485
+ sample 1:
+ time = 111221
+ flags = 1
+ data = length 10938, hash 68420875
+ sample 2:
+ time = 177954
+ flags = 0
+ data = length 6, hash 34E6CF79
+ sample 3:
+ time = 177954
+ flags = 0
+ data = length 518, hash 546C177
+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_h264_no_access_unit_delimiters.ts.3.dump b/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.3.dump
new file mode 100644
index 0000000..8710d53
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.3.dump
@@ -0,0 +1,27 @@
+seekMap:
+ isSeekable = true
+ duration = 66733
+ getPosition(0) = [[timeUs=0, position=0]]
+ getPosition(1) = [[timeUs=1, position=0]]
+ getPosition(33366) = [[timeUs=33366, position=6420]]
+ getPosition(66733) = [[timeUs=66733, position=13028]]
+numberOfTracks = 2
+track 256:
+ total output bytes = 255
+ sample count = 0
+ format 0:
+ id = 1/256
+ sampleMimeType = video/avc
+ codecs = avc1.64001E
+ width = 640
+ height = 426
+ initializationData:
+ data = length 29, hash 4C2CAE9C
+ data = length 9, hash D971CD89
+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_h264_no_access_unit_delimiters.ts.unknown_length.dump b/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.unknown_length.dump
new file mode 100644
index 0000000..a23c081
--- /dev/null
+++ b/tree/testdata/src/test/assets/ts/sample_h264_no_access_unit_delimiters.ts.unknown_length.dump
@@ -0,0 +1,40 @@
+seekMap:
+ isSeekable = false
+ duration = UNSET TIME
+ getPosition(0) = [[timeUs=0, position=0]]
+numberOfTracks = 2
+track 256:
+ total output bytes = 12451
+ sample count = 4
+ format 0:
+ id = 1/256
+ sampleMimeType = video/avc
+ codecs = avc1.64001E
+ width = 640
+ height = 426
+ initializationData:
+ data = length 29, hash 4C2CAE9C
+ data = length 9, hash D971CD89
+ sample 0:
+ time = 66733
+ flags = 0
+ data = length 734, hash AF0D9485
+ sample 1:
+ time = 66733
+ flags = 1
+ data = length 10938, hash 68420875
+ sample 2:
+ time = 133466
+ flags = 0
+ data = length 6, hash 34E6CF79
+ sample 3:
+ time = 133466
+ flags = 0
+ data = length 518, hash 546C177
+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.0.dump b/tree/testdata/src/test/assets/ts/sample_h265.ts.0.dump
index 1c06207..cee71dd 100644
--- a/tree/testdata/src/test/assets/ts/sample_h265.ts.0.dump
+++ b/tree/testdata/src/test/assets/ts/sample_h265.ts.0.dump
@@ -19,119 +19,119 @@
sample 0:
time = 66666
flags = 1
- data = length 2510, hash 796A98BE
+ data = length 2517, hash 85352308
sample 1:
time = 100000
flags = 0
- data = length 1219, hash 131AA4E4
+ data = length 1226, hash 11D564DA
sample 2:
time = 266666
flags = 0
- data = length 7810, hash 3F881DB9
+ data = length 7817, hash 50D15703
sample 3:
time = 200000
flags = 0
- data = length 2306, hash 9A77959C
+ data = length 2313, hash ECA5AEE6
sample 4:
time = 133333
flags = 0
- data = length 1058, hash B887F7EF
+ data = length 1065, hash 8720A939
sample 5:
time = 166666
flags = 0
- data = length 98, hash D95BF6E3
+ data = length 105, hash 3A3A582D
sample 6:
time = 233333
flags = 0
- data = length 61, hash 574C41C3
+ data = length 68, hash FC241239
sample 7:
time = 433333
flags = 0
- data = length 296, hash E92DB288
+ data = length 303, hash 41B28452
sample 8:
time = 366666
flags = 0
- data = length 137, hash 586DADD6
+ data = length 144, hash 60BBCD4C
sample 9:
time = 300000
flags = 0
- data = length 218, hash 91E82C9F
+ data = length 225, hash E0FAD7E9
sample 10:
time = 333333
flags = 0
- data = length 177, hash 4A4FEEC0
+ data = length 184, hash A3A6E036
sample 11:
time = 400000
flags = 0
- data = length 82, hash 2E2ADD8
+ data = length 89, hash 43B0E322
sample 12:
time = 533333
flags = 0
- data = length 290, hash 63CF7D90
+ data = length 297, hash 6D9FEEDA
sample 13:
time = 500000
flags = 0
- data = length 268, hash E8CBAC11
+ data = length 275, hash 27430DB
sample 14:
time = 466666
flags = 0
- data = length 178, hash C5B1613E
+ data = length 185, hash 97389E88
sample 15:
time = 566666
flags = 0
- data = length 271, hash 76652FC5
+ data = length 278, hash 5819FEBB
sample 16:
time = 733333
flags = 0
- data = length 257, hash 960B5DF4
+ data = length 264, hash 8545F36A
sample 17:
time = 666666
flags = 0
- data = length 206, hash 87358D00
+ data = length 213, hash 52C7574A
sample 18:
time = 600000
flags = 0
- data = length 130, hash 4D7A038D
+ data = length 137, hash D4F0BCD7
sample 19:
time = 633333
flags = 0
- data = length 114, hash 2B3C616E
+ data = length 121, hash BE52EEB8
sample 20:
time = 700000
flags = 0
- data = length 95, hash 37D79559
+ data = length 102, hash 6AA3C84F
sample 21:
time = 900000
flags = 0
- data = length 233, hash 80308C9E
+ data = length 240, hash 8E3CA414
sample 22:
time = 833333
flags = 0
- data = length 203, hash E70BA5F2
+ data = length 210, hash 5D050FE8
sample 23:
time = 766666
flags = 0
- data = length 95, hash BA6FA2D3
+ data = length 102, hash ED3BD5C9
sample 24:
time = 800000
flags = 0
- data = length 103, hash 51291041
+ data = length 110, hash CF65ED37
sample 25:
time = 866666
flags = 0
- data = length 111, hash EE9DCFC9
+ data = length 118, hash BA0156BF
sample 26:
time = 1033333
flags = 0
- data = length 253, hash D0CEFBA7
+ data = length 260, hash ED6ABC1D
sample 27:
time = 966666
flags = 0
- data = length 134, hash 8802EEF0
+ data = length 141, hash 9787F33A
sample 28:
time = 933333
flags = 0
- data = length 80, hash C635D9C2
+ data = length 87, hash EEC4D98C
track 8448:
total output bytes = 0
sample count = 0
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
index 95d8d9b..a9db2af 100644
--- a/tree/testdata/src/test/assets/ts/sample_h265.ts.1.dump
+++ b/tree/testdata/src/test/assets/ts/sample_h265.ts.1.dump
@@ -19,83 +19,83 @@
sample 0:
time = 300000
flags = 0
- data = length 218, hash 91E82C9F
+ data = length 225, hash E0FAD7E9
sample 1:
time = 333333
flags = 0
- data = length 177, hash 4A4FEEC0
+ data = length 184, hash A3A6E036
sample 2:
time = 400000
flags = 0
- data = length 82, hash 2E2ADD8
+ data = length 89, hash 43B0E322
sample 3:
time = 533333
flags = 0
- data = length 290, hash 63CF7D90
+ data = length 297, hash 6D9FEEDA
sample 4:
time = 500000
flags = 0
- data = length 268, hash E8CBAC11
+ data = length 275, hash 27430DB
sample 5:
time = 466666
flags = 0
- data = length 178, hash C5B1613E
+ data = length 185, hash 97389E88
sample 6:
time = 566666
flags = 0
- data = length 271, hash 76652FC5
+ data = length 278, hash 5819FEBB
sample 7:
time = 733333
flags = 0
- data = length 257, hash 960B5DF4
+ data = length 264, hash 8545F36A
sample 8:
time = 666666
flags = 0
- data = length 206, hash 87358D00
+ data = length 213, hash 52C7574A
sample 9:
time = 600000
flags = 0
- data = length 130, hash 4D7A038D
+ data = length 137, hash D4F0BCD7
sample 10:
time = 633333
flags = 0
- data = length 114, hash 2B3C616E
+ data = length 121, hash BE52EEB8
sample 11:
time = 700000
flags = 0
- data = length 95, hash 37D79559
+ data = length 102, hash 6AA3C84F
sample 12:
time = 900000
flags = 0
- data = length 233, hash 80308C9E
+ data = length 240, hash 8E3CA414
sample 13:
time = 833333
flags = 0
- data = length 203, hash E70BA5F2
+ data = length 210, hash 5D050FE8
sample 14:
time = 766666
flags = 0
- data = length 95, hash BA6FA2D3
+ data = length 102, hash ED3BD5C9
sample 15:
time = 800000
flags = 0
- data = length 103, hash 51291041
+ data = length 110, hash CF65ED37
sample 16:
time = 866666
flags = 0
- data = length 111, hash EE9DCFC9
+ data = length 118, hash BA0156BF
sample 17:
time = 1033333
flags = 0
- data = length 253, hash D0CEFBA7
+ data = length 260, hash ED6ABC1D
sample 18:
time = 966666
flags = 0
- data = length 134, hash 8802EEF0
+ data = length 141, hash 9787F33A
sample 19:
time = 933333
flags = 0
- data = length 80, hash C635D9C2
+ data = length 87, hash EEC4D98C
track 8448:
total output bytes = 0
sample count = 0
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
index 9080f5c..7f694a3 100644
--- a/tree/testdata/src/test/assets/ts/sample_h265.ts.2.dump
+++ b/tree/testdata/src/test/assets/ts/sample_h265.ts.2.dump
@@ -19,47 +19,47 @@
sample 0:
time = 600000
flags = 0
- data = length 130, hash 4D7A038D
+ data = length 137, hash D4F0BCD7
sample 1:
time = 633333
flags = 0
- data = length 114, hash 2B3C616E
+ data = length 121, hash BE52EEB8
sample 2:
time = 700000
flags = 0
- data = length 95, hash 37D79559
+ data = length 102, hash 6AA3C84F
sample 3:
time = 900000
flags = 0
- data = length 233, hash 80308C9E
+ data = length 240, hash 8E3CA414
sample 4:
time = 833333
flags = 0
- data = length 203, hash E70BA5F2
+ data = length 210, hash 5D050FE8
sample 5:
time = 766666
flags = 0
- data = length 95, hash BA6FA2D3
+ data = length 102, hash ED3BD5C9
sample 6:
time = 800000
flags = 0
- data = length 103, hash 51291041
+ data = length 110, hash CF65ED37
sample 7:
time = 866666
flags = 0
- data = length 111, hash EE9DCFC9
+ data = length 118, hash BA0156BF
sample 8:
time = 1033333
flags = 0
- data = length 253, hash D0CEFBA7
+ data = length 260, hash ED6ABC1D
sample 9:
time = 966666
flags = 0
- data = length 134, hash 8802EEF0
+ data = length 141, hash 9787F33A
sample 10:
time = 933333
flags = 0
- data = length 80, hash C635D9C2
+ data = length 87, hash EEC4D98C
track 8448:
total output bytes = 0
sample count = 0
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
index 5fe7bce..7926850 100644
--- a/tree/testdata/src/test/assets/ts/sample_h265.ts.3.dump
+++ b/tree/testdata/src/test/assets/ts/sample_h265.ts.3.dump
@@ -19,11 +19,11 @@
sample 0:
time = 966666
flags = 0
- data = length 134, hash 8802EEF0
+ data = length 141, hash 9787F33A
sample 1:
time = 933333
flags = 0
- data = length 80, hash C635D9C2
+ data = length 87, hash EEC4D98C
track 8448:
total output bytes = 0
sample count = 0
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
index 733be6f..2df0a73 100644
--- 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
@@ -16,119 +16,119 @@
sample 0:
time = 66666
flags = 1
- data = length 2510, hash 796A98BE
+ data = length 2517, hash 85352308
sample 1:
time = 100000
flags = 0
- data = length 1219, hash 131AA4E4
+ data = length 1226, hash 11D564DA
sample 2:
time = 266666
flags = 0
- data = length 7810, hash 3F881DB9
+ data = length 7817, hash 50D15703
sample 3:
time = 200000
flags = 0
- data = length 2306, hash 9A77959C
+ data = length 2313, hash ECA5AEE6
sample 4:
time = 133333
flags = 0
- data = length 1058, hash B887F7EF
+ data = length 1065, hash 8720A939
sample 5:
time = 166666
flags = 0
- data = length 98, hash D95BF6E3
+ data = length 105, hash 3A3A582D
sample 6:
time = 233333
flags = 0
- data = length 61, hash 574C41C3
+ data = length 68, hash FC241239
sample 7:
time = 433333
flags = 0
- data = length 296, hash E92DB288
+ data = length 303, hash 41B28452
sample 8:
time = 366666
flags = 0
- data = length 137, hash 586DADD6
+ data = length 144, hash 60BBCD4C
sample 9:
time = 300000
flags = 0
- data = length 218, hash 91E82C9F
+ data = length 225, hash E0FAD7E9
sample 10:
time = 333333
flags = 0
- data = length 177, hash 4A4FEEC0
+ data = length 184, hash A3A6E036
sample 11:
time = 400000
flags = 0
- data = length 82, hash 2E2ADD8
+ data = length 89, hash 43B0E322
sample 12:
time = 533333
flags = 0
- data = length 290, hash 63CF7D90
+ data = length 297, hash 6D9FEEDA
sample 13:
time = 500000
flags = 0
- data = length 268, hash E8CBAC11
+ data = length 275, hash 27430DB
sample 14:
time = 466666
flags = 0
- data = length 178, hash C5B1613E
+ data = length 185, hash 97389E88
sample 15:
time = 566666
flags = 0
- data = length 271, hash 76652FC5
+ data = length 278, hash 5819FEBB
sample 16:
time = 733333
flags = 0
- data = length 257, hash 960B5DF4
+ data = length 264, hash 8545F36A
sample 17:
time = 666666
flags = 0
- data = length 206, hash 87358D00
+ data = length 213, hash 52C7574A
sample 18:
time = 600000
flags = 0
- data = length 130, hash 4D7A038D
+ data = length 137, hash D4F0BCD7
sample 19:
time = 633333
flags = 0
- data = length 114, hash 2B3C616E
+ data = length 121, hash BE52EEB8
sample 20:
time = 700000
flags = 0
- data = length 95, hash 37D79559
+ data = length 102, hash 6AA3C84F
sample 21:
time = 900000
flags = 0
- data = length 233, hash 80308C9E
+ data = length 240, hash 8E3CA414
sample 22:
time = 833333
flags = 0
- data = length 203, hash E70BA5F2
+ data = length 210, hash 5D050FE8
sample 23:
time = 766666
flags = 0
- data = length 95, hash BA6FA2D3
+ data = length 102, hash ED3BD5C9
sample 24:
time = 800000
flags = 0
- data = length 103, hash 51291041
+ data = length 110, hash CF65ED37
sample 25:
time = 866666
flags = 0
- data = length 111, hash EE9DCFC9
+ data = length 118, hash BA0156BF
sample 26:
time = 1033333
flags = 0
- data = length 253, hash D0CEFBA7
+ data = length 260, hash ED6ABC1D
sample 27:
time = 966666
flags = 0
- data = length 134, hash 8802EEF0
+ data = length 141, hash 9787F33A
sample 28:
time = 933333
flags = 0
- data = length 80, hash C635D9C2
+ data = length 87, hash EEC4D98C
track 8448:
total output bytes = 0
sample count = 0
diff --git a/tree/testdata/src/test/assets/webvtt/with_positioning b/tree/testdata/src/test/assets/webvtt/with_positioning
index 7db327c..c4c897a 100644
--- a/tree/testdata/src/test/assets/webvtt/with_positioning
+++ b/tree/testdata/src/test/assets/webvtt/with_positioning
@@ -2,7 +2,7 @@
NOTE Position with percentage and position alignment
-00:00:00.000 --> 00:00:01.234 position:10%,start align:start size:35%
+00:00:00.000 --> 00:00:01.234 position:60%,end align:start size:35%
This is the first subtitle.
NOTE Wrong position provided. It should be provided as
@@ -30,3 +30,13 @@
00:10.000 --> 00:11.000 align:center
This is the sixth subtitle.
+
+NOTE In older drafts position alignment could be start,middle,end
+
+00:12.000 --> 00:13.000 position:20%,start
+This is the seventh subtitle.
+
+NOTE In the released spec position alignment can be line-left,center,line-right
+
+00:14.000 --> 00:15.000 position:70%,line-right
+This is the eighth subtitle.
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 269a992..808cb0c 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
@@ -27,7 +27,10 @@
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
/**
* Assertion methods for {@link Extractor}.
@@ -35,8 +38,60 @@
public final class ExtractorAsserts {
/**
- * A factory for {@link Extractor} instances.
+ * Returns a list of arrays containing {@link Config} objects to exercise different extractor
+ * paths.
+ *
+ * <p>This is intended to be used from tests using {@code ParameterizedRobolectricTestRunner} or
+ * {@code org.junit.runners.Parameterized}.
*/
+ public static List<Object[]> configs() {
+ return Arrays.asList(
+ new Object[] {new Config(true, false, false, false)},
+ new Object[] {new Config(true, false, false, true)},
+ new Object[] {new Config(true, false, true, false)},
+ new Object[] {new Config(true, false, true, true)},
+ new Object[] {new Config(true, true, false, false)},
+ new Object[] {new Config(true, true, false, true)},
+ new Object[] {new Config(true, true, true, false)},
+ new Object[] {new Config(true, true, true, true)},
+ new Object[] {new Config(false, false, false, false)});
+ }
+
+ /** A config of different environments to simulate and extractor behaviour to test. */
+ public static class Config {
+
+ /**
+ * Whether to sniff the data by calling {@link Extractor#sniff(ExtractorInput)} prior to
+ * consuming it.
+ */
+ public final boolean sniffFirst;
+ /** Whether to simulate IO errors. */
+ public final boolean simulateIOErrors;
+ /** Whether to simulate unknown input length. */
+ public final boolean simulateUnknownLength;
+ /** Whether to simulate partial reads. */
+ public final boolean simulatePartialReads;
+
+ private Config(
+ boolean sniffFirst,
+ boolean simulateIOErrors,
+ boolean simulateUnknownLength,
+ boolean simulatePartialReads) {
+ this.sniffFirst = sniffFirst;
+ this.simulateIOErrors = simulateIOErrors;
+ this.simulateUnknownLength = simulateUnknownLength;
+ this.simulatePartialReads = simulatePartialReads;
+ }
+
+ @Override
+ public String toString() {
+ return Util.formatInvariant(
+ "sniff=%s,ioErr=%s,unknownLen=%s,partRead=%s",
+ sniffFirst, simulateIOErrors, simulateUnknownLength, simulatePartialReads);
+ }
+ }
+
+ /** A factory for {@link Extractor} instances. */
public interface ExtractorFactory {
Extractor create();
}
@@ -66,8 +121,7 @@
}
/**
- * Asserts that an extractor behaves correctly given valid input data. Can only be used from
- * Robolectric tests.
+ * Asserts that an extractor behaves correctly given valid input data.
*
* <ul>
* <li>Calls {@link Extractor#seek(long, long)} and {@link Extractor#release()} without calling
@@ -81,8 +135,8 @@
* @param file The path to the input sample.
* @throws IOException If reading from the input fails.
*/
- public static void assertBehavior(ExtractorFactory factory, String file) throws IOException {
- assertBehavior(factory, file, ApplicationProvider.getApplicationContext());
+ public static void assertAllBehaviors(ExtractorFactory factory, String file) throws IOException {
+ assertAllBehaviors(factory, file, file);
}
/**
@@ -98,41 +152,71 @@
* @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
* class which is to be tested.
* @param file The path to the input sample.
- * @param context To be used to load the sample file.
- * @throws IOException If reading from the input fails.
- */
- public static void assertBehavior(ExtractorFactory factory, String file, Context context)
- throws IOException {
- assertBehavior(factory, file, context, file);
- }
-
- /**
- * Asserts that an extractor behaves correctly given valid input data:
- *
- * <ul>
- * <li>Calls {@link Extractor#seek(long, long)} and {@link Extractor#release()} without calling
- * {@link Extractor#init(ExtractorOutput)} to check these calls do not fail.
- * <li>Calls {@link #assertOutput(Extractor, String, byte[], Context, boolean, boolean, boolean,
- * boolean)} with all possible combinations of "simulate" parameters.
- * </ul>
- *
- * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
- * class which is to be tested.
- * @param file The path to the input sample.
- * @param context To be used to load the sample file.
* @param dumpFilesPrefix The dump files prefix appended to the dump files path.
* @throws IOException If reading from the input fails.
*/
+ public static void assertAllBehaviors(
+ ExtractorFactory factory, String file, String dumpFilesPrefix) throws IOException {
+ // Check behavior prior to initialization.
+ Extractor extractor = factory.create();
+ extractor.seek(0, 0);
+ extractor.release();
+ // Assert output.
+ Context context = ApplicationProvider.getApplicationContext();
+ byte[] fileData = TestUtil.getByteArray(context, file);
+ assertOutput(factory, dumpFilesPrefix, fileData, context);
+ }
+
+ /**
+ * Asserts that an extractor consumes valid input data successfully under the conditions specified
+ * by {@code config}.
+ *
+ * <p>The output of the extractor is compared against prerecorded dump files whose names are
+ * derived from the {@code file} parameter.
+ *
+ * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
+ * class which is to be tested.
+ * @param file The path to the input sample.
+ * @param config Details on the environment to simulate and behaviours to assert.
+ * @throws IOException If reading from the input fails.
+ */
+ public static void assertBehavior(ExtractorFactory factory, String file, Config config)
+ throws IOException {
+ assertBehavior(factory, file, config, file);
+ }
+
+ /**
+ * Asserts that an extractor consumes valid input data successfully successfully under the
+ * conditions specified by {@code config}.
+ *
+ * <p>The output of the extractor is compared against prerecorded dump files.
+ *
+ * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
+ * class which is to be tested.
+ * @param file The path to the input sample.
+ * @param config Details on the environment to simulate and behaviours to assert.
+ * @param dumpFilesPrefix The dump files prefix prepended to the dump files path.
+ * @throws IOException If reading from the input fails.
+ */
public static void assertBehavior(
- ExtractorFactory factory, String file, Context context, String dumpFilesPrefix)
+ ExtractorFactory factory, String file, Config config, String dumpFilesPrefix)
throws IOException {
// Check behavior prior to initialization.
Extractor extractor = factory.create();
extractor.seek(0, 0);
extractor.release();
// Assert output.
+ Context context = ApplicationProvider.getApplicationContext();
byte[] fileData = TestUtil.getByteArray(context, file);
- assertOutput(factory, dumpFilesPrefix, fileData, context);
+ assertOutput(
+ factory.create(),
+ dumpFilesPrefix,
+ fileData,
+ context,
+ config.sniffFirst,
+ config.simulateIOErrors,
+ config.simulateUnknownLength,
+ config.simulatePartialReads);
}
/**
@@ -148,7 +232,7 @@
* @param context To be used to load the sample file.
* @throws IOException If reading from the input fails.
*/
- public static void assertOutput(
+ private static void assertOutput(
ExtractorFactory factory, String dumpFilesPrefix, byte[] data, Context context)
throws IOException {
assertOutput(factory.create(), dumpFilesPrefix, data, context, true, false, false, false);
@@ -163,11 +247,11 @@
}
/**
- * Asserts that {@code extractor} consumes {@code data} successfully and that its output for
- * various initial seek times and for a known and unknown length matches prerecorded dump files.
+ * Asserts that an extractor consumes valid input data successfully under the specified
+ * conditions.
*
* @param extractor The {@link Extractor} to be tested.
- * @param dumpFilesPrefix The dump files prefix appended to the dump files path.
+ * @param dumpFilesPrefix The dump files prefix prepended to the dump files path.
* @param data Content of the input file.
* @param context To be used to load the sample file.
* @param sniffFirst Whether to sniff the data by calling {@link Extractor#sniff(ExtractorInput)}
@@ -178,7 +262,7 @@
* @return The {@link FakeExtractorOutput} used in the test.
* @throws IOException If reading from the input fails.
*/
- public static FakeExtractorOutput assertOutput(
+ private static FakeExtractorOutput assertOutput(
Extractor extractor,
String dumpFilesPrefix,
byte[] data,
diff --git a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java
index 5cefbff..4486630 100644
--- a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java
+++ b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java
@@ -27,6 +27,7 @@
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Documented;
@@ -34,11 +35,14 @@
import java.lang.annotation.RetentionPolicy;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
-/**
- * A fake {@link ExtractorOutput}.
- */
+/** A fake {@link ExtractorOutput}. */
public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpable {
+ private static final String DUMP_UPDATE_INSTRUCTIONS =
+ "To update the dump file, change FakeExtractorOutput#DUMP_FILE_ACTION to WRITE_TO_LOCAL (for"
+ + " Robolectric tests) or WRITE_TO_DEVICE (for instrumentation tests) and re-run the"
+ + " test.";
+
/**
* Possible actions to take with the dumps generated from this {@code FakeExtractorOutput} in
* {@link #assertOutput(Context, String)}.
@@ -128,13 +132,15 @@
String actual = new Dumper().add(this).toString();
if (DUMP_FILE_ACTION == COMPARE_WITH_EXISTING) {
- String expected = TestUtil.getString(context, dumpFile);
+ String expected;
+ try {
+ expected = TestUtil.getString(context, dumpFile);
+ } catch (FileNotFoundException e) {
+ throw new IOException("Dump file not found. " + DUMP_UPDATE_INSTRUCTIONS, e);
+ }
assertWithMessage(
- "Extractor output doesn't match golden file: %s\n"
- + "To update the golden, change FakeExtractorOutput#DUMP_FILE_ACTION to"
- + " WRITE_TO_LOCAL (for Robolectric tests) or WRITE_TO_DEVICE (for"
- + " instrumentation tests) and re-run the test.",
- dumpFile)
+ "Extractor output doesn't match dump file: %s\n%s",
+ dumpFile, DUMP_UPDATE_INSTRUCTIONS)
.that(actual)
.isEqualTo(expected);
} else {
diff --git a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java
index 03a52cf..3bd0b6b 100644
--- a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java
+++ b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java
@@ -23,6 +23,7 @@
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.SeekParameters;
+import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleStream;
@@ -121,15 +122,14 @@
@Override
public synchronized void prepare(Callback callback, long positionUs) {
eventDispatcher.loadStarted(
- FAKE_DATA_SPEC,
+ new LoadEventInfo(FAKE_DATA_SPEC, SystemClock.elapsedRealtime()),
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
- /* mediaEndTimeUs = */ C.TIME_UNSET,
- SystemClock.elapsedRealtime());
+ /* mediaEndTimeUs = */ C.TIME_UNSET);
prepareCallback = callback;
if (deferOnPrepared) {
playerHandler = Util.createHandler();
@@ -273,18 +273,19 @@
prepared = true;
Util.castNonNull(prepareCallback).onPrepared(this);
eventDispatcher.loadCompleted(
- FAKE_DATA_SPEC,
- FAKE_DATA_SPEC.uri,
- /* responseHeaders= */ Collections.emptyMap(),
+ new LoadEventInfo(
+ FAKE_DATA_SPEC,
+ FAKE_DATA_SPEC.uri,
+ /* responseHeaders= */ Collections.emptyMap(),
+ SystemClock.elapsedRealtime(),
+ /* loadDurationMs= */ 0,
+ /* bytesLoaded= */ 100),
C.DATA_TYPE_MEDIA,
C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ 0,
- /* mediaEndTimeUs = */ C.TIME_UNSET,
- SystemClock.elapsedRealtime(),
- /* loadDurationMs= */ 0,
- /* bytesLoaded= */ 100);
+ /* mediaEndTimeUs = */ C.TIME_UNSET);
}
}
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 b3c6bf7..fba8a9d 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
@@ -72,7 +72,9 @@
}
@Override
- public int sampleData(DataReader input, int length, boolean allowEndOfInput) throws IOException {
+ public int sampleData(
+ DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart)
+ throws IOException {
byte[] newData = new byte[length];
int bytesAppended = input.read(newData, 0, length);
if (bytesAppended == C.RESULT_END_OF_INPUT) {
@@ -87,7 +89,7 @@
}
@Override
- public void sampleData(ParsableByteArray data, int length) {
+ public void sampleData(ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) {
byte[] newData = new byte[length];
data.readBytes(newData, 0, length);
sampleData = TestUtil.joinByteArrays(sampleData, newData);
@@ -294,6 +296,7 @@
addIfNonDefault(dumper, "subsampleOffsetUs", format -> format.subsampleOffsetUs);
addIfNonDefault(dumper, "selectionFlags", format -> format.selectionFlags);
addIfNonDefault(dumper, "language", format -> format.language);
+ addIfNonDefault(dumper, "label", format -> format.label);
if (format.drmInitData != null) {
dumper.add("drmInitData", format.drmInitData.hashCode());
}
diff --git a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java
index 27d6b08..1972801 100644
--- a/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java
+++ b/tree/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java
@@ -55,6 +55,17 @@
private MediaPeriodAsserts() {}
/**
+ * Prepares the {@link MediaPeriod} and asserts that it provides the specified track groups.
+ *
+ * @param mediaPeriod The {@link MediaPeriod} to test.
+ * @param expectedGroups The expected track groups.
+ */
+ public static void assertTrackGroups(MediaPeriod mediaPeriod, TrackGroupArray expectedGroups) {
+ TrackGroupArray actualGroups = prepareAndGetTrackGroups(mediaPeriod);
+ assertThat(actualGroups).isEqualTo(expectedGroups);
+ }
+
+ /**
* Asserts that the values returns by {@link MediaPeriod#getStreamKeys(List)} are compatible with
* a {@link FilterableManifest} using these stream keys.
*
@@ -85,7 +96,7 @@
int periodIndex,
@Nullable String ignoredMimeType) {
MediaPeriod mediaPeriod = mediaPeriodFactory.createMediaPeriod(manifest, periodIndex);
- TrackGroupArray trackGroupArray = getTrackGroups(mediaPeriod);
+ TrackGroupArray trackGroupArray = prepareAndGetTrackGroups(mediaPeriod);
// Create test vector of query test selections:
// - One selection with one track per group, two tracks or all tracks.
@@ -146,7 +157,7 @@
// The filtered manifest should only have one period left.
MediaPeriod filteredMediaPeriod =
mediaPeriodFactory.createMediaPeriod(filteredManifest, /* periodIndex= */ 0);
- TrackGroupArray filteredTrackGroupArray = getTrackGroups(filteredMediaPeriod);
+ TrackGroupArray filteredTrackGroupArray = prepareAndGetTrackGroups(filteredMediaPeriod);
for (TrackSelection trackSelection : testSelection) {
if (ignoredMimeType != null
&& ignoredMimeType.equals(trackSelection.getFormat(0).sampleMimeType)) {
@@ -186,7 +197,7 @@
return true;
}
- private static TrackGroupArray getTrackGroups(MediaPeriod mediaPeriod) {
+ private static TrackGroupArray prepareAndGetTrackGroups(MediaPeriod mediaPeriod) {
AtomicReference<TrackGroupArray> trackGroupArray = new AtomicReference<>();
DummyMainThread dummyMainThread = new DummyMainThread();
ConditionVariable preparedCondition = new ConditionVariable();
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 ad36635..9d431b7 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
@@ -23,6 +23,7 @@
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
@@ -36,6 +37,7 @@
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Supplier;
+import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -78,6 +80,7 @@
@Nullable private Renderer[] renderers;
@Nullable private RenderersFactory renderersFactory;
private boolean useLazyPreparation;
+ private boolean throwWhenStuckBuffering;
private @MonotonicNonNull Looper looper;
public Builder(Context context) {
@@ -248,6 +251,19 @@
}
/**
+ * Sets whether the player should throw when it detects it's stuck buffering.
+ *
+ * <p>This method is experimental, and will be renamed or removed in a future release.
+ *
+ * @param throwWhenStuckBuffering Whether to throw when the player detects it's stuck buffering.
+ * @return This builder.
+ */
+ public Builder experimental_setThrowWhenStuckBuffering(boolean throwWhenStuckBuffering) {
+ this.throwWhenStuckBuffering = throwWhenStuckBuffering;
+ return this;
+ }
+
+ /**
* Builds an {@link SimpleExoPlayer} using the provided values or their defaults.
*
* @return The built {@link ExoPlayerTestRunner}.
@@ -281,6 +297,7 @@
.setClock(clock)
.setUseLazyPreparation(useLazyPreparation)
.setLooper(looper)
+ .experimental_setThrowWhenStuckBuffering(throwWhenStuckBuffering)
.build();
}
}
@@ -291,8 +308,7 @@
* Run tasks of the main {@link Looper} until the {@code player}'s state reaches the {@code
* expectedState}.
*/
- public static void runUntilPlaybackState(
- SimpleExoPlayer player, @Player.State int expectedState) {
+ public static void runUntilPlaybackState(Player player, @Player.State int expectedState) {
verifyMainTestThread(player);
if (player.getPlaybackState() == expectedState) {
return;
@@ -318,7 +334,7 @@
* Player.EventListener#onPlaybackSpeedChanged} callback with that matches {@code
* expectedPlayWhenReady}.
*/
- public static void runUntilPlayWhenReady(SimpleExoPlayer player, boolean expectedPlayWhenReady) {
+ public static void runUntilPlayWhenReady(Player player, boolean expectedPlayWhenReady) {
verifyMainTestThread(player);
if (player.getPlayWhenReady() == expectedPlayWhenReady) {
return;
@@ -343,13 +359,13 @@
* Run tasks of the main {@link Looper} until the {@code player} calls the {@link
* Player.EventListener#onTimelineChanged} callback.
*
- * @param player The {@link SimpleExoPlayer}.
+ * @param player The {@link Player}.
* @param expectedTimeline A specific {@link Timeline} to wait for, or null if any timeline is
* accepted.
* @return The received {@link Timeline}.
*/
public static Timeline runUntilTimelineChanged(
- SimpleExoPlayer player, @Nullable Timeline expectedTimeline) {
+ Player player, @Nullable Timeline expectedTimeline) {
verifyMainTestThread(player);
if (expectedTimeline != null && expectedTimeline.equals(player.getCurrentTimeline())) {
@@ -378,7 +394,7 @@
* Player.DiscontinuityReason}.
*/
public static void runUntilPositionDiscontinuity(
- SimpleExoPlayer player, @Player.DiscontinuityReason int expectedReason) {
+ Player player, @Player.DiscontinuityReason int expectedReason) {
AtomicBoolean receivedCallback = new AtomicBoolean(false);
Player.EventListener listener =
new Player.EventListener() {
@@ -398,10 +414,10 @@
* Run tasks of the main {@link Looper} until the {@code player} calls the {@link
* Player.EventListener#onPlayerError} callback.
*
- * @param player The {@link SimpleExoPlayer}.
+ * @param player The {@link Player}.
* @return The raised error.
*/
- public static ExoPlaybackException runUntilError(SimpleExoPlayer player) {
+ public static ExoPlaybackException runUntilError(Player player) {
verifyMainTestThread(player);
AtomicReference<ExoPlaybackException> receivedError = new AtomicReference<>();
Player.EventListener listener =
@@ -436,6 +452,23 @@
runUntil(() -> receivedCallback.get());
}
+ /**
+ * Runs tasks of the main {@link Looper} until the {@code player} handled all previously issued
+ * commands completely on the internal playback thread.
+ */
+ public static void runUntilPendingCommandsAreFullyHandled(ExoPlayer player) {
+ verifyMainTestThread(player);
+ // Send message to player that will arrive after all other pending commands. Thus, the message
+ // execution on the app thread will also happen after all other pending command
+ // acknowledgements have arrived back on the app thread.
+ AtomicBoolean receivedMessageCallback = new AtomicBoolean(false);
+ player
+ .createMessage((type, data) -> receivedMessageCallback.set(true))
+ .setHandler(Util.createHandler())
+ .send();
+ runUntil(() -> receivedMessageCallback.get());
+ }
+
/** Run tasks of the main {@link Looper} until the {@code condition} returns {@code true}. */
public static void runUntil(Supplier<Boolean> condition) {
verifyMainTestThread();
@@ -451,7 +484,7 @@
}
}
- private static void verifyMainTestThread(SimpleExoPlayer player) {
+ private static void verifyMainTestThread(Player player) {
if (Looper.myLooper() != Looper.getMainLooper()
|| player.getApplicationLooper() != Looper.getMainLooper()) {
throw new IllegalStateException();