Merge branch 'main' into rtp-h263

This commit is contained in:
Rakesh Kumar 2022-05-05 14:36:22 +05:30 committed by GitHub
commit ffa04ea949
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
231 changed files with 8441 additions and 2437 deletions

View File

@ -1,42 +0,0 @@
---
name: Bug report
about: Issue template for a bug report.
title: ''
labels: bug, needs triage
assignees: ''
---
We can only process bug reports that are actionable. Unclear bug reports or
reports with insufficient information may not get attention.
Before filing a bug:
-------------------------
- Search existing issues, including issues that are closed:
https://github.com/androidx/media/issues?q=is%3Aissue
- For ExoPlayer-related bugs, please also check the ExoPlayer tracker:
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
When reporting a bug:
-------------------------
Describe how the issue can be reproduced, ideally using one of the demo apps
or a small sample app that youre able to share as source code on GitHub. To
increase the chance of your issue getting attention, please also include:
- Clear reproduction steps including observed and expected behavior
- Output of running "adb bugreport" in the console shortly after encountering
the issue
- URI to test content for reproduction
- For protected content:
- DRM scheme and license server URL
- Authentication HTTP headers
- AndroidX Media version number
- Android version
- Android device
If there's something you don't want to post publicly, please submit the issue,
then email the link/bug report to dev.exoplayer@gmail.com using a subject in the
format "Issue #1234", where #1234 is your issue number (we don't reply to
emails).

99
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@ -0,0 +1,99 @@
name: Bug Report
description: Report a bug in the Media3 library
labels: ["bug", "needs triage"]
body:
- type: markdown
attributes:
value: |
We can only process bug reports that are actionable. Unclear bug reports or reports with insufficient information may not get attention.
Before filing a bug:
-------------------------
- Search existing issues, including issues that are closed: https://github.com/androidx/media/issues?q=is%3Aissue
- For ExoPlayer-related bugs, please also check the ExoPlayer tracker: https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- type: dropdown
attributes:
label: Media3 Version
description: What version of Media3 are you using?
options:
- 1.0.0-alpha03
- 1.0.0-alpha02
- 1.0.0-alpha01
validations:
required: true
- type: textarea
attributes:
label: Devices that reproduce the issue
placeholder: |
Example:
* Pixel 4 running Android 12
* Samsung S21 running Android 11
validations:
required: true
- type: textarea
attributes:
label: Devices that do not reproduce the issue
placeholder: |
Example:
* Pixel 3 running Android Pie
- type: dropdown
attributes:
label: Reproducible in the demo app?
description: Please try and reproduce the issue in the [Media3 demo app](https://github.com/androidx/media/tree/release/demos/main).
options:
- "Yes"
- "No"
- Not tested
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: Clear and complete steps we can use to reproduce the problem
placeholder: |
Example:
1. Play the attached media in the demo app
2. Seek forward 10s
validations:
required: true
- type: textarea
attributes:
label: Expected result
placeholder: |
Example:
The media plays successfully
validations:
required: true
- type: textarea
attributes:
label: Actual result
placeholder: |
Example:
Playback crashes with the following stack trace:
...
validations:
required: true
- type: textarea
attributes:
label: Media
description: |
Media we can use to reproduce the problem. Either:
* Attach a file here
* Include a media URL
* Refer to a piece of media from the demo app (e.g. `Misc > Dizzy (MP4)`)
* If you don't want to post media publicly please email the info to dev.exoplayer@gmail.com with subject 'Issue #\<issuenumber\>' after filing this issue, and note that you will do this here.
* If you are certain the issue does not depend on the media being played, enter "Not applicable" here.
For DRM-protected media please also include the scheme and license server URL.
validations:
required: true
- type: checkboxes
attributes:
label: Bug Report
description: |
After filing this issue please run `adb bugreport` shortly after reproducing the problem (ideally in the [demo app](https://github.com/androidx/media/tree/release/demos/main)) to capture a zip file, and email this to dev.exoplayer@gmail.com with subject 'Issue #\<issuenumber\>'.
**Note:** Logcat output is **not** the same as a full bug report, and is often missing information that's useful for diagnosing issues. Please ensure you're sending a full bug report zip file.
options:
- label: You will email the zip file produced by `adb bugreport` to dev.exoplayer@gmail.com after filing this issue.

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -15,10 +15,21 @@
* Track selection:
* Flatten `TrackSelectionOverrides` class into `TrackSelectionParameters`,
and promote `TrackSelectionOverride` to a top level class.
* Rename `TracksInfo` to `Tracks` and `TracksInfo.TrackGroupInfo` to
`Tracks.Group`. `Player.getCurrentTracksInfo` and
`Player.Listener.onTracksInfoChanged` have also been renamed to
`Player.getCurrentTracks` and `Player.Listener.onTracksChanged`.
* Video:
* Rename `DummySurface` to `PlaceHolderSurface`.
* Audio:
* Use LG AC3 audio decoder advertising non-standard MIME type.
* Ad playback / IMA:
* Decrease ad polling rate from every 100ms to every 200ms, to line up with
Media Rating Council (MRC) recommendations.
* Extractors:
* Matroska: Parse `DiscardPadding` for Opus tracks.
* Parse bitrates from `esds` boxes.
* MP4: Parse initialization data from AV1 tracks.
* UI:
* Fix delivery of events to `OnClickListener`s set on `PlayerView` and
`LegacyPlayerView`, in the case that `useController=false`
@ -35,9 +46,34 @@
views to be used with other `Player` implementations, and removes the
dependency from the UI module to the ExoPlayer module. This is a
breaking change.
* Don't show forced text tracks in the `PlayerView` track selector, and
keep a suitable forced text track selected if "None" is selected
([#9432](https://github.com/google/ExoPlayer/issues/9432)).
* HLS:
* Fallback to chunkful preparation if the playlist CODECS attribute
does not contain the audio codec
([#10065](https://github.com/google/ExoPlayer/issues/10065)).
* RTSP:
* Add RTP reader for MPEG4
([#35](https://github.com/androidx/media/pull/35))
* Add RTP reader for HEVC
([#36](https://github.com/androidx/media/pull/36)).
* Add RTP reader for AMR. Currently only mono-channel, non-interleaved
AMR streams are supported. Compound AMR RTP payload is not supported.
([#46](https://github.com/androidx/media/pull/46))
* Add RTP reader for VP8
([#47](https://github.com/androidx/media/pull/47)).
* Add RTP reader for WAV
([#56](https://github.com/androidx/media/pull/56)).
* Fix RTSP basic authorization header.
([#9544](https://github.com/google/ExoPlayer/issues/9544)).
* Throw checked exception when parsing RTSP timing
([#10165](https://github.com/google/ExoPlayer/issues/10165)).
* Session:
* Fix NPE in MediaControllerImplLegacy
([#59](https://github.com/androidx/media/pull/59))
* Data sources:
* Rename `DummyDataSource` to `PlaceHolderDataSource`.
* Remove deprecated symbols:
* Remove `Player.Listener.onTracksChanged`. Use
`Player.Listener.onTracksInfoChanged` instead.

View File

@ -19,7 +19,7 @@ project.ext {
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
// additional robolectric config.
targetSdkVersion = 30
compileSdkVersion = 31
compileSdkVersion = 32
dexmakerVersion = '2.28.1'
junitVersion = '4.13.2'
// Use the same Guava version as the Android repo:

View File

@ -87,3 +87,7 @@ include modulePrefix + 'test-data'
project(modulePrefix + 'test-data').projectDir = new File(rootDir, 'libraries/test_data')
include modulePrefix + 'test-utils'
project(modulePrefix + 'test-utils').projectDir = new File(rootDir, 'libraries/test_utils')
include modulePrefix + 'test-session-common'
project(modulePrefix + 'test-session-common').projectDir = new File(rootDir, 'libraries/test_session_common')
include modulePrefix + 'test-session-current'
project(modulePrefix + 'test-session-current').projectDir = new File(rootDir, 'libraries/test_session_current')

View File

@ -26,7 +26,7 @@ import androidx.media3.common.Player;
import androidx.media3.common.Player.DiscontinuityReason;
import androidx.media3.common.Player.TimelineChangeReason;
import androidx.media3.common.Timeline;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.ui.PlayerControlView;
import androidx.media3.ui.PlayerView;
@ -57,7 +57,7 @@ import java.util.ArrayList;
private final ArrayList<MediaItem> mediaQueue;
private final Listener listener;
private TracksInfo lastSeenTrackGroupInfo;
private Tracks lastSeenTracks;
private int currentItemIndex;
private Player currentPlayer;
@ -219,19 +219,19 @@ import java.util.ArrayList;
}
@Override
public void onTracksInfoChanged(TracksInfo tracksInfo) {
if (currentPlayer != localPlayer || tracksInfo == lastSeenTrackGroupInfo) {
public void onTracksChanged(Tracks tracks) {
if (currentPlayer != localPlayer || tracks == lastSeenTracks) {
return;
}
if (tracksInfo.containsType(C.TRACK_TYPE_VIDEO)
&& !tracksInfo.isTypeSupported(C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) {
if (tracks.containsType(C.TRACK_TYPE_VIDEO)
&& !tracks.isTypeSupported(C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) {
listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO);
}
if (tracksInfo.containsType(C.TRACK_TYPE_AUDIO)
&& !tracksInfo.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) {
if (tracks.containsType(C.TRACK_TYPE_AUDIO)
&& !tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) {
listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO);
}
lastSeenTrackGroupInfo = tracksInfo;
lastSeenTracks = tracks;
}
// CastPlayer.SessionAvailabilityListener implementation.

View File

@ -20,6 +20,7 @@ import static androidx.media3.demo.main.DemoUtil.DOWNLOAD_NOTIFICATION_CHANNEL_I
import android.app.Notification;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.util.NotificationUtil;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.offline.Download;
@ -32,6 +33,7 @@ import androidx.media3.exoplayer.scheduler.Scheduler;
import java.util.List;
/** A service for downloading media. */
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public class DemoDownloadService extends DownloadService {
private static final int JOB_ID = 1;

View File

@ -16,6 +16,7 @@
package androidx.media3.demo.main;
import android.content.Context;
import androidx.annotation.OptIn;
import androidx.media3.database.DatabaseProvider;
import androidx.media3.database.StandaloneDatabaseProvider;
import androidx.media3.datasource.DataSource;
@ -71,6 +72,7 @@ public final class DemoUtil {
return BuildConfig.USE_DECODER_EXTENSIONS;
}
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public static RenderersFactory buildRenderersFactory(
Context context, boolean preferExtensionRenderer) {
@DefaultRenderersFactory.ExtensionRendererMode
@ -116,6 +118,7 @@ public final class DemoUtil {
return dataSourceFactory;
}
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(
Context context) {
if (downloadNotificationHelper == null) {
@ -135,6 +138,7 @@ public final class DemoUtil {
return downloadTracker;
}
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static synchronized Cache getDownloadCache(Context context) {
if (downloadCache == null) {
File downloadContentDirectory =
@ -146,6 +150,7 @@ public final class DemoUtil {
return downloadCache;
}
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static synchronized void ensureDownloadManagerInitialized(Context context) {
if (downloadManager == null) {
downloadManager =
@ -160,6 +165,7 @@ public final class DemoUtil {
}
}
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static synchronized DatabaseProvider getDatabaseProvider(Context context) {
if (databaseProvider == null) {
databaseProvider = new StandaloneDatabaseProvider(context);
@ -177,6 +183,7 @@ public final class DemoUtil {
return downloadDirectory;
}
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static CacheDataSource.Factory buildReadOnlyCacheDataSource(
DataSource.Factory upstreamFactory, Cache cache) {
return new CacheDataSource.Factory()

View File

@ -23,6 +23,7 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.FragmentManager;
import androidx.media3.common.DrmInitData;
@ -30,7 +31,7 @@ import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
@ -53,6 +54,7 @@ import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArraySet;
/** Tracks media that has been downloaded. */
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public class DownloadTracker {
/** Listens for changes in the tracked downloads. */
@ -301,17 +303,17 @@ public class DownloadTracker {
return;
}
TracksInfo tracksInfo = downloadHelper.getTracksInfo(/* periodIndex= */ 0);
if (!TrackSelectionDialog.willHaveContent(tracksInfo)) {
Tracks tracks = downloadHelper.getTracks(/* periodIndex= */ 0);
if (!TrackSelectionDialog.willHaveContent(tracks)) {
Log.d(TAG, "No dialog content. Downloading entire stream.");
startDownload();
downloadHelper.release();
return;
}
trackSelectionDialog =
TrackSelectionDialog.createForTracksInfoAndParameters(
TrackSelectionDialog.createForTracksAndParameters(
/* titleId= */ R.string.exo_download_description,
tracksInfo,
tracks,
DownloadHelper.getDefaultTrackSelectorParameters(context),
/* allowAdaptiveSelections= */ false,
/* allowMultipleOverrides= */ true,

View File

@ -17,6 +17,7 @@ package androidx.media3.demo.main;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Pair;
import android.view.KeyEvent;
@ -27,6 +28,7 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
@ -35,7 +37,7 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.ExoPlayer;
@ -81,7 +83,7 @@ public class PlayerActivity extends AppCompatActivity
private List<MediaItem> mediaItems;
private TrackSelectionParameters trackSelectionParameters;
private DebugTextViewHelper debugViewHelper;
private TracksInfo lastSeenTracksInfo;
private Tracks lastSeenTracks;
private boolean startAutoPlay;
private int startItemIndex;
private long startPosition;
@ -142,7 +144,7 @@ public class PlayerActivity extends AppCompatActivity
@Override
public void onStart() {
super.onStart();
if (Util.SDK_INT > 23) {
if (Build.VERSION.SDK_INT > 23) {
initializePlayer();
if (playerView != null) {
playerView.onResume();
@ -153,7 +155,7 @@ public class PlayerActivity extends AppCompatActivity
@Override
public void onResume() {
super.onResume();
if (Util.SDK_INT <= 23 || player == null) {
if (Build.VERSION.SDK_INT <= 23 || player == null) {
initializePlayer();
if (playerView != null) {
playerView.onResume();
@ -164,7 +166,7 @@ public class PlayerActivity extends AppCompatActivity
@Override
public void onPause() {
super.onPause();
if (Util.SDK_INT <= 23) {
if (Build.VERSION.SDK_INT <= 23) {
if (playerView != null) {
playerView.onPause();
}
@ -175,7 +177,7 @@ public class PlayerActivity extends AppCompatActivity
@Override
public void onStop() {
super.onStop();
if (Util.SDK_INT > 23) {
if (Build.VERSION.SDK_INT > 23) {
if (playerView != null) {
playerView.onPause();
}
@ -274,7 +276,7 @@ public class PlayerActivity extends AppCompatActivity
RenderersFactory renderersFactory =
DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders);
lastSeenTracksInfo = TracksInfo.EMPTY;
lastSeenTracks = Tracks.EMPTY;
player =
new ExoPlayer.Builder(/* context= */ this)
.setRenderersFactory(renderersFactory)
@ -342,7 +344,7 @@ public class PlayerActivity extends AppCompatActivity
MediaItem.DrmConfiguration drmConfiguration = mediaItem.localConfiguration.drmConfiguration;
if (drmConfiguration != null) {
if (Util.SDK_INT < 18) {
if (Build.VERSION.SDK_INT < 18) {
showToast(R.string.error_drm_unsupported_before_api_18);
finish();
return Collections.emptyList();
@ -454,22 +456,20 @@ public class PlayerActivity extends AppCompatActivity
@Override
@SuppressWarnings("ReferenceEquality")
public void onTracksInfoChanged(TracksInfo tracksInfo) {
public void onTracksChanged(Tracks tracks) {
updateButtonVisibility();
if (tracksInfo == lastSeenTracksInfo) {
if (tracks == lastSeenTracks) {
return;
}
if (tracksInfo.containsType(C.TRACK_TYPE_VIDEO)
&& !tracksInfo.isTypeSupported(
C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) {
if (tracks.containsType(C.TRACK_TYPE_VIDEO)
&& !tracks.isTypeSupported(C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) {
showToast(R.string.error_unsupported_video);
}
if (tracksInfo.containsType(C.TRACK_TYPE_AUDIO)
&& !tracksInfo.isTypeSupported(
C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) {
if (tracks.containsType(C.TRACK_TYPE_AUDIO)
&& !tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) {
showToast(R.string.error_unsupported_audio);
}
lastSeenTracksInfo = tracksInfo;
lastSeenTracks = tracks;
}
}
@ -508,29 +508,32 @@ public class PlayerActivity extends AppCompatActivity
private static List<MediaItem> createMediaItems(Intent intent, DownloadTracker downloadTracker) {
List<MediaItem> mediaItems = new ArrayList<>();
for (MediaItem item : IntentUtil.createMediaItemsFromIntent(intent)) {
@Nullable
DownloadRequest downloadRequest =
downloadTracker.getDownloadRequest(item.localConfiguration.uri);
if (downloadRequest != null) {
MediaItem.Builder builder = item.buildUpon();
builder
.setMediaId(downloadRequest.id)
.setUri(downloadRequest.uri)
.setCustomCacheKey(downloadRequest.customCacheKey)
.setMimeType(downloadRequest.mimeType)
.setStreamKeys(downloadRequest.streamKeys);
@Nullable
MediaItem.DrmConfiguration drmConfiguration = item.localConfiguration.drmConfiguration;
if (drmConfiguration != null) {
builder.setDrmConfiguration(
drmConfiguration.buildUpon().setKeySetId(downloadRequest.keySetId).build());
}
mediaItems.add(builder.build());
} else {
mediaItems.add(item);
}
mediaItems.add(
maybeSetDownloadProperties(
item, downloadTracker.getDownloadRequest(item.localConfiguration.uri)));
}
return mediaItems;
}
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static MediaItem maybeSetDownloadProperties(
MediaItem item, @Nullable DownloadRequest downloadRequest) {
if (downloadRequest == null) {
return item;
}
MediaItem.Builder builder = item.buildUpon();
builder
.setMediaId(downloadRequest.id)
.setUri(downloadRequest.uri)
.setCustomCacheKey(downloadRequest.customCacheKey)
.setMimeType(downloadRequest.mimeType)
.setStreamKeys(downloadRequest.streamKeys);
@Nullable
MediaItem.DrmConfiguration drmConfiguration = item.localConfiguration.drmConfiguration;
if (drmConfiguration != null) {
builder.setDrmConfiguration(
drmConfiguration.buildUpon().setKeySetId(downloadRequest.keySetId).build());
}
return builder.build();
}
}

View File

@ -41,6 +41,7 @@ import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaItem.ClippingConfiguration;
@ -53,6 +54,7 @@ import androidx.media3.datasource.DataSourceUtil;
import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.offline.DownloadService;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
@ -120,6 +122,7 @@ public class SampleChooserActivity extends AppCompatActivity
}
/** Start the download service if it should be running but it's not currently. */
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private void startDownloadService() {
// Starting the service in the foreground causes notification flicker if there is no scheduled
// action. Starting it in the background throws an exception if the app is in the background too
@ -274,6 +277,7 @@ public class SampleChooserActivity extends AppCompatActivity
private boolean sawError;
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
@Override
protected List<PlaylistGroup> doInBackground(String... uris) {
List<PlaylistGroup> result = new ArrayList<>();
@ -484,7 +488,7 @@ public class SampleChooserActivity extends AppCompatActivity
private PlaylistGroup getGroup(String groupName, List<PlaylistGroup> groups) {
for (int i = 0; i < groups.size(); i++) {
if (Util.areEqual(groupName, groups.get(i).title)) {
if (Objects.equal(groupName, groups.get(i).title)) {
return groups.get(i);
}
}

View File

@ -35,8 +35,7 @@ import androidx.media3.common.Player;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.TracksInfo.TrackGroupInfo;
import androidx.media3.common.Tracks;
import androidx.media3.ui.TrackSelectionView;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
@ -79,16 +78,16 @@ public final class TrackSelectionDialog extends DialogFragment {
* specified {@link Player}.
*/
public static boolean willHaveContent(Player player) {
return willHaveContent(player.getCurrentTracksInfo());
return willHaveContent(player.getCurrentTracks());
}
/**
* Returns whether a track selection dialog will have content to display if initialized with the
* specified {@link TracksInfo}.
* specified {@link Tracks}.
*/
public static boolean willHaveContent(TracksInfo tracksInfo) {
for (TrackGroupInfo trackGroupInfo : tracksInfo.getTrackGroupInfos()) {
if (SUPPORTED_TRACK_TYPES.contains(trackGroupInfo.getTrackType())) {
public static boolean willHaveContent(Tracks tracks) {
for (Tracks.Group trackGroup : tracks.getGroups()) {
if (SUPPORTED_TRACK_TYPES.contains(trackGroup.getType())) {
return true;
}
}
@ -105,9 +104,9 @@ public final class TrackSelectionDialog extends DialogFragment {
*/
public static TrackSelectionDialog createForPlayer(
Player player, DialogInterface.OnDismissListener onDismissListener) {
return createForTracksInfoAndParameters(
return createForTracksAndParameters(
R.string.track_selection_title,
player.getCurrentTracksInfo(),
player.getCurrentTracks(),
player.getTrackSelectionParameters(),
/* allowAdaptiveSelections= */ true,
/* allowMultipleOverrides= */ false,
@ -116,10 +115,10 @@ public final class TrackSelectionDialog extends DialogFragment {
}
/**
* Creates a dialog for given {@link TracksInfo} and {@link TrackSelectionParameters}.
* Creates a dialog for given {@link Tracks} and {@link TrackSelectionParameters}.
*
* @param titleId The resource id of the dialog title.
* @param tracksInfo The {@link TracksInfo} describing the tracks to display.
* @param tracks The {@link Tracks} describing the tracks to display.
* @param trackSelectionParameters The initial {@link TrackSelectionParameters}.
* @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track)
* can be made.
@ -128,9 +127,9 @@ public final class TrackSelectionDialog extends DialogFragment {
* @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is
* dismissed.
*/
public static TrackSelectionDialog createForTracksInfoAndParameters(
public static TrackSelectionDialog createForTracksAndParameters(
int titleId,
TracksInfo tracksInfo,
Tracks tracks,
TrackSelectionParameters trackSelectionParameters,
boolean allowAdaptiveSelections,
boolean allowMultipleOverrides,
@ -138,7 +137,7 @@ public final class TrackSelectionDialog extends DialogFragment {
DialogInterface.OnDismissListener onDismissListener) {
TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog();
trackSelectionDialog.init(
tracksInfo,
tracks,
trackSelectionParameters,
titleId,
allowAdaptiveSelections,
@ -169,7 +168,7 @@ public final class TrackSelectionDialog extends DialogFragment {
}
private void init(
TracksInfo tracksInfo,
Tracks tracks,
TrackSelectionParameters trackSelectionParameters,
int titleId,
boolean allowAdaptiveSelections,
@ -182,16 +181,16 @@ public final class TrackSelectionDialog extends DialogFragment {
for (int i = 0; i < SUPPORTED_TRACK_TYPES.size(); i++) {
@C.TrackType int trackType = SUPPORTED_TRACK_TYPES.get(i);
ArrayList<TrackGroupInfo> trackGroupInfos = new ArrayList<>();
for (TrackGroupInfo trackGroupInfo : tracksInfo.getTrackGroupInfos()) {
if (trackGroupInfo.getTrackType() == trackType) {
trackGroupInfos.add(trackGroupInfo);
ArrayList<Tracks.Group> trackGroups = new ArrayList<>();
for (Tracks.Group trackGroup : tracks.getGroups()) {
if (trackGroup.getType() == trackType) {
trackGroups.add(trackGroup);
}
}
if (!trackGroupInfos.isEmpty()) {
if (!trackGroups.isEmpty()) {
TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment();
tabFragment.init(
trackGroupInfos,
trackGroups,
trackSelectionParameters.disabledTrackTypes.contains(trackType),
trackSelectionParameters.overrides,
allowAdaptiveSelections,
@ -300,7 +299,7 @@ public final class TrackSelectionDialog extends DialogFragment {
public static final class TrackSelectionViewFragment extends Fragment
implements TrackSelectionView.TrackSelectionListener {
private List<TrackGroupInfo> trackGroupInfos;
private List<Tracks.Group> trackGroups;
private boolean allowAdaptiveSelections;
private boolean allowMultipleOverrides;
@ -313,12 +312,12 @@ public final class TrackSelectionDialog extends DialogFragment {
}
public void init(
List<TrackGroupInfo> trackGroupInfos,
List<Tracks.Group> trackGroups,
boolean isDisabled,
Map<TrackGroup, TrackSelectionOverride> overrides,
boolean allowAdaptiveSelections,
boolean allowMultipleOverrides) {
this.trackGroupInfos = trackGroupInfos;
this.trackGroups = trackGroups;
this.isDisabled = isDisabled;
this.allowAdaptiveSelections = allowAdaptiveSelections;
this.allowMultipleOverrides = allowMultipleOverrides;
@ -326,8 +325,7 @@ public final class TrackSelectionDialog extends DialogFragment {
// handle the case where the TrackSelectionView is never created.
this.overrides =
new HashMap<>(
TrackSelectionView.filterOverrides(
overrides, trackGroupInfos, allowMultipleOverrides));
TrackSelectionView.filterOverrides(overrides, trackGroups, allowMultipleOverrides));
}
@Override
@ -343,7 +341,7 @@ public final class TrackSelectionDialog extends DialogFragment {
trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides);
trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);
trackSelectionView.init(
trackGroupInfos,
trackGroups,
isDisabled,
overrides,
/* trackFormatComparator= */ null,

View File

@ -0,0 +1,37 @@
#version 100
// Copyright 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 2 fragment shader that overlays the bitmap from uTexSampler1 over a video
// frame from uTexSampler0.
precision mediump float;
// Texture containing an input video frame.
uniform sampler2D uTexSampler0;
// Texture containing the overlap bitmap.
uniform sampler2D uTexSampler1;
// Horizontal scaling factor for the overlap bitmap.
uniform float uScaleX;
// Vertical scaling factory for the overlap bitmap.
uniform float uScaleY;
varying vec2 vTexSamplingCoord;
void main() {
vec4 videoColor = texture2D(uTexSampler0, vTexSamplingCoord);
vec4 overlayColor = texture2D(uTexSampler1,
vec2(vTexSamplingCoord.x * uScaleX,
vTexSamplingCoord.y * uScaleY));
// Blend the video decoder output and the overlay bitmap.
gl_FragColor = videoColor * (1.0 - overlayColor.a)
+ overlayColor * overlayColor.a;
}

View File

@ -0,0 +1,31 @@
#version 100
// Copyright 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 2 fragment shader that samples from a (non-external) texture with uTexSampler,
// copying from this texture to the current output while applying a vignette effect
// by linearly darkening the pixels between uInnerRadius and uOuterRadius.
precision mediump float;
uniform sampler2D uTexSampler;
uniform vec2 uCenter;
uniform float uInnerRadius;
uniform float uOuterRadius;
varying vec2 vTexSamplingCoord;
void main() {
vec3 src = texture2D(uTexSampler, vTexSamplingCoord).xyz;
float dist = distance(vTexSamplingCoord, uCenter);
float scale = clamp(1.0 - (dist - uInnerRadius) / (uOuterRadius - uInnerRadius), 0.0, 1.0);
gl_FragColor = vec4(src.r * scale, src.g * scale, src.b * scale, 1.0);
}

View File

@ -0,0 +1,24 @@
#version 100
// Copyright 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ES 2 vertex shader that leaves the coordinates unchanged.
attribute vec4 aFramePosition;
attribute vec4 aTexSamplingCoord;
varying vec2 vTexSamplingCoord;
void main() {
gl_Position = aFramePosition;
vTexSamplingCoord = aTexSamplingCoord.xy;
}

View File

@ -0,0 +1,94 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.transformer;
import android.graphics.Matrix;
import androidx.media3.common.C;
import androidx.media3.common.util.Util;
import androidx.media3.transformer.AdvancedFrameProcessor;
import androidx.media3.transformer.GlFrameProcessor;
/**
* Factory for {@link GlFrameProcessor GlFrameProcessors} that create video effects by applying
* transformation matrices to the individual video frames using {@link AdvancedFrameProcessor}.
*/
/* package */ final class AdvancedFrameProcessorFactory {
/**
* Returns a {@link GlFrameProcessor} that rescales the frames over the first {@value
* #ZOOM_DURATION_SECONDS} seconds, such that the rectangle filled with the input frame increases
* linearly in size from a single point to filling the full output frame.
*/
public static GlFrameProcessor createZoomInTransitionFrameProcessor() {
return new AdvancedFrameProcessor(
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculateZoomInTransitionMatrix);
}
/**
* Returns a {@link GlFrameProcessor} that crops frames to a rectangle that moves on an ellipse.
*/
public static GlFrameProcessor createDizzyCropFrameProcessor() {
return new AdvancedFrameProcessor(
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculateDizzyCropMatrix);
}
/**
* Returns a {@link GlFrameProcessor} that rotates a frame in 3D around the y-axis and applies
* perspective projection to 2D.
*/
public static GlFrameProcessor createSpin3dFrameProcessor() {
return new AdvancedFrameProcessor(
/* matrixProvider= */ AdvancedFrameProcessorFactory::calculate3dSpinMatrix);
}
private static final float ZOOM_DURATION_SECONDS = 2f;
private static final float DIZZY_CROP_ROTATION_PERIOD_US = 1_500_000f;
private static Matrix calculateZoomInTransitionMatrix(long presentationTimeUs) {
Matrix transformationMatrix = new Matrix();
float scale = Math.min(1, presentationTimeUs / (C.MICROS_PER_SECOND * ZOOM_DURATION_SECONDS));
transformationMatrix.postScale(/* sx= */ scale, /* sy= */ scale);
return transformationMatrix;
}
private static android.graphics.Matrix calculateDizzyCropMatrix(long presentationTimeUs) {
double theta = presentationTimeUs * 2 * Math.PI / DIZZY_CROP_ROTATION_PERIOD_US;
float centerX = 0.5f * (float) Math.cos(theta);
float centerY = 0.5f * (float) Math.sin(theta);
android.graphics.Matrix transformationMatrix = new android.graphics.Matrix();
transformationMatrix.postTranslate(/* dx= */ centerX, /* dy= */ centerY);
transformationMatrix.postScale(/* sx= */ 2f, /* sy= */ 2f);
return transformationMatrix;
}
private static float[] calculate3dSpinMatrix(long presentationTimeUs) {
float[] transformationMatrix = new float[16];
android.opengl.Matrix.frustumM(
transformationMatrix,
/* offset= */ 0,
/* left= */ -1f,
/* right= */ 1f,
/* bottom= */ -1f,
/* top= */ 1f,
/* near= */ 3f,
/* far= */ 5f);
android.opengl.Matrix.translateM(
transformationMatrix, /* mOffset= */ 0, /* x= */ 0f, /* y= */ 0f, /* z= */ -4f);
float theta = Util.usToMs(presentationTimeUs) / 10f;
android.opengl.Matrix.rotateM(
transformationMatrix, /* mOffset= */ 0, theta, /* x= */ 0f, /* y= */ 1f, /* z= */ 0f);
return transformationMatrix;
}
}

View File

@ -0,0 +1,162 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.transformer;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.util.Size;
import androidx.media3.common.C;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.transformer.GlFrameProcessor;
import java.io.IOException;
import java.util.Locale;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link GlFrameProcessor} that overlays a bitmap with a logo and timer on each frame.
*
* <p>The bitmap is drawn using an Android {@link Canvas}.
*/
// TODO(b/227625365): Delete this class and use a frame processor from the Transformer library, once
// overlaying a bitmap and text is supported in Transformer.
/* package */ final class BitmapOverlayFrameProcessor implements GlFrameProcessor {
static {
GlUtil.glAssertionsEnabled = true;
}
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_bitmap_overlay_es2.glsl";
private static final int BITMAP_WIDTH_HEIGHT = 512;
private final Paint paint;
private final Bitmap overlayBitmap;
private final Canvas overlayCanvas;
private float bitmapScaleX;
private float bitmapScaleY;
private int bitmapTexId;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull Bitmap logoBitmap;
private @MonotonicNonNull GlProgram glProgram;
public BitmapOverlayFrameProcessor() {
paint = new Paint();
paint.setTextSize(64);
paint.setAntiAlias(true);
paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF);
paint.setColor(Color.GRAY);
overlayBitmap =
Bitmap.createBitmap(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT, Bitmap.Config.ARGB_8888);
overlayCanvas = new Canvas(overlayBitmap);
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
if (inputWidth > inputHeight) {
bitmapScaleX = inputWidth / (float) inputHeight;
bitmapScaleY = 1f;
} else {
bitmapScaleX = 1f;
bitmapScaleY = inputHeight / (float) inputWidth;
}
outputSize = new Size(inputWidth, inputHeight);
try {
logoBitmap =
((BitmapDrawable)
context.getPackageManager().getApplicationIcon(context.getPackageName()))
.getBitmap();
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException(e);
}
bitmapTexId = GlUtil.createTexture(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
glProgram.setBufferAttribute(
"aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
glProgram.setBufferAttribute(
"aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
glProgram.setSamplerTexIdUniform("uTexSampler1", bitmapTexId, /* texUnitIndex= */ 1);
glProgram.setFloatUniform("uScaleX", bitmapScaleX);
glProgram.setFloatUniform("uScaleY", bitmapScaleY);
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
}
@Override
public void drawFrame(long presentationTimeUs) {
checkStateNotNull(glProgram);
glProgram.use();
// Draw to the canvas and store it in a texture.
String text =
String.format(Locale.US, "%.02f", presentationTimeUs / (float) C.MICROS_PER_SECOND);
overlayBitmap.eraseColor(Color.TRANSPARENT);
overlayCanvas.drawBitmap(checkStateNotNull(logoBitmap), /* left= */ 3, /* top= */ 378, paint);
overlayCanvas.drawText(text, /* x= */ 160, /* y= */ 466, paint);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, bitmapTexId);
GLUtils.texSubImage2D(
GLES20.GL_TEXTURE_2D,
/* level= */ 0,
/* xoffset= */ 0,
/* yoffset= */ 0,
flipBitmapVertically(overlayBitmap));
GlUtil.checkGlError();
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
}
@Override
public void release() {
if (glProgram != null) {
glProgram.delete();
}
}
private static Bitmap flipBitmapVertically(Bitmap bitmap) {
Matrix flip = new Matrix();
flip.postScale(1f, -1f);
return Bitmap.createBitmap(
bitmap,
/* x= */ 0,
/* y= */ 0,
bitmap.getWidth(),
bitmap.getHeight(),
flip,
/* filter= */ true);
}
}

View File

@ -34,6 +34,8 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util;
import com.google.android.material.slider.RangeSlider;
import com.google.android.material.slider.Slider;
import java.util.Arrays;
import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -54,25 +56,48 @@ public final class ConfigurationActivity extends AppCompatActivity {
public static final String SCALE_Y = "scale_y";
public static final String ROTATE_DEGREES = "rotate_degrees";
public static final String ENABLE_FALLBACK = "enable_fallback";
public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping";
public static final String ENABLE_HDR_EDITING = "enable_hdr_editing";
public static final String DEMO_FRAME_PROCESSORS_SELECTIONS = "demo_frame_processors_selections";
public static final String PERIODIC_VIGNETTE_CENTER_X = "periodic_vignette_center_x";
public static final String PERIODIC_VIGNETTE_CENTER_Y = "periodic_vignette_center_y";
public static final String PERIODIC_VIGNETTE_INNER_RADIUS = "periodic_vignette_inner_radius";
public static final String PERIODIC_VIGNETTE_OUTER_RADIUS = "periodic_vignette_outer_radius";
private static final String[] INPUT_URIS = {
"https://html5demos.com/assets/dizzy.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/1920w_1080h_4s.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4",
"https://html5demos.com/assets/dizzy.webm",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/8k24fps_4s.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_avc_aac.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/slowMotion_stopwatch_240fps_long.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-hdr-hdr10.mp4",
};
private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS
"MP4 with H264 video and AAC audio",
"Short MP4 with H265 video and AAC audio",
"MP4 with H265 video and AAC audio",
"Long MP4 with H264 video and AAC audio",
"WebM with VP8 video and Vorbis audio",
"4K 60fps MP4 with H264 video and AAC audio (portrait, timestamps always increase)",
"8k 24fps MP4 with H265 video and AAC audio",
"MP4 with H264 video and AAC audio (portrait, H > W, 0\u00B0)",
"MP4 with H264 video and AAC audio (portrait, H < W, 90\u00B0)",
"SEF slow motion with 240 fps",
"MP4 with HDR (HDR10) H265 video (encoding may fail)",
};
private static final String[] DEMO_FRAME_PROCESSORS = {
"Dizzy crop", "Periodic vignette", "3D spin", "Overlay logo & timer", "Zoom in start"
};
private static final int PERIODIC_VIGNETTE_INDEX = 1;
private static final String SAME_AS_INPUT_OPTION = "same as input";
private static final float HALF_DIAGONAL = 1f / (float) Math.sqrt(2);
private @MonotonicNonNull Button chooseFileButton;
private @MonotonicNonNull TextView chosenFileTextView;
private @MonotonicNonNull Button selectFileButton;
private @MonotonicNonNull TextView selectedFileTextView;
private @MonotonicNonNull CheckBox removeAudioCheckbox;
private @MonotonicNonNull CheckBox removeVideoCheckbox;
private @MonotonicNonNull CheckBox flattenForSlowMotionCheckbox;
@ -82,8 +107,15 @@ public final class ConfigurationActivity extends AppCompatActivity {
private @MonotonicNonNull Spinner scaleSpinner;
private @MonotonicNonNull Spinner rotateSpinner;
private @MonotonicNonNull CheckBox enableFallbackCheckBox;
private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox;
private @MonotonicNonNull CheckBox enableHdrEditingCheckBox;
private @MonotonicNonNull Button selectDemoFrameProcessorsButton;
private boolean @MonotonicNonNull [] demoFrameProcessorsSelections;
private int inputUriPosition;
private float periodicVignetteCenterX;
private float periodicVignetteCenterY;
private float periodicVignetteInnerRadius;
private float periodicVignetteOuterRadius;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@ -92,11 +124,11 @@ public final class ConfigurationActivity extends AppCompatActivity {
findViewById(R.id.transform_button).setOnClickListener(this::startTransformation);
chooseFileButton = findViewById(R.id.choose_file_button);
chooseFileButton.setOnClickListener(this::chooseFile);
selectFileButton = findViewById(R.id.select_file_button);
selectFileButton.setOnClickListener(this::selectFile);
chosenFileTextView = findViewById(R.id.chosen_file_text_view);
chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
selectedFileTextView = findViewById(R.id.selected_file_text_view);
selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
removeAudioCheckbox = findViewById(R.id.remove_audio_checkbox);
removeAudioCheckbox.setOnClickListener(this::onRemoveAudio);
@ -148,7 +180,14 @@ public final class ConfigurationActivity extends AppCompatActivity {
rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "60", "90", "180");
enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox);
enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox);
enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported());
findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported());
enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox);
demoFrameProcessorsSelections = new boolean[DEMO_FRAME_PROCESSORS.length];
selectDemoFrameProcessorsButton = findViewById(R.id.select_demo_frameprocessors_button);
selectDemoFrameProcessorsButton.setOnClickListener(this::selectFrameProcessors);
}
@Override
@ -156,8 +195,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
super.onResume();
@Nullable Uri intentUri = getIntent().getData();
if (intentUri != null) {
checkNotNull(chooseFileButton).setEnabled(false);
checkNotNull(chosenFileTextView).setText(intentUri.toString());
checkNotNull(selectFileButton).setEnabled(false);
checkNotNull(selectedFileTextView).setText(intentUri.toString());
}
}
@ -177,7 +216,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"scaleSpinner",
"rotateSpinner",
"enableFallbackCheckBox",
"enableHdrEditingCheckBox"
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox",
"demoFrameProcessorsSelections"
})
private void startTransformation(View view) {
Intent transformerIntent = new Intent(this, TransformerActivity.class);
@ -209,7 +250,14 @@ public final class ConfigurationActivity extends AppCompatActivity {
bundle.putFloat(ROTATE_DEGREES, Float.parseFloat(selectedRotate));
}
bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked());
bundle.putBoolean(
ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked());
bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked());
bundle.putBooleanArray(DEMO_FRAME_PROCESSORS_SELECTIONS, demoFrameProcessorsSelections);
bundle.putFloat(PERIODIC_VIGNETTE_CENTER_X, periodicVignetteCenterX);
bundle.putFloat(PERIODIC_VIGNETTE_CENTER_Y, periodicVignetteCenterY);
bundle.putFloat(PERIODIC_VIGNETTE_INNER_RADIUS, periodicVignetteInnerRadius);
bundle.putFloat(PERIODIC_VIGNETTE_OUTER_RADIUS, periodicVignetteOuterRadius);
transformerIntent.putExtras(bundle);
@Nullable Uri intentUri = getIntent().getData();
@ -219,19 +267,63 @@ public final class ConfigurationActivity extends AppCompatActivity {
startActivity(transformerIntent);
}
private void chooseFile(View view) {
private void selectFile(View view) {
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.choose_file_title)
.setTitle(R.string.select_file_title)
.setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog)
.setPositiveButton(android.R.string.ok, /* listener= */ null)
.create()
.show();
}
@RequiresNonNull("chosenFileTextView")
private void selectFrameProcessors(View view) {
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.select_demo_frameprocessors)
.setMultiChoiceItems(
DEMO_FRAME_PROCESSORS,
checkNotNull(demoFrameProcessorsSelections),
this::selectFrameProcessor)
.setPositiveButton(android.R.string.ok, /* listener= */ null)
.create()
.show();
}
@RequiresNonNull("selectedFileTextView")
private void selectFileInDialog(DialogInterface dialog, int which) {
inputUriPosition = which;
chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
}
@RequiresNonNull("demoFrameProcessorsSelections")
private void selectFrameProcessor(DialogInterface dialog, int which, boolean isChecked) {
demoFrameProcessorsSelections[which] = isChecked;
if (!isChecked || which != PERIODIC_VIGNETTE_INDEX) {
return;
}
View dialogView =
getLayoutInflater().inflate(R.layout.periodic_vignette_options, /* root= */ null);
Slider centerXSlider =
checkNotNull(dialogView.findViewById(R.id.periodic_vignette_center_x_slider));
Slider centerYSlider =
checkNotNull(dialogView.findViewById(R.id.periodic_vignette_center_y_slider));
RangeSlider radiusRangeSlider =
checkNotNull(dialogView.findViewById(R.id.periodic_vignette_radius_range_slider));
radiusRangeSlider.setValues(0f, HALF_DIAGONAL);
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.periodic_vignette_options)
.setView(dialogView)
.setPositiveButton(
android.R.string.ok,
(DialogInterface dialogInterface, int i) -> {
periodicVignetteCenterX = centerXSlider.getValue();
periodicVignetteCenterY = centerYSlider.getValue();
List<Float> radiusRange = radiusRangeSlider.getValues();
periodicVignetteInnerRadius = radiusRange.get(0);
periodicVignetteOuterRadius = radiusRange.get(1);
})
.create()
.show();
}
@RequiresNonNull({
@ -241,7 +333,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner",
"scaleSpinner",
"rotateSpinner",
"enableHdrEditingCheckBox"
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox",
"selectDemoFrameProcessorsButton"
})
private void onRemoveAudio(View view) {
if (((CheckBox) view).isChecked()) {
@ -259,7 +353,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner",
"scaleSpinner",
"rotateSpinner",
"enableHdrEditingCheckBox"
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox",
"selectDemoFrameProcessorsButton"
})
private void onRemoveVideo(View view) {
if (((CheckBox) view).isChecked()) {
@ -276,7 +372,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner",
"scaleSpinner",
"rotateSpinner",
"enableHdrEditingCheckBox"
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox",
"selectDemoFrameProcessorsButton"
})
private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) {
audioMimeSpinner.setEnabled(isAudioEnabled);
@ -284,13 +382,22 @@ public final class ConfigurationActivity extends AppCompatActivity {
resolutionHeightSpinner.setEnabled(isVideoEnabled);
scaleSpinner.setEnabled(isVideoEnabled);
rotateSpinner.setEnabled(isVideoEnabled);
enableRequestSdrToneMappingCheckBox.setEnabled(
isRequestSdrToneMappingSupported() && isVideoEnabled);
enableHdrEditingCheckBox.setEnabled(isVideoEnabled);
selectDemoFrameProcessorsButton.setEnabled(isVideoEnabled);
findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled);
findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled);
findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled);
findViewById(R.id.scale).setEnabled(isVideoEnabled);
findViewById(R.id.rotate).setEnabled(isVideoEnabled);
findViewById(R.id.request_sdr_tone_mapping)
.setEnabled(isRequestSdrToneMappingSupported() && isVideoEnabled);
findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled);
}
private static boolean isRequestSdrToneMappingSupported() {
return Util.SDK_INT >= 31;
}
}

View File

@ -0,0 +1,118 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.demo.transformer;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.opengl.GLES20;
import android.util.Size;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.transformer.GlFrameProcessor;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link GlFrameProcessor} that periodically dims the frames such that pixels are darker the
* further they are away from the frame center.
*/
/* package */ final class PeriodicVignetteFrameProcessor implements GlFrameProcessor {
static {
GlUtil.glAssertionsEnabled = true;
}
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_vignette_es2.glsl";
private static final float DIMMING_PERIOD_US = 5_600_000f;
private float centerX;
private float centerY;
private float minInnerRadius;
private float deltaInnerRadius;
private float outerRadius;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull GlProgram glProgram;
/**
* Creates a new instance.
*
* <p>The inner radius of the vignette effect oscillates smoothly between {@code minInnerRadius}
* and {@code maxInnerRadius}.
*
* <p>The pixels between the inner radius and the {@code outerRadius} are darkened linearly based
* on their distance from {@code innerRadius}. All pixels outside {@code outerRadius} are black.
*
* <p>The parameters are given in normalized texture coordinates from 0 to 1.
*
* @param context The {@link Context}.
* @param centerX The x-coordinate of the center of the effect.
* @param centerY The y-coordinate of the center of the effect.
* @param minInnerRadius The lower bound of the radius that is unaffected by the effect.
* @param maxInnerRadius The upper bound of the radius that is unaffected by the effect.
* @param outerRadius The radius after which all pixels are black.
*/
public PeriodicVignetteFrameProcessor(
float centerX, float centerY, float minInnerRadius, float maxInnerRadius, float outerRadius) {
checkArgument(minInnerRadius <= maxInnerRadius);
checkArgument(maxInnerRadius <= outerRadius);
this.centerX = centerX;
this.centerY = centerY;
this.minInnerRadius = minInnerRadius;
this.deltaInnerRadius = maxInnerRadius - minInnerRadius;
this.outerRadius = outerRadius;
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
outputSize = new Size(inputWidth, inputHeight);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
glProgram.setFloatsUniform("uCenter", new float[] {centerX, centerY});
glProgram.setFloatsUniform("uOuterRadius", new float[] {outerRadius});
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
glProgram.setBufferAttribute(
"aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
glProgram.setBufferAttribute(
"aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
}
@Override
public void drawFrame(long presentationTimeUs) {
checkStateNotNull(glProgram).use();
double theta = presentationTimeUs * 2 * Math.PI / DIMMING_PERIOD_US;
float innerRadius = minInnerRadius + deltaInnerRadius * (0.5f - 0.5f * (float) Math.cos(theta));
glProgram.setFloatsUniform("uInnerRadius", new float[] {innerRadius});
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
}
@Override
public void release() {
if (glProgram != null) {
glProgram.delete();
}
}
}

View File

@ -40,6 +40,7 @@ import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.util.DebugTextViewHelper;
import androidx.media3.transformer.DefaultEncoderFactory;
import androidx.media3.transformer.EncoderSelector;
import androidx.media3.transformer.GlFrameProcessor;
import androidx.media3.transformer.ProgressHolder;
import androidx.media3.transformer.TransformationException;
import androidx.media3.transformer.TransformationRequest;
@ -50,6 +51,7 @@ import androidx.media3.ui.PlayerView;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
@ -225,6 +227,8 @@ public final class TransformerActivity extends AppCompatActivity {
bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0);
requestBuilder.setRotationDegrees(rotateDegrees);
requestBuilder.setEnableRequestSdrToneMapping(
bundle.getBoolean(ConfigurationActivity.ENABLE_REQUEST_SDR_TONE_MAPPING));
requestBuilder.experimental_setEnableHdrEditing(
bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING));
transformerBuilder
@ -235,6 +239,37 @@ public final class TransformerActivity extends AppCompatActivity {
new DefaultEncoderFactory(
EncoderSelector.DEFAULT,
/* enableFallback= */ bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK)));
ImmutableList.Builder<GlFrameProcessor> frameProcessors = new ImmutableList.Builder<>();
@Nullable
boolean[] selectedFrameProcessors =
bundle.getBooleanArray(ConfigurationActivity.DEMO_FRAME_PROCESSORS_SELECTIONS);
if (selectedFrameProcessors != null) {
if (selectedFrameProcessors[0]) {
frameProcessors.add(AdvancedFrameProcessorFactory.createDizzyCropFrameProcessor());
}
if (selectedFrameProcessors[1]) {
frameProcessors.add(
new PeriodicVignetteFrameProcessor(
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X),
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_Y),
/* minInnerRadius= */ bundle.getFloat(
ConfigurationActivity.PERIODIC_VIGNETTE_INNER_RADIUS),
/* maxInnerRadius= */ bundle.getFloat(
ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS),
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS)));
}
if (selectedFrameProcessors[2]) {
frameProcessors.add(AdvancedFrameProcessorFactory.createSpin3dFrameProcessor());
}
if (selectedFrameProcessors[3]) {
frameProcessors.add(new BitmapOverlayFrameProcessor());
}
if (selectedFrameProcessors[4]) {
frameProcessors.add(AdvancedFrameProcessorFactory.createZoomInTransitionFrameProcessor());
}
transformerBuilder.setFrameProcessors(frameProcessors.build());
}
}
return transformerBuilder
.addListener(

View File

@ -34,18 +34,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/choose_file_button"
android:id="@+id/select_file_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="@string/choose_file_title"
android:text="@string/select_file_title"
app:layout_constraintTop_toBottomOf="@+id/configuration_text_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/chosen_file_text_view"
android:id="@+id/selected_file_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
@ -57,14 +57,14 @@
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/choose_file_button" />
app:layout_constraintTop_toBottomOf="@+id/select_file_button" />
<androidx.core.widget.NestedScrollView
android:layout_width="fill_parent"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chosen_file_text_view"
app:layout_constraintBottom_toTopOf="@+id/transform_button">
app:layout_constraintTop_toBottomOf="@+id/selected_file_text_view"
app:layout_constraintBottom_toTopOf="@+id/select_demo_frameprocessors_button">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
@ -169,6 +169,16 @@
android:layout_gravity="right"
android:checked="true"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/request_sdr_tone_mapping"
android:text="@string/request_sdr_tone_mapping" />
<CheckBox
android:id="@+id/request_sdr_tone_mapping_checkbox"
android:layout_gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
@ -181,6 +191,17 @@
</TableRow>
</TableLayout>
</androidx.core.widget.NestedScrollView>
<Button
android:id="@+id/select_demo_frameprocessors_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="@string/select_demo_frameprocessors"
app:layout_constraintBottom_toTopOf="@+id/transform_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/transform_button"
android:layout_width="wrap_content"

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tools:context=".ConfigurationActivity">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:stretchColumns="1"
android:layout_marginTop="32dp"
android:measureWithLargestChild="true"
android:paddingLeft="24dp"
android:paddingRight="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/center_x" />
<com.google.android.material.slider.Slider
android:id="@+id/periodic_vignette_center_x_slider"
android:valueFrom="0.0"
android:value="0.5"
android:valueTo="1.0"
android:layout_gravity="right"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/center_y" />
<com.google.android.material.slider.Slider
android:id="@+id/periodic_vignette_center_y_slider"
android:valueFrom="0.0"
android:value="0.5"
android:valueTo="1.0"
android:layout_gravity="right"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/radius_range" />
<com.google.android.material.slider.RangeSlider
android:id="@+id/periodic_vignette_radius_range_slider"
android:valueFrom="0.0"
android:valueTo="1.414"
android:layout_gravity="right"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -17,7 +17,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name" translatable="false">Transformer Demo</string>
<string name="configuration" translatable="false">Configuration</string>
<string name="choose_file_title" translatable="false">Choose file</string>
<string name="select_file_title" translatable="false">Choose file</string>
<string name="remove_audio" translatable="false">Remove audio</string>
<string name="remove_video" translatable="false">Remove video</string>
<string name="flatten_for_slow_motion" translatable="false">Flatten for slow motion</string>
@ -27,12 +27,18 @@
<string name="scale" translatable="false">Scale video</string>
<string name="rotate" translatable="false">Rotate video (degrees)</string>
<string name="enable_fallback" translatable="false">Enable fallback</string>
<string name="transform" translatable="false">Transform</string>
<string name="request_sdr_tone_mapping" translatable="false">Request SDR tone-mapping (API 31+)</string>
<string name="hdr_editing" translatable="false">[Experimental] HDR editing</string>
<string name="select_demo_frameprocessors" translatable="false">Add demo effects</string>
<string name="periodic_vignette_options" translatable="false">Periodic vignette options</string>
<string name="transform" translatable="false">Transform</string>
<string name="debug_preview" translatable="false">Debug preview:</string>
<string name="debug_preview_not_available" translatable="false">No debug preview available.</string>
<string name="transformation_started" translatable="false">Transformation started</string>
<string name="transformation_timer" translatable="false">Transformation started %d seconds ago.</string>
<string name="transformation_completed" translatable="false">Transformation completed in %d seconds.</string>
<string name="transformation_error" translatable="false">Transformation error</string>
<string name="center_x">Center X</string>
<string name="center_y">Center Y</string>
<string name="radius_range">Radius range</string>
</resources>

View File

@ -40,8 +40,7 @@ import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.TracksInfo.TrackGroupInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.Assertions;
@ -103,7 +102,7 @@ public final class CastPlayer extends BasePlayer {
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_TRACK_INFOS)
COMMAND_GET_TRACKS)
.build();
public static final float MIN_SPEED_SUPPORTED = 0.5f;
@ -136,7 +135,7 @@ public final class CastPlayer extends BasePlayer {
private final StateHolder<PlaybackParameters> playbackParameters;
@Nullable private RemoteMediaClient remoteMediaClient;
private CastTimeline currentTimeline;
private TracksInfo currentTracksInfo;
private Tracks currentTracks;
private Commands availableCommands;
private @Player.State int playbackState;
private int currentWindowIndex;
@ -212,7 +211,7 @@ public final class CastPlayer extends BasePlayer {
playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT);
playbackState = STATE_IDLE;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
currentTracksInfo = TracksInfo.EMPTY;
currentTracks = Tracks.EMPTY;
availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build();
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
@ -544,8 +543,8 @@ public final class CastPlayer extends BasePlayer {
}
@Override
public TracksInfo getCurrentTracksInfo() {
return currentTracksInfo;
public Tracks getCurrentTracks() {
return currentTracks;
}
@Override
@ -818,7 +817,7 @@ public final class CastPlayer extends BasePlayer {
}
if (updateTracksAndSelectionsAndNotifyIfChanged()) {
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksInfoChanged(currentTracksInfo));
Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(currentTracks));
}
updateAvailableCommandsAndNotifyIfChanged();
listeners.flushEvents();
@ -978,8 +977,8 @@ public final class CastPlayer extends BasePlayer {
@Nullable
List<MediaTrack> castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null;
if (castMediaTracks == null || castMediaTracks.isEmpty()) {
boolean hasChanged = !TracksInfo.EMPTY.equals(currentTracksInfo);
currentTracksInfo = TracksInfo.EMPTY;
boolean hasChanged = !Tracks.EMPTY.equals(currentTracks);
currentTracks = Tracks.EMPTY;
return hasChanged;
}
@Nullable long[] activeTrackIds = mediaStatus.getActiveTrackIds();
@ -987,20 +986,19 @@ public final class CastPlayer extends BasePlayer {
activeTrackIds = EMPTY_TRACK_ID_ARRAY;
}
TrackGroupInfo[] trackGroupInfos = new TrackGroupInfo[castMediaTracks.size()];
Tracks.Group[] trackGroups = new Tracks.Group[castMediaTracks.size()];
for (int i = 0; i < castMediaTracks.size(); i++) {
MediaTrack mediaTrack = castMediaTracks.get(i);
TrackGroup trackGroup =
new TrackGroup(/* id= */ Integer.toString(i), CastUtils.mediaTrackToFormat(mediaTrack));
@C.FormatSupport int[] trackSupport = new int[] {C.FORMAT_HANDLED};
boolean[] trackSelected = new boolean[] {isTrackActive(mediaTrack.getId(), activeTrackIds)};
trackGroupInfos[i] =
new TrackGroupInfo(
trackGroup, /* adaptiveSupported= */ false, trackSupport, trackSelected);
trackGroups[i] =
new Tracks.Group(trackGroup, /* adaptiveSupported= */ false, trackSupport, trackSelected);
}
TracksInfo newTracksInfo = new TracksInfo(ImmutableList.copyOf(trackGroupInfos));
if (!newTracksInfo.equals(currentTracksInfo)) {
currentTracksInfo = newTracksInfo;
Tracks newTracks = new Tracks(ImmutableList.copyOf(trackGroups));
if (!newTracks.equals(currentTracks)) {
currentTracks = newTracks;
return true;
}
return false;

View File

@ -585,8 +585,9 @@ public final class C {
/**
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE},
* {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}.
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_FIRST_SAMPLE},
* {@link #BUFFER_FLAG_LAST_SAMPLE}, {@link #BUFFER_FLAG_ENCRYPTED} and {@link
* #BUFFER_FLAG_DECODE_ONLY}.
*/
@UnstableApi
@Documented
@ -597,6 +598,7 @@ public final class C {
value = {
BUFFER_FLAG_KEY_FRAME,
BUFFER_FLAG_END_OF_STREAM,
BUFFER_FLAG_FIRST_SAMPLE,
BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA,
BUFFER_FLAG_LAST_SAMPLE,
BUFFER_FLAG_ENCRYPTED,
@ -608,6 +610,8 @@ public final class C {
/** Flag for empty buffers that signal that the end of the stream was reached. */
@UnstableApi
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
/** Indicates that a buffer is known to contain the first media sample of the stream. */
@UnstableApi public static final int BUFFER_FLAG_FIRST_SAMPLE = 1 << 27; // 0x08000000
/** Indicates that a buffer has supplemental data. */
@UnstableApi public static final int BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA = 1 << 28; // 0x10000000
/** Indicates that a buffer is known to contain the last media sample of the stream. */
@ -732,17 +736,17 @@ public final class C {
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_RTSP, TYPE_OTHER})
public @interface ContentType {}
/** Value returned by {@link Util#inferContentType(String)} for DASH manifests. */
/** Value returned by {@link Util#inferContentType} for DASH manifests. */
@UnstableApi public static final int TYPE_DASH = 0;
/** Value returned by {@link Util#inferContentType(String)} for Smooth Streaming manifests. */
/** Value returned by {@link Util#inferContentType} for Smooth Streaming manifests. */
@UnstableApi public static final int TYPE_SS = 1;
/** Value returned by {@link Util#inferContentType(String)} for HLS manifests. */
/** Value returned by {@link Util#inferContentType} for HLS manifests. */
@UnstableApi public static final int TYPE_HLS = 2;
/** Value returned by {@link Util#inferContentType(String)} for RTSP. */
/** Value returned by {@link Util#inferContentType} for RTSP. */
@UnstableApi public static final int TYPE_RTSP = 3;
/**
* Value returned by {@link Util#inferContentType(String)} for files other than DASH, HLS or
* Smooth Streaming manifests, or RTSP URIs.
* Value returned by {@link Util#inferContentType} for files other than DASH, HLS or Smooth
* Streaming manifests, or RTSP URIs.
*/
@UnstableApi public static final int TYPE_OTHER = 4;

View File

@ -442,10 +442,10 @@ public class ForwardingPlayer implements Player {
player.release();
}
/** Calls {@link Player#getCurrentTracksInfo()} on the delegate and returns the result. */
/** Calls {@link Player#getCurrentTracks()} on the delegate and returns the result. */
@Override
public TracksInfo getCurrentTracksInfo() {
return player.getCurrentTracksInfo();
public Tracks getCurrentTracks() {
return player.getCurrentTracks();
}
/** Calls {@link Player#getTrackSelectionParameters()} on the delegate and returns the result. */
@ -831,8 +831,8 @@ public class ForwardingPlayer implements Player {
}
@Override
public void onTracksInfoChanged(TracksInfo tracksInfo) {
listener.onTracksInfoChanged(tracksInfo);
public void onTracksChanged(Tracks tracks) {
listener.onTracksChanged(tracks);
}
@Override

View File

@ -108,11 +108,10 @@ public final class MimeTypes {
public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4";
public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm";
@UnstableApi
public static final String APPLICATION_MATROSKA = BASE_TYPE_APPLICATION + "/x-matroska";
public static final String APPLICATION_MPD = BASE_TYPE_APPLICATION + "/dash+xml";
@UnstableApi public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
public static final String APPLICATION_SS = BASE_TYPE_APPLICATION + "/vnd.ms-sstr+xml";
public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3";
public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608";
@ -135,7 +134,7 @@ public final class MimeTypes {
@UnstableApi public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif";
@UnstableApi public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy";
public static final String APPLICATION_AIT = BASE_TYPE_APPLICATION + "/vnd.dvb.ait";
@UnstableApi public static final String APPLICATION_RTSP = BASE_TYPE_APPLICATION + "/x-rtsp";
public static final String APPLICATION_RTSP = BASE_TYPE_APPLICATION + "/x-rtsp";
// image/ MIME types

View File

@ -57,8 +57,8 @@ import java.util.List;
* <ul>
* <li>They can provide a {@link Timeline} representing the structure of the media being played,
* which can be obtained by calling {@link #getCurrentTimeline()}.
* <li>They can provide a {@link TracksInfo} defining the currently available tracks and which are
* selected to be rendered, which can be obtained by calling {@link #getCurrentTracksInfo()}.
* <li>They can provide a {@link Tracks} defining the currently available tracks and which are
* selected to be rendered, which can be obtained by calling {@link #getCurrentTracks()}.
* </ul>
*/
public interface Player {
@ -381,7 +381,7 @@ public interface Player {
COMMAND_SET_VIDEO_SURFACE,
COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACK_INFOS,
COMMAND_GET_TRACKS,
};
private final FlagSet.Builder flagsBuilder;
@ -523,6 +523,11 @@ public interface Player {
return flags.contains(command);
}
/** Returns whether the set of commands contains at least one of the given {@code commands}. */
public boolean containsAny(@Command int... commands) {
return flags.containsAny(commands);
}
/** Returns the number of commands in this set. */
public int size() {
return flags.size();
@ -671,14 +676,14 @@ public interface Player {
@Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {}
/**
* Called when the available or selected tracks change.
* Called when the tracks change.
*
* <p>{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration.
*
* @param tracksInfo The available tracks information. Never null, but may be of length zero.
* @param tracks The available tracks information. Never null, but may be of length zero.
*/
default void onTracksInfoChanged(TracksInfo tracksInfo) {}
default void onTracksChanged(Tracks tracks) {}
/**
* Called when the combined {@link MediaMetadata} changes.
@ -1303,7 +1308,7 @@ public interface Player {
int EVENT_TIMELINE_CHANGED = 0;
/** {@link #getCurrentMediaItem()} changed or the player started repeating the current item. */
int EVENT_MEDIA_ITEM_TRANSITION = 1;
/** {@link #getCurrentTracksInfo()} changed. */
/** {@link #getCurrentTracks()} changed. */
int EVENT_TRACKS_CHANGED = 2;
/** {@link #isLoading()} ()} changed. */
int EVENT_IS_LOADING_CHANGED = 3;
@ -1382,7 +1387,7 @@ public interface Player {
* #COMMAND_GET_VOLUME}, {@link #COMMAND_GET_DEVICE_VOLUME}, {@link #COMMAND_SET_VOLUME}, {@link
* #COMMAND_SET_DEVICE_VOLUME}, {@link #COMMAND_ADJUST_DEVICE_VOLUME}, {@link
* #COMMAND_SET_VIDEO_SURFACE}, {@link #COMMAND_GET_TEXT}, {@link
* #COMMAND_SET_TRACK_SELECTION_PARAMETERS} or {@link #COMMAND_GET_TRACK_INFOS}.
* #COMMAND_SET_TRACK_SELECTION_PARAMETERS} or {@link #COMMAND_GET_TRACKS}.
*/
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
// with Kotlin usages from before TYPE_USE was added.
@ -1420,7 +1425,7 @@ public interface Player {
COMMAND_SET_VIDEO_SURFACE,
COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACK_INFOS,
COMMAND_GET_TRACKS,
})
@interface Command {}
/** Command to start, pause or resume playback. */
@ -1498,8 +1503,8 @@ public interface Player {
int COMMAND_GET_TEXT = 28;
/** Command to set the player's track selection parameters. */
int COMMAND_SET_TRACK_SELECTION_PARAMETERS = 29;
/** Command to get track infos. */
int COMMAND_GET_TRACK_INFOS = 30;
/** Command to get details of the current track selection. */
int COMMAND_GET_TRACKS = 30;
/** Represents an invalid {@link Command}. */
int COMMAND_INVALID = -1;
@ -2088,11 +2093,11 @@ public interface Player {
void release();
/**
* Returns information about the current tracks.
* Returns the current tracks.
*
* @see Listener#onTracksInfoChanged(TracksInfo)
* @see Listener#onTracksChanged(Tracks)
*/
TracksInfo getCurrentTracksInfo();
Tracks getCurrentTracks();
/**
* Returns the parameters constraining the track selection.

View File

@ -35,8 +35,8 @@ import java.util.Arrays;
import java.util.List;
/**
* An immutable group of tracks. All tracks in a group present the same content, but their formats
* may differ.
* An immutable group of tracks available within a media stream. All tracks in a group present the
* same content, but their formats may differ.
*
* <p>As an example of how tracks can be grouped, consider an adaptive playback where a main video
* feed is provided in five resolutions, and an alternative video feed (e.g., a different camera
@ -48,17 +48,21 @@ import java.util.List;
* languages is not considered to be the same. Conversely, audio tracks in the same language that
* only differ in properties such as bitrate, sampling rate, channel count and so on can be grouped.
* This also applies to text tracks.
*
* <p>Note also that this class only contains information derived from the media itself. Unlike
* {@link Tracks.Group}, it does not include runtime information such as the extent to which
* playback of each track is supported by the device, or which tracks are currently selected.
*/
public final class TrackGroup implements Bundleable {
private static final String TAG = "TrackGroup";
/** The number of tracks in the group. */
public final int length;
@UnstableApi public final int length;
/** An identifier for the track group. */
public final String id;
@UnstableApi public final String id;
/** The type of tracks in the group. */
public final @C.TrackType int type;
@UnstableApi public final @C.TrackType int type;
private final Format[] formats;
@ -113,6 +117,7 @@ public final class TrackGroup implements Bundleable {
* @param index The index of the track.
* @return The track's format.
*/
@UnstableApi
public Format getFormat(int index) {
return formats[index];
}
@ -126,6 +131,7 @@ public final class TrackGroup implements Bundleable {
* @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists.
*/
@SuppressWarnings("ReferenceEquality")
@UnstableApi
public int indexOf(Format format) {
for (int i = 0; i < formats.length; i++) {
if (format == formats[i]) {

View File

@ -49,8 +49,8 @@ import java.util.List;
*/
public final class TrackSelectionOverride implements Bundleable {
/** The {@link TrackGroup} whose {@link #trackIndices} are forced to be selected. */
public final TrackGroup trackGroup;
/** The media {@link TrackGroup} whose {@link #trackIndices} are forced to be selected. */
public final TrackGroup mediaTrackGroup;
/** The indices of tracks in a {@link TrackGroup} to be selected. */
public final ImmutableList<Integer> trackIndices;
@ -68,32 +68,32 @@ public final class TrackSelectionOverride implements Bundleable {
/**
* Constructs an instance to force {@code trackIndex} in {@code trackGroup} to be selected.
*
* @param trackGroup The {@link TrackGroup} for which to override the track selection.
* @param mediaTrackGroup The media {@link TrackGroup} for which to override the track selection.
* @param trackIndex The index of the track in the {@link TrackGroup} to select.
*/
public TrackSelectionOverride(TrackGroup trackGroup, int trackIndex) {
this(trackGroup, ImmutableList.of(trackIndex));
public TrackSelectionOverride(TrackGroup mediaTrackGroup, int trackIndex) {
this(mediaTrackGroup, ImmutableList.of(trackIndex));
}
/**
* Constructs an instance to force {@code trackIndices} in {@code trackGroup} to be selected.
*
* @param trackGroup The {@link TrackGroup} for which to override the track selection.
* @param mediaTrackGroup The media {@link TrackGroup} for which to override the track selection.
* @param trackIndices The indices of the tracks in the {@link TrackGroup} to select.
*/
public TrackSelectionOverride(TrackGroup trackGroup, List<Integer> trackIndices) {
public TrackSelectionOverride(TrackGroup mediaTrackGroup, List<Integer> trackIndices) {
if (!trackIndices.isEmpty()) {
if (min(trackIndices) < 0 || max(trackIndices) >= trackGroup.length) {
if (min(trackIndices) < 0 || max(trackIndices) >= mediaTrackGroup.length) {
throw new IndexOutOfBoundsException();
}
}
this.trackGroup = trackGroup;
this.mediaTrackGroup = mediaTrackGroup;
this.trackIndices = ImmutableList.copyOf(trackIndices);
}
/** Returns the {@link C.TrackType} of the overridden track group. */
public @C.TrackType int getTrackType() {
return trackGroup.type;
public @C.TrackType int getType() {
return mediaTrackGroup.type;
}
@Override
@ -105,12 +105,12 @@ public final class TrackSelectionOverride implements Bundleable {
return false;
}
TrackSelectionOverride that = (TrackSelectionOverride) obj;
return trackGroup.equals(that.trackGroup) && trackIndices.equals(that.trackIndices);
return mediaTrackGroup.equals(that.mediaTrackGroup) && trackIndices.equals(that.trackIndices);
}
@Override
public int hashCode() {
return trackGroup.hashCode() + 31 * trackIndices.hashCode();
return mediaTrackGroup.hashCode() + 31 * trackIndices.hashCode();
}
// Bundleable implementation
@ -119,7 +119,7 @@ public final class TrackSelectionOverride implements Bundleable {
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putBundle(keyForField(FIELD_TRACK_GROUP), trackGroup.toBundle());
bundle.putBundle(keyForField(FIELD_TRACK_GROUP), mediaTrackGroup.toBundle());
bundle.putIntArray(keyForField(FIELD_TRACKS), Ints.toArray(trackIndices));
return bundle;
}
@ -129,9 +129,9 @@ public final class TrackSelectionOverride implements Bundleable {
public static final Creator<TrackSelectionOverride> CREATOR =
bundle -> {
Bundle trackGroupBundle = checkNotNull(bundle.getBundle(keyForField(FIELD_TRACK_GROUP)));
TrackGroup trackGroup = TrackGroup.CREATOR.fromBundle(trackGroupBundle);
TrackGroup mediaTrackGroup = TrackGroup.CREATOR.fromBundle(trackGroupBundle);
int[] tracks = checkNotNull(bundle.getIntArray(keyForField(FIELD_TRACKS)));
return new TrackSelectionOverride(trackGroup, Ints.asList(tracks));
return new TrackSelectionOverride(mediaTrackGroup, Ints.asList(tracks));
};
private static String keyForField(@FieldNumber int field) {

View File

@ -96,6 +96,7 @@ public class TrackSelectionParameters implements Bundleable {
// Text
private ImmutableList<String> preferredTextLanguages;
private @C.RoleFlags int preferredTextRoleFlags;
private @C.SelectionFlags int ignoredTextSelectionFlags;
private boolean selectUndeterminedTextLanguage;
// General
private boolean forceLowestBitrate;
@ -129,6 +130,7 @@ public class TrackSelectionParameters implements Bundleable {
// Text
preferredTextLanguages = ImmutableList.of();
preferredTextRoleFlags = 0;
ignoredTextSelectionFlags = 0;
selectUndeterminedTextLanguage = false;
// General
forceLowestBitrate = false;
@ -229,6 +231,10 @@ public class TrackSelectionParameters implements Bundleable {
bundle.getInt(
keyForField(FIELD_PREFERRED_TEXT_ROLE_FLAGS),
DEFAULT_WITHOUT_CONTEXT.preferredTextRoleFlags);
ignoredTextSelectionFlags =
bundle.getInt(
keyForField(FIELD_IGNORED_TEXT_SELECTION_FLAGS),
DEFAULT_WITHOUT_CONTEXT.ignoredTextSelectionFlags);
selectUndeterminedTextLanguage =
bundle.getBoolean(
keyForField(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE),
@ -249,7 +255,7 @@ public class TrackSelectionParameters implements Bundleable {
overrides = new HashMap<>();
for (int i = 0; i < overrideList.size(); i++) {
TrackSelectionOverride override = overrideList.get(i);
overrides.put(override.trackGroup, override);
overrides.put(override.mediaTrackGroup, override);
}
int[] disabledTrackTypeArray =
firstNonNull(bundle.getIntArray(keyForField(FIELD_DISABLED_TRACK_TYPE)), new int[0]);
@ -292,6 +298,7 @@ public class TrackSelectionParameters implements Bundleable {
// Text
preferredTextLanguages = parameters.preferredTextLanguages;
preferredTextRoleFlags = parameters.preferredTextRoleFlags;
ignoredTextSelectionFlags = parameters.ignoredTextSelectionFlags;
selectUndeterminedTextLanguage = parameters.selectUndeterminedTextLanguage;
// General
forceLowestBitrate = parameters.forceLowestBitrate;
@ -615,6 +622,18 @@ public class TrackSelectionParameters implements Bundleable {
return this;
}
/**
* Sets a bitmask of selection flags that are ignored for text track selections.
*
* @param ignoredTextSelectionFlags A bitmask of {@link C.SelectionFlags} that are ignored for
* text track selections.
* @return This builder.
*/
public Builder setIgnoredTextSelectionFlags(@C.SelectionFlags int ignoredTextSelectionFlags) {
this.ignoredTextSelectionFlags = ignoredTextSelectionFlags;
return this;
}
/**
* Sets whether a text track with undetermined language should be selected if no track with
* {@link #setPreferredTextLanguages(String...) a preferred language} is available, or if the
@ -659,20 +678,20 @@ public class TrackSelectionParameters implements Bundleable {
/** Adds an override, replacing any override for the same {@link TrackGroup}. */
public Builder addOverride(TrackSelectionOverride override) {
overrides.put(override.trackGroup, override);
overrides.put(override.mediaTrackGroup, override);
return this;
}
/** Sets an override, replacing all existing overrides with the same track type. */
public Builder setOverrideForType(TrackSelectionOverride override) {
clearOverridesOfType(override.getTrackType());
overrides.put(override.trackGroup, override);
clearOverridesOfType(override.getType());
overrides.put(override.mediaTrackGroup, override);
return this;
}
/** Removes the override for the provided {@link TrackGroup}, if there is one. */
public Builder clearOverride(TrackGroup trackGroup) {
overrides.remove(trackGroup);
/** Removes the override for the provided media {@link TrackGroup}, if there is one. */
public Builder clearOverride(TrackGroup mediaTrackGroup) {
overrides.remove(mediaTrackGroup);
return this;
}
@ -681,7 +700,7 @@ public class TrackSelectionParameters implements Bundleable {
Iterator<TrackSelectionOverride> it = overrides.values().iterator();
while (it.hasNext()) {
TrackSelectionOverride override = it.next();
if (override.getTrackType() == trackType) {
if (override.getType() == trackType) {
it.remove();
}
}
@ -900,6 +919,11 @@ public class TrackSelectionParameters implements Bundleable {
* is enabled.
*/
public final @C.RoleFlags int preferredTextRoleFlags;
/**
* Bitmask of selection flags that are ignored for text track selections. See {@link
* C.SelectionFlags}. The default value is {@code 0} (i.e., no flags are ignored).
*/
public final @C.SelectionFlags int ignoredTextSelectionFlags;
/**
* Whether a text track with undetermined language should be selected if no track with {@link
* #preferredTextLanguages} is available, or if {@link #preferredTextLanguages} is unset. The
@ -953,6 +977,7 @@ public class TrackSelectionParameters implements Bundleable {
// Text
this.preferredTextLanguages = builder.preferredTextLanguages;
this.preferredTextRoleFlags = builder.preferredTextRoleFlags;
this.ignoredTextSelectionFlags = builder.ignoredTextSelectionFlags;
this.selectUndeterminedTextLanguage = builder.selectUndeterminedTextLanguage;
// General
this.forceLowestBitrate = builder.forceLowestBitrate;
@ -996,8 +1021,10 @@ public class TrackSelectionParameters implements Bundleable {
&& maxAudioChannelCount == other.maxAudioChannelCount
&& maxAudioBitrate == other.maxAudioBitrate
&& preferredAudioMimeTypes.equals(other.preferredAudioMimeTypes)
// Text
&& preferredTextLanguages.equals(other.preferredTextLanguages)
&& preferredTextRoleFlags == other.preferredTextRoleFlags
&& ignoredTextSelectionFlags == other.ignoredTextSelectionFlags
&& selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage
// General
&& forceLowestBitrate == other.forceLowestBitrate
@ -1032,6 +1059,7 @@ public class TrackSelectionParameters implements Bundleable {
// Text
result = 31 * result + preferredTextLanguages.hashCode();
result = 31 * result + preferredTextRoleFlags;
result = 31 * result + ignoredTextSelectionFlags;
result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0);
// General
result = 31 * result + (forceLowestBitrate ? 1 : 0);
@ -1046,11 +1074,7 @@ public class TrackSelectionParameters implements Bundleable {
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
FIELD_PREFERRED_AUDIO_LANGUAGES,
FIELD_PREFERRED_AUDIO_ROLE_FLAGS,
FIELD_PREFERRED_TEXT_LANGUAGES,
FIELD_PREFERRED_TEXT_ROLE_FLAGS,
FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE,
// Video
FIELD_MAX_VIDEO_WIDTH,
FIELD_MAX_VIDEO_HEIGHT,
FIELD_MAX_VIDEO_FRAMERATE,
@ -1063,14 +1087,23 @@ public class TrackSelectionParameters implements Bundleable {
FIELD_VIEWPORT_HEIGHT,
FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE,
FIELD_PREFERRED_VIDEO_MIMETYPES,
FIELD_PREFERRED_VIDEO_ROLE_FLAGS,
// Audio
FIELD_PREFERRED_AUDIO_LANGUAGES,
FIELD_PREFERRED_AUDIO_ROLE_FLAGS,
FIELD_MAX_AUDIO_CHANNEL_COUNT,
FIELD_MAX_AUDIO_BITRATE,
FIELD_PREFERRED_AUDIO_MIME_TYPES,
// Text
FIELD_PREFERRED_TEXT_LANGUAGES,
FIELD_PREFERRED_TEXT_ROLE_FLAGS,
FIELD_IGNORED_TEXT_SELECTION_FLAGS,
FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE,
// General
FIELD_FORCE_LOWEST_BITRATE,
FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE,
FIELD_SELECTION_OVERRIDES,
FIELD_DISABLED_TRACK_TYPE,
FIELD_PREFERRED_VIDEO_ROLE_FLAGS
})
private @interface FieldNumber {}
@ -1099,6 +1132,7 @@ public class TrackSelectionParameters implements Bundleable {
private static final int FIELD_SELECTION_OVERRIDES = 23;
private static final int FIELD_DISABLED_TRACK_TYPE = 24;
private static final int FIELD_PREFERRED_VIDEO_ROLE_FLAGS = 25;
private static final int FIELD_IGNORED_TEXT_SELECTION_FLAGS = 26;
@UnstableApi
@Override
@ -1136,6 +1170,7 @@ public class TrackSelectionParameters implements Bundleable {
bundle.putStringArray(
keyForField(FIELD_PREFERRED_TEXT_LANGUAGES), preferredTextLanguages.toArray(new String[0]));
bundle.putInt(keyForField(FIELD_PREFERRED_TEXT_ROLE_FLAGS), preferredTextRoleFlags);
bundle.putInt(keyForField(FIELD_IGNORED_TEXT_SELECTION_FLAGS), ignoredTextSelectionFlags);
bundle.putBoolean(
keyForField(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE), selectUndeterminedTextLanguage);
// General

View File

@ -37,19 +37,19 @@ import java.util.Arrays;
import java.util.List;
/** Information about groups of tracks. */
public final class TracksInfo implements Bundleable {
public final class Tracks implements Bundleable {
/**
* Information about a single group of tracks, including the underlying {@link TrackGroup}, the
* level to which each track is supported by the player, and whether any of the tracks are
* selected.
*/
public static final class TrackGroupInfo implements Bundleable {
public static final class Group implements Bundleable {
/** The number of tracks in the group. */
public final int length;
private final TrackGroup trackGroup;
private final TrackGroup mediaTrackGroup;
private final boolean adaptiveSupported;
private final @C.FormatSupport int[] trackSupport;
private final boolean[] trackSelected;
@ -57,45 +57,52 @@ public final class TracksInfo implements Bundleable {
/**
* Constructs an instance.
*
* @param trackGroup The underlying {@link TrackGroup}.
* @param mediaTrackGroup The underlying {@link TrackGroup} defined by the media.
* @param adaptiveSupported Whether the player supports adaptive selections containing more than
* one track in the group.
* @param trackSupport The {@link C.FormatSupport} of each track in the group.
* @param trackSelected Whether each track in the {@code trackGroup} is selected.
*/
@UnstableApi
public TrackGroupInfo(
TrackGroup trackGroup,
public Group(
TrackGroup mediaTrackGroup,
boolean adaptiveSupported,
@C.FormatSupport int[] trackSupport,
boolean[] trackSelected) {
length = trackGroup.length;
length = mediaTrackGroup.length;
checkArgument(length == trackSupport.length && length == trackSelected.length);
this.trackGroup = trackGroup;
this.mediaTrackGroup = mediaTrackGroup;
this.adaptiveSupported = adaptiveSupported && length > 1;
this.trackSupport = trackSupport.clone();
this.trackSelected = trackSelected.clone();
}
/** Returns the underlying {@link TrackGroup}. */
public TrackGroup getTrackGroup() {
return trackGroup;
/**
* Returns the underlying {@link TrackGroup} defined by the media.
*
* <p>Unlike this class, {@link TrackGroup} only contains information defined by the media
* itself, and does not contain runtime information such as which tracks are supported and
* currently selected. This makes it suitable for use as a {@code key} in certain {@code (key,
* value)} data structures.
*/
public TrackGroup getMediaTrackGroup() {
return mediaTrackGroup;
}
/**
* Returns the {@link Format} for a specified track.
*
* @param trackIndex The index of the track in the {@link TrackGroup}.
* @param trackIndex The index of the track in the group.
* @return The {@link Format} of the track.
*/
public Format getTrackFormat(int trackIndex) {
return trackGroup.getFormat(trackIndex);
return mediaTrackGroup.getFormat(trackIndex);
}
/**
* Returns the level of support for a specified track.
*
* @param trackIndex The index of the track in the {@link TrackGroup}.
* @param trackIndex The index of the track in the group.
* @return The {@link C.FormatSupport} of the track.
*/
@UnstableApi
@ -107,7 +114,7 @@ public final class TracksInfo implements Bundleable {
* Returns whether a specified track is supported for playback, without exceeding the advertised
* capabilities of the device. Equivalent to {@code isTrackSupported(trackIndex, false)}.
*
* @param trackIndex The index of the track in the {@link TrackGroup}.
* @param trackIndex The index of the track in the group.
* @return True if the track's format can be played, false otherwise.
*/
public boolean isTrackSupported(int trackIndex) {
@ -117,7 +124,7 @@ public final class TracksInfo implements Bundleable {
/**
* Returns whether a specified track is supported for playback.
*
* @param trackIndex The index of the track in the {@link TrackGroup}.
* @param trackIndex The index of the track in the group.
* @param allowExceedsCapabilities Whether to consider the track as supported if it has a
* supported {@link Format#sampleMimeType MIME type}, but otherwise exceeds the advertised
* capabilities of the device. For example, a video track for which there's a corresponding
@ -178,7 +185,7 @@ public final class TracksInfo implements Bundleable {
* playing, however some player implementations have ways of getting such information. For
* example, ExoPlayer provides this information via {@code ExoTrackSelection.getSelectedFormat}.
*
* @param trackIndex The index of the track in the {@link TrackGroup}.
* @param trackIndex The index of the track in the group.
* @return True if the track is selected, false otherwise.
*/
public boolean isTrackSelected(int trackIndex) {
@ -186,8 +193,8 @@ public final class TracksInfo implements Bundleable {
}
/** Returns the {@link C.TrackType} of the group. */
public @C.TrackType int getTrackType() {
return trackGroup.type;
public @C.TrackType int getType() {
return mediaTrackGroup.type;
}
@Override
@ -198,16 +205,16 @@ public final class TracksInfo implements Bundleable {
if (other == null || getClass() != other.getClass()) {
return false;
}
TrackGroupInfo that = (TrackGroupInfo) other;
Group that = (Group) other;
return adaptiveSupported == that.adaptiveSupported
&& trackGroup.equals(that.trackGroup)
&& mediaTrackGroup.equals(that.mediaTrackGroup)
&& Arrays.equals(trackSupport, that.trackSupport)
&& Arrays.equals(trackSelected, that.trackSelected);
}
@Override
public int hashCode() {
int result = trackGroup.hashCode();
int result = mediaTrackGroup.hashCode();
result = 31 * result + (adaptiveSupported ? 1 : 0);
result = 31 * result + Arrays.hashCode(trackSupport);
result = 31 * result + Arrays.hashCode(trackSelected);
@ -234,16 +241,16 @@ public final class TracksInfo implements Bundleable {
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putBundle(keyForField(FIELD_TRACK_GROUP), trackGroup.toBundle());
bundle.putBundle(keyForField(FIELD_TRACK_GROUP), mediaTrackGroup.toBundle());
bundle.putIntArray(keyForField(FIELD_TRACK_SUPPORT), trackSupport);
bundle.putBooleanArray(keyForField(FIELD_TRACK_SELECTED), trackSelected);
bundle.putBoolean(keyForField(FIELD_ADAPTIVE_SUPPORTED), adaptiveSupported);
return bundle;
}
/** Object that can restores a {@code TracksInfo} from a {@link Bundle}. */
/** Object that can restore a group of tracks from a {@link Bundle}. */
@UnstableApi
public static final Creator<TrackGroupInfo> CREATOR =
public static final Creator<Group> CREATOR =
bundle -> {
TrackGroup trackGroup =
fromNullableBundle(
@ -258,7 +265,7 @@ public final class TracksInfo implements Bundleable {
new boolean[trackGroup.length]);
boolean adaptiveSupported =
bundle.getBoolean(keyForField(FIELD_ADAPTIVE_SUPPORTED), false);
return new TrackGroupInfo(trackGroup, adaptiveSupported, trackSupport, selected);
return new Group(trackGroup, adaptiveSupported, trackSupport, selected);
};
private static String keyForField(@FieldNumber int field) {
@ -266,31 +273,35 @@ public final class TracksInfo implements Bundleable {
}
}
/** An {@code TrackInfo} that contains no tracks. */
@UnstableApi public static final TracksInfo EMPTY = new TracksInfo(ImmutableList.of());
/** Empty tracks. */
@UnstableApi public static final Tracks EMPTY = new Tracks(ImmutableList.of());
private final ImmutableList<TrackGroupInfo> trackGroupInfos;
private final ImmutableList<Group> groups;
/**
* Constructs an instance.
*
* @param trackGroupInfos The {@link TrackGroupInfo TrackGroupInfos} describing the groups of
* tracks.
* @param groups The {@link Group groups} of tracks.
*/
@UnstableApi
public TracksInfo(List<TrackGroupInfo> trackGroupInfos) {
this.trackGroupInfos = ImmutableList.copyOf(trackGroupInfos);
public Tracks(List<Group> groups) {
this.groups = ImmutableList.copyOf(groups);
}
/** Returns the {@link TrackGroupInfo TrackGroupInfos} describing the groups of tracks. */
public ImmutableList<TrackGroupInfo> getTrackGroupInfos() {
return trackGroupInfos;
/** Returns the {@link Group groups} of tracks. */
public ImmutableList<Group> getGroups() {
return groups;
}
/** Returns {@code true} if there are no tracks, and {@code false} otherwise. */
public boolean isEmpty() {
return groups.isEmpty();
}
/** Returns true if there are tracks of type {@code trackType}, and false otherwise. */
public boolean containsType(@C.TrackType int trackType) {
for (int i = 0; i < trackGroupInfos.size(); i++) {
if (trackGroupInfos.get(i).getTrackType() == trackType) {
for (int i = 0; i < groups.size(); i++) {
if (groups.get(i).getType() == trackType) {
return true;
}
}
@ -299,7 +310,7 @@ public final class TracksInfo implements Bundleable {
/**
* Returns true if at least one track of type {@code trackType} is {@link
* TrackGroupInfo#isTrackSupported(int) supported}.
* Group#isTrackSupported(int) supported}.
*/
public boolean isTypeSupported(@C.TrackType int trackType) {
return isTypeSupported(trackType, /* allowExceedsCapabilities= */ false);
@ -307,7 +318,7 @@ public final class TracksInfo implements Bundleable {
/**
* Returns true if at least one track of type {@code trackType} is {@link
* TrackGroupInfo#isTrackSupported(int, boolean) supported}.
* Group#isTrackSupported(int, boolean) supported}.
*
* @param allowExceedsCapabilities Whether to consider the track as supported if it has a
* supported {@link Format#sampleMimeType MIME type}, but otherwise exceeds the advertised
@ -316,9 +327,9 @@ public final class TracksInfo implements Bundleable {
* Such tracks may be playable in some cases.
*/
public boolean isTypeSupported(@C.TrackType int trackType, boolean allowExceedsCapabilities) {
for (int i = 0; i < trackGroupInfos.size(); i++) {
if (trackGroupInfos.get(i).getTrackType() == trackType) {
if (trackGroupInfos.get(i).isSupported(allowExceedsCapabilities)) {
for (int i = 0; i < groups.size(); i++) {
if (groups.get(i).getType() == trackType) {
if (groups.get(i).isSupported(allowExceedsCapabilities)) {
return true;
}
}
@ -348,9 +359,9 @@ public final class TracksInfo implements Bundleable {
/** Returns true if at least one track of the type {@code trackType} is selected for playback. */
public boolean isTypeSelected(@C.TrackType int trackType) {
for (int i = 0; i < trackGroupInfos.size(); i++) {
TrackGroupInfo trackGroupInfo = trackGroupInfos.get(i);
if (trackGroupInfo.isSelected() && trackGroupInfo.getTrackType() == trackType) {
for (int i = 0; i < groups.size(); i++) {
Group group = groups.get(i);
if (group.isSelected() && group.getType() == trackType) {
return true;
}
}
@ -365,13 +376,13 @@ public final class TracksInfo implements Bundleable {
if (other == null || getClass() != other.getClass()) {
return false;
}
TracksInfo that = (TracksInfo) other;
return trackGroupInfos.equals(that.trackGroupInfos);
Tracks that = (Tracks) other;
return groups.equals(that.groups);
}
@Override
public int hashCode() {
return trackGroupInfos.hashCode();
return groups.hashCode();
}
// Bundleable implementation.
@ -379,31 +390,30 @@ public final class TracksInfo implements Bundleable {
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_TRACK_GROUP_INFOS,
FIELD_TRACK_GROUPS,
})
private @interface FieldNumber {}
private static final int FIELD_TRACK_GROUP_INFOS = 0;
private static final int FIELD_TRACK_GROUPS = 0;
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(
keyForField(FIELD_TRACK_GROUP_INFOS), toBundleArrayList(trackGroupInfos));
bundle.putParcelableArrayList(keyForField(FIELD_TRACK_GROUPS), toBundleArrayList(groups));
return bundle;
}
/** Object that can restore a {@code TracksInfo} from a {@link Bundle}. */
/** Object that can restore tracks from a {@link Bundle}. */
@UnstableApi
public static final Creator<TracksInfo> CREATOR =
public static final Creator<Tracks> CREATOR =
bundle -> {
List<TrackGroupInfo> trackGroupInfos =
List<Group> groups =
fromBundleNullableList(
TrackGroupInfo.CREATOR,
bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUP_INFOS)),
Group.CREATOR,
bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUPS)),
/* defaultValue= */ ImmutableList.of());
return new TracksInfo(trackGroupInfos);
return new Tracks(groups);
};
private static String keyForField(@FieldNumber int field) {

View File

@ -15,6 +15,8 @@
*/
package androidx.media3.common.util;
import static androidx.media3.common.util.Assertions.checkArgument;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
@ -31,6 +33,12 @@ public final class CodecSpecificDataUtil {
private static final String[] HEVC_GENERAL_PROFILE_SPACE_STRINGS =
new String[] {"", "A", "B", "C"};
// MP4V-ES
private static final int VISUAL_OBJECT_LAYER = 1;
private static final int VISUAL_OBJECT_LAYER_START = 0x20;
private static final int EXTENDED_PAR = 0x0F;
private static final int RECTANGULAR = 0x00;
/**
* Parses an ALAC AudioSpecificConfig (i.e. an <a
* href="https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt">ALACSpecificConfig</a>).
@ -72,6 +80,87 @@ public final class CodecSpecificDataUtil {
&& initializationData.get(0)[0] == 1;
}
/**
* Parses an MPEG-4 Visual configuration information, as defined in ISO/IEC14496-2.
*
* @param videoSpecificConfig A byte array containing the MPEG-4 Visual configuration information
* to parse.
* @return A pair of the video's width and height.
*/
public static Pair<Integer, Integer> getVideoResolutionFromMpeg4VideoConfig(
byte[] videoSpecificConfig) {
int offset = 0;
boolean foundVOL = false;
ParsableByteArray scratchBytes = new ParsableByteArray(videoSpecificConfig);
while (offset + 3 < videoSpecificConfig.length) {
if (scratchBytes.readUnsignedInt24() != VISUAL_OBJECT_LAYER
|| (videoSpecificConfig[offset + 3] & 0xF0) != VISUAL_OBJECT_LAYER_START) {
scratchBytes.setPosition(scratchBytes.getPosition() - 2);
offset++;
continue;
}
foundVOL = true;
break;
}
checkArgument(foundVOL, "Invalid input: VOL not found.");
ParsableBitArray scratchBits = new ParsableBitArray(videoSpecificConfig);
// Skip the start codecs from the bitstream
scratchBits.skipBits((offset + 4) * 8);
scratchBits.skipBits(1); // random_accessible_vol
scratchBits.skipBits(8); // video_object_type_indication
if (scratchBits.readBit()) { // object_layer_identifier
scratchBits.skipBits(4); // video_object_layer_verid
scratchBits.skipBits(3); // video_object_layer_priority
}
int aspectRatioInfo = scratchBits.readBits(4);
if (aspectRatioInfo == EXTENDED_PAR) {
scratchBits.skipBits(8); // par_width
scratchBits.skipBits(8); // par_height
}
if (scratchBits.readBit()) { // vol_control_parameters
scratchBits.skipBits(2); // chroma_format
scratchBits.skipBits(1); // low_delay
if (scratchBits.readBit()) { // vbv_parameters
scratchBits.skipBits(79);
}
}
int videoObjectLayerShape = scratchBits.readBits(2);
checkArgument(
videoObjectLayerShape == RECTANGULAR,
"Only supports rectangular video object layer shape.");
checkArgument(scratchBits.readBit()); // marker_bit
int vopTimeIncrementResolution = scratchBits.readBits(16);
checkArgument(scratchBits.readBit()); // marker_bit
if (scratchBits.readBit()) { // fixed_vop_rate
checkArgument(vopTimeIncrementResolution > 0);
vopTimeIncrementResolution--;
int numBitsToSkip = 0;
while (vopTimeIncrementResolution > 0) {
numBitsToSkip++;
vopTimeIncrementResolution >>= 1;
}
scratchBits.skipBits(numBitsToSkip); // fixed_vop_time_increment
}
checkArgument(scratchBits.readBit()); // marker_bit
int videoObjectLayerWidth = scratchBits.readBits(13);
checkArgument(scratchBits.readBit()); // marker_bit
int videoObjectLayerHeight = scratchBits.readBits(13);
checkArgument(scratchBits.readBit()); // marker_bit
scratchBits.skipBits(1); // interlaced
return Pair.create(videoObjectLayerWidth, videoObjectLayerHeight);
}
/**
* Builds an RFC 6381 AVC codec string using the provided parameters.
*

View File

@ -364,45 +364,48 @@ public final class GlProgram {
* <p>Should be called before each drawing call.
*/
public void bind() {
if (type == GLES20.GL_FLOAT) {
GLES20.glUniform1fv(location, /* count= */ 1, value, /* offset= */ 0);
GlUtil.checkGlError();
return;
switch (type) {
case GLES20.GL_FLOAT:
GLES20.glUniform1fv(location, /* count= */ 1, value, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_FLOAT_VEC2:
GLES20.glUniform2fv(location, /* count= */ 1, value, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_FLOAT_VEC3:
GLES20.glUniform3fv(location, /* count= */ 1, value, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_FLOAT_MAT3:
GLES20.glUniformMatrix3fv(
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_FLOAT_MAT4:
GLES20.glUniformMatrix4fv(
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_SAMPLER_2D:
case GLES11Ext.GL_SAMPLER_EXTERNAL_OES:
case GL_SAMPLER_EXTERNAL_2D_Y2Y_EXT:
if (texId == 0) {
throw new IllegalStateException("No call to setSamplerTexId() before bind.");
}
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + texUnitIndex);
GlUtil.checkGlError();
GlUtil.bindTexture(
type == GLES20.GL_SAMPLER_2D
? GLES20.GL_TEXTURE_2D
: GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
texId);
GLES20.glUniform1i(location, texUnitIndex);
GlUtil.checkGlError();
break;
default:
throw new IllegalStateException("Unexpected uniform type: " + type);
}
if (type == GLES20.GL_FLOAT_MAT3) {
GLES20.glUniformMatrix3fv(
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
GlUtil.checkGlError();
return;
}
if (type == GLES20.GL_FLOAT_MAT4) {
GLES20.glUniformMatrix4fv(
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
GlUtil.checkGlError();
return;
}
if (texId == 0) {
throw new IllegalStateException("No call to setSamplerTexId() before bind.");
}
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + texUnitIndex);
if (type == GLES11Ext.GL_SAMPLER_EXTERNAL_OES || type == GL_SAMPLER_EXTERNAL_2D_Y2Y_EXT) {
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
} else if (type == GLES20.GL_SAMPLER_2D) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
} else {
throw new IllegalStateException("Unexpected uniform type: " + type);
}
GLES20.glUniform1i(location, texUnitIndex);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GlUtil.checkGlError();
}
}
}

View File

@ -56,6 +56,9 @@ public final class GlUtil {
/** Number of vertices in a rectangle. */
public static final int RECTANGLE_VERTICES_COUNT = 4;
/** Length of the normalized device coordinate (NDC) space, which spans from -1 to 1. */
public static final float LENGTH_NDC = 2f;
private static final String TAG = "GlUtil";
// https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_protected_content.txt
@ -147,7 +150,11 @@ public final class GlUtil {
}
/**
* Returns whether creating a GL context with {@value #EXTENSION_SURFACELESS_CONTEXT} is possible.
* Returns whether the {@value #EXTENSION_SURFACELESS_CONTEXT} extension is supported.
*
* <p>This extension allows passing {@link EGL14#EGL_NO_SURFACE} for both the write and read
* surfaces in a call to {@link EGL14#eglMakeCurrent(EGLDisplay, EGLSurface, EGLSurface,
* EGLContext)}.
*/
public static boolean isSurfacelessContextExtensionSupported() {
if (Util.SDK_INT < 17) {
@ -207,6 +214,77 @@ public final class GlUtil {
EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ);
}
/**
* Creates a new {@link EGLSurface} wrapping a pixel buffer.
*
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
* @param width The width of the pixel buffer.
* @param height The height of the pixel buffer.
*/
@RequiresApi(17)
private static EGLSurface createPbufferSurface(EGLDisplay eglDisplay, int width, int height) {
int[] pbufferAttributes =
new int[] {
EGL14.EGL_WIDTH, width,
EGL14.EGL_HEIGHT, height,
EGL14.EGL_NONE
};
return Api17.createEglPbufferSurface(
eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_8888, pbufferAttributes);
}
/**
* Returns a placeholder {@link EGLSurface} to use when reading and writing to the surface is not
* required.
*
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
* @return {@link EGL14#EGL_NO_SURFACE} if supported and a 1x1 pixel buffer surface otherwise.
*/
@RequiresApi(17)
public static EGLSurface createPlaceholderEglSurface(EGLDisplay eglDisplay) {
return isSurfacelessContextExtensionSupported()
? EGL14.EGL_NO_SURFACE
: createPbufferSurface(eglDisplay, /* width= */ 1, /* height= */ 1);
}
/**
* Creates and focuses a new {@link EGLSurface} wrapping a 1x1 pixel buffer.
*
* @param eglContext The {@link EGLContext} to make current.
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
*/
@RequiresApi(17)
public static void focusPlaceholderEglSurface(EGLContext eglContext, EGLDisplay eglDisplay) {
EGLSurface eglSurface = createPbufferSurface(eglDisplay, /* width= */ 1, /* height= */ 1);
focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1);
}
/**
* Creates and focuses a new {@link EGLSurface} wrapping a 1x1 pixel buffer, for HDR rendering
* with Rec. 2020 color primaries and using the PQ transfer function.
*
* @param eglContext The {@link EGLContext} to make current.
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
*/
@RequiresApi(17)
public static void focusPlaceholderEglSurfaceBt2020Pq(
EGLContext eglContext, EGLDisplay eglDisplay) {
int[] pbufferAttributes =
new int[] {
EGL14.EGL_WIDTH,
/* width= */ 1,
EGL14.EGL_HEIGHT,
/* height= */ 1,
EGL_GL_COLORSPACE_KHR,
EGL_GL_COLORSPACE_BT2020_PQ_EXT,
EGL14.EGL_NONE
};
EGLSurface eglSurface =
Api17.createEglPbufferSurface(
eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_1010102, pbufferAttributes);
focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1);
}
/**
* If there is an OpenGl error, logs the error and if {@link #glAssertionsEnabled} is true throws
* a {@link GlException}.
@ -335,7 +413,9 @@ public final class GlUtil {
* GL_CLAMP_TO_EDGE wrapping.
*/
public static int createExternalTexture() {
return generateAndBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
int texId = generateTexture();
bindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
return texId;
}
/**
@ -346,7 +426,8 @@ public final class GlUtil {
*/
public static int createTexture(int width, int height) {
assertValidTextureSize(width, height);
int texId = generateAndBindTexture(GLES20.GL_TEXTURE_2D);
int texId = generateTexture();
bindTexture(GLES20.GL_TEXTURE_2D, texId);
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(width * height * 4);
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D,
@ -362,27 +443,37 @@ public final class GlUtil {
return texId;
}
/** Returns a new GL texture identifier. */
private static int generateTexture() {
checkEglException(
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
int[] texId = new int[1];
GLES20.glGenTextures(/* n= */ 1, texId, /* offset= */ 0);
checkGlError();
return texId[0];
}
/**
* Returns a GL texture identifier of a newly generated and bound texture of the requested type
* with default configuration of GL_LINEAR filtering and GL_CLAMP_TO_EDGE wrapping.
* Binds the texture of the given type with default configuration of GL_LINEAR filtering and
* GL_CLAMP_TO_EDGE wrapping.
*
* @param texId The texture identifier.
* @param textureTarget The target to which the texture is bound, e.g. {@link
* GLES20#GL_TEXTURE_2D} for a two-dimensional texture or {@link
* GLES11Ext#GL_TEXTURE_EXTERNAL_OES} for an external texture.
*/
private static int generateAndBindTexture(int textureTarget) {
int[] texId = new int[1];
GLES20.glGenTextures(/* n= */ 1, texId, /* offset= */ 0);
checkGlError();
GLES20.glBindTexture(textureTarget, texId[0]);
/* package */ static void bindTexture(int textureTarget, int texId) {
GLES20.glBindTexture(textureTarget, texId);
checkGlError();
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
checkGlError();
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
checkGlError();
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
checkGlError();
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
checkGlError();
return texId[0];
}
/**
@ -391,6 +482,9 @@ public final class GlUtil {
* @param texId The identifier of the texture to attach to the framebuffer.
*/
public static int createFboForTexture(int texId) {
checkEglException(
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
int[] fboId = new int[1];
GLES20.glGenFramebuffers(/* n= */ 1, fboId, /* offset= */ 0);
checkGlError();
@ -480,6 +574,19 @@ public final class GlUtil {
return eglSurface;
}
@DoNotInline
public static EGLSurface createEglPbufferSurface(
EGLDisplay eglDisplay, int[] configAttributes, int[] pbufferAttributes) {
EGLSurface eglSurface =
EGL14.eglCreatePbufferSurface(
eglDisplay,
getEglConfig(eglDisplay, configAttributes),
pbufferAttributes,
/* offset= */ 0);
checkEglException("Error creating surface");
return eglSurface;
}
@DoNotInline
public static void focusRenderTarget(
EGLDisplay eglDisplay,

View File

@ -47,6 +47,19 @@ public final class MediaFormatUtil {
// The constant value must not be changed, because it's also set by the framework MediaParser API.
public static final String KEY_PCM_ENCODING_EXTENDED = "exo-pcm-encoding-int";
/**
* The {@link MediaFormat} key for the maximum bitrate in bits per second.
*
* <p>The associated value is an integer.
*
* <p>The key string constant is the same as {@code MediaFormat#KEY_MAX_BITRATE}. Values for it
* are already returned by the framework MediaExtractor; the key is a hidden field in {@code
* MediaFormat} though, which is why it's being replicated here.
*/
// The constant value must not be changed, because it's also set by the framework MediaParser and
// MediaExtractor APIs.
public static final String KEY_MAX_BIT_RATE = "max-bitrate";
private static final int MAX_POWER_OF_TWO_INT = 1 << 30;
/**
@ -63,6 +76,7 @@ public final class MediaFormatUtil {
public static MediaFormat createMediaFormatFromFormat(Format format) {
MediaFormat result = new MediaFormat();
maybeSetInteger(result, MediaFormat.KEY_BIT_RATE, format.bitrate);
maybeSetInteger(result, KEY_MAX_BIT_RATE, format.peakBitrate);
maybeSetInteger(result, MediaFormat.KEY_CHANNEL_COUNT, format.channelCount);
maybeSetColorInfo(result, format.colorInfo);

View File

@ -47,8 +47,27 @@ import java.lang.annotation.Target;
* Android Studio, in order to alert developers to the risk of breaking changes.
*
* <p>Individual usage sites can be opted-in to suppress the lint error by using the {@link
* androidx.annotation.OptIn} annotation: {@code @androidx.annotation.OptIn(markerClass =
* androidx.media3.common.util.UnstableApi.class)}.
* androidx.annotation.OptIn} annotation.
*
* <p>In Java:
*
* <pre>{@code
* import androidx.annotation.OptIn;
* import androidx.media3.common.util.UnstableApi;
* ...
* @OptIn(markerClass = UnstableApi.class)
* private void methodUsingUnstableApis() { ... }
* }</pre>
*
* <p>In Kotlin:
*
* <pre>{@code
* import androidx.annotation.OptIn
* import androidx.media3.common.util.UnstableApi
* ...
* @OptIn(UnstableApi::class)
* private fun methodUsingUnstableApis() { ... }
* }</pre>
*
* <p>Whole projects can be opted-in by suppressing the specific lint error in their <a
* href="https://developer.android.com/studio/write/lint#pref">{@code lint.xml} file</a>:

View File

@ -1121,6 +1121,25 @@ public final class Util {
return min;
}
/**
* Returns the maximum value in the given {@link SparseLongArray}.
*
* @param sparseLongArray The {@link SparseLongArray}.
* @return The maximum value.
* @throws NoSuchElementException If the array is empty.
*/
@RequiresApi(18)
public static long maxValue(SparseLongArray sparseLongArray) {
if (sparseLongArray.size() == 0) {
throw new NoSuchElementException();
}
long max = Long.MIN_VALUE;
for (int i = 0; i < sparseLongArray.size(); i++) {
max = max(max, sparseLongArray.valueAt(i));
}
return max;
}
/**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving {@link
* C#TIME_UNSET} and {@link C#TIME_END_OF_SOURCE} values.
@ -1885,10 +1904,10 @@ public final class Util {
/**
* Returns the MIME type corresponding to the given adaptive {@link ContentType}, or {@code null}
* if the content type is {@link C#TYPE_OTHER}.
* if the content type is not adaptive.
*/
@Nullable
public static String getAdaptiveMimeTypeForContentType(int contentType) {
public static String getAdaptiveMimeTypeForContentType(@ContentType int contentType) {
switch (contentType) {
case C.TYPE_DASH:
return MimeTypes.APPLICATION_MPD;
@ -1896,6 +1915,7 @@ public final class Util {
return MimeTypes.APPLICATION_M3U8;
case C.TYPE_SS:
return MimeTypes.APPLICATION_SS;
case C.TYPE_RTSP:
case C.TYPE_OTHER:
default:
return null;

View File

@ -33,7 +33,7 @@ public final class TrackSelectionOverrideTest {
TrackSelectionOverride trackSelectionOverride =
new TrackSelectionOverride(newTrackGroupWithIds(1, 2), /* trackIndex= */ 1);
assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.mediaTrackGroup).isEqualTo(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.trackIndices).containsExactly(1).inOrder();
}
@ -42,7 +42,7 @@ public final class TrackSelectionOverrideTest {
TrackSelectionOverride trackSelectionOverride =
new TrackSelectionOverride(newTrackGroupWithIds(1, 2), ImmutableList.of(1));
assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.mediaTrackGroup).isEqualTo(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.trackIndices).containsExactly(1);
}
@ -51,7 +51,7 @@ public final class TrackSelectionOverrideTest {
TrackSelectionOverride trackSelectionOverride =
new TrackSelectionOverride(newTrackGroupWithIds(1, 2), ImmutableList.of());
assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.mediaTrackGroup).isEqualTo(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.trackIndices).isEmpty();
}

View File

@ -56,6 +56,7 @@ public final class TrackSelectionParametersTest {
assertThat(parameters.preferredAudioMimeTypes).isEmpty();
assertThat(parameters.preferredTextLanguages).isEmpty();
assertThat(parameters.preferredTextRoleFlags).isEqualTo(0);
assertThat(parameters.ignoredTextSelectionFlags).isEqualTo(0);
assertThat(parameters.selectUndeterminedTextLanguage).isFalse();
// General
assertThat(parameters.forceLowestBitrate).isFalse();
@ -98,6 +99,7 @@ public final class TrackSelectionParametersTest {
// Text
.setPreferredTextLanguages("de", "en")
.setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
.setIgnoredTextSelectionFlags(C.SELECTION_FLAG_AUTOSELECT)
.setSelectUndeterminedTextLanguage(true)
// General
.setForceLowestBitrate(false)
@ -141,12 +143,14 @@ public final class TrackSelectionParametersTest {
// Text
assertThat(parameters.preferredTextLanguages).containsExactly("de", "en").inOrder();
assertThat(parameters.preferredTextRoleFlags).isEqualTo(C.ROLE_FLAG_CAPTION);
assertThat(parameters.ignoredTextSelectionFlags).isEqualTo(C.SELECTION_FLAG_AUTOSELECT);
assertThat(parameters.selectUndeterminedTextLanguage).isTrue();
// General
assertThat(parameters.forceLowestBitrate).isFalse();
assertThat(parameters.forceHighestSupportedBitrate).isTrue();
assertThat(parameters.overrides)
.containsExactly(override1.trackGroup, override1, override2.trackGroup, override2);
.containsExactly(
override1.mediaTrackGroup, override1, override2.mediaTrackGroup, override2);
assertThat(parameters.disabledTrackTypes)
.containsExactly(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT);
}
@ -200,7 +204,8 @@ public final class TrackSelectionParametersTest {
TrackSelectionParameters.CREATOR.fromBundle(trackSelectionParameters.toBundle());
assertThat(fromBundle).isEqualTo(trackSelectionParameters);
assertThat(trackSelectionParameters.overrides).containsExactly(override.trackGroup, override);
assertThat(trackSelectionParameters.overrides)
.containsExactly(override.mediaTrackGroup, override);
}
@Test
@ -217,7 +222,8 @@ public final class TrackSelectionParametersTest {
.build();
assertThat(trackSelectionParameters.overrides)
.containsExactly(override1.trackGroup, override1, override2.trackGroup, override2);
.containsExactly(
override1.mediaTrackGroup, override1, override2.mediaTrackGroup, override2);
}
@Test
@ -234,7 +240,8 @@ public final class TrackSelectionParametersTest {
.addOverride(override2)
.build();
assertThat(trackSelectionParameters.overrides).containsExactly(override2.trackGroup, override2);
assertThat(trackSelectionParameters.overrides)
.containsExactly(override2.mediaTrackGroup, override2);
}
@Test
@ -250,7 +257,8 @@ public final class TrackSelectionParametersTest {
.setOverrideForType(override2)
.build();
assertThat(trackSelectionParameters.overrides).containsExactly(override2.trackGroup, override2);
assertThat(trackSelectionParameters.overrides)
.containsExactly(override2.mediaTrackGroup, override2);
}
@Test
@ -266,7 +274,8 @@ public final class TrackSelectionParametersTest {
.clearOverridesOfType(C.TRACK_TYPE_AUDIO)
.build();
assertThat(trackSelectionParameters.overrides).containsExactly(override2.trackGroup, override2);
assertThat(trackSelectionParameters.overrides)
.containsExactly(override2.mediaTrackGroup, override2);
}
@Test
@ -279,10 +288,11 @@ public final class TrackSelectionParametersTest {
new TrackSelectionParameters.Builder(getApplicationContext())
.addOverride(override1)
.addOverride(override2)
.clearOverride(override2.trackGroup)
.clearOverride(override2.mediaTrackGroup)
.build();
assertThat(trackSelectionParameters.overrides).containsExactly(override1.trackGroup, override1);
assertThat(trackSelectionParameters.overrides)
.containsExactly(override1.mediaTrackGroup, override1);
}
private static TrackGroup newTrackGroupWithIds(int... ids) {

View File

@ -1,144 +0,0 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common;
import static androidx.media3.common.MimeTypes.AUDIO_AAC;
import static androidx.media3.common.MimeTypes.VIDEO_H264;
import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.TracksInfo.TrackGroupInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link TracksInfo}. */
@RunWith(AndroidJUnit4.class)
public class TracksInfoTest {
@Test
public void roundTripViaBundle_ofEmptyTracksInfo_yieldsEqualInstance() {
TracksInfo before = TracksInfo.EMPTY;
TracksInfo after = TracksInfo.CREATOR.fromBundle(before.toBundle());
assertThat(after).isEqualTo(before);
}
@Test
public void roundTripViaBundle_ofTracksInfo_yieldsEqualInstance() {
TracksInfo before =
new TracksInfo(
ImmutableList.of(
new TrackGroupInfo(
new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()),
/* adaptiveSupported= */ false,
new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
/* tracksSelected= */ new boolean[] {true}),
new TrackGroupInfo(
new TrackGroup(
new Format.Builder().setSampleMimeType(VIDEO_H264).build(),
new Format.Builder().setSampleMimeType(VIDEO_H264).build()),
/* adaptiveSupported= */ true,
new int[] {C.FORMAT_UNSUPPORTED_DRM, C.FORMAT_UNSUPPORTED_TYPE},
/* tracksSelected= */ new boolean[] {false, true})));
TracksInfo after = TracksInfo.CREATOR.fromBundle(before.toBundle());
assertThat(after).isEqualTo(before);
}
@Test
public void tracksInfoGetters_withoutTrack_returnExpectedValues() {
TracksInfo tracksInfo = new TracksInfo(ImmutableList.of());
assertThat(tracksInfo.containsType(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracksInfo.isTypeSupported(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracksInfo.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true))
.isFalse();
assertThat(tracksInfo.isTypeSelected(C.TRACK_TYPE_AUDIO)).isFalse();
ImmutableList<TrackGroupInfo> trackGroupInfos = tracksInfo.getTrackGroupInfos();
assertThat(trackGroupInfos).isEmpty();
}
@Test
public void tracksInfo_emptyStaticInstance_isEmpty() {
TracksInfo tracksInfo = TracksInfo.EMPTY;
assertThat(tracksInfo.getTrackGroupInfos()).isEmpty();
assertThat(tracksInfo).isEqualTo(new TracksInfo(ImmutableList.of()));
}
@Test
public void tracksInfoGetters_ofComplexTracksInfo_returnExpectedValues() {
TrackGroupInfo trackGroupInfo0 =
new TrackGroupInfo(
new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()),
/* adaptiveSupported= */ false,
new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
/* tracksSelected= */ new boolean[] {false});
TrackGroupInfo trackGroupInfo1 =
new TrackGroupInfo(
new TrackGroup(
new Format.Builder().setSampleMimeType(VIDEO_H264).build(),
new Format.Builder().setSampleMimeType(VIDEO_H264).build()),
/* adaptiveSupported= */ true,
new int[] {C.FORMAT_UNSUPPORTED_DRM, C.FORMAT_HANDLED},
/* tracksSelected= */ new boolean[] {false, true});
TracksInfo tracksInfo = new TracksInfo(ImmutableList.of(trackGroupInfo0, trackGroupInfo1));
assertThat(tracksInfo.containsType(C.TRACK_TYPE_AUDIO)).isTrue();
assertThat(tracksInfo.containsType(C.TRACK_TYPE_VIDEO)).isTrue();
assertThat(tracksInfo.containsType(C.TRACK_TYPE_TEXT)).isFalse();
assertThat(tracksInfo.isTypeSupported(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracksInfo.isTypeSupported(C.TRACK_TYPE_VIDEO)).isTrue();
assertThat(tracksInfo.isTypeSupported(C.TRACK_TYPE_TEXT)).isFalse();
assertThat(tracksInfo.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true))
.isTrue();
assertThat(tracksInfo.isTypeSupported(C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true))
.isTrue();
assertThat(tracksInfo.isTypeSupported(C.TRACK_TYPE_TEXT, /* allowExceedsCapabilities= */ true))
.isFalse();
assertThat(tracksInfo.isTypeSelected(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracksInfo.isTypeSelected(C.TRACK_TYPE_VIDEO)).isTrue();
ImmutableList<TrackGroupInfo> trackGroupInfos = tracksInfo.getTrackGroupInfos();
assertThat(trackGroupInfos).hasSize(2);
assertThat(trackGroupInfos.get(0)).isSameInstanceAs(trackGroupInfo0);
assertThat(trackGroupInfos.get(1)).isSameInstanceAs(trackGroupInfo1);
assertThat(trackGroupInfos.get(0).isTrackSupported(0)).isFalse();
assertThat(trackGroupInfos.get(1).isTrackSupported(0)).isFalse();
assertThat(trackGroupInfos.get(1).isTrackSupported(1)).isTrue();
assertThat(trackGroupInfos.get(0).getTrackSupport(0)).isEqualTo(C.FORMAT_EXCEEDS_CAPABILITIES);
assertThat(trackGroupInfos.get(1).getTrackSupport(0)).isEqualTo(C.FORMAT_UNSUPPORTED_DRM);
assertThat(trackGroupInfos.get(1).getTrackSupport(1)).isEqualTo(C.FORMAT_HANDLED);
assertThat(trackGroupInfos.get(0).isTrackSelected(0)).isFalse();
assertThat(trackGroupInfos.get(1).isTrackSelected(0)).isFalse();
assertThat(trackGroupInfos.get(1).isTrackSelected(1)).isTrue();
assertThat(trackGroupInfos.get(0).getTrackType()).isEqualTo(C.TRACK_TYPE_AUDIO);
assertThat(trackGroupInfos.get(1).getTrackType()).isEqualTo(C.TRACK_TYPE_VIDEO);
}
/**
* Tests that {@link TrackGroupInfo#isAdaptiveSupported} returns false if the group only contains
* a single track, even if true is passed to the constructor.
*/
@Test
public void trackGroupInfo_withSingleTrack_isNotAdaptive() {
TrackGroupInfo trackGroupInfo0 =
new TrackGroupInfo(
new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()),
/* adaptiveSupported= */ true,
new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
/* tracksSelected= */ new boolean[] {false});
assertThat(trackGroupInfo0.isAdaptiveSupported()).isFalse();
}
}

View File

@ -0,0 +1,143 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common;
import static androidx.media3.common.MimeTypes.AUDIO_AAC;
import static androidx.media3.common.MimeTypes.VIDEO_H264;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link Tracks}. */
@RunWith(AndroidJUnit4.class)
public class TracksTest {
@Test
public void roundTripViaBundle_ofEmptyTracks_yieldsEqualInstance() {
Tracks before = Tracks.EMPTY;
Tracks after = Tracks.CREATOR.fromBundle(before.toBundle());
assertThat(after).isEqualTo(before);
}
@Test
public void roundTripViaBundle_ofTracks_yieldsEqualInstance() {
Tracks before =
new Tracks(
ImmutableList.of(
new Tracks.Group(
new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()),
/* adaptiveSupported= */ false,
new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
/* trackSelected= */ new boolean[] {true}),
new Tracks.Group(
new TrackGroup(
new Format.Builder().setSampleMimeType(VIDEO_H264).build(),
new Format.Builder().setSampleMimeType(VIDEO_H264).build()),
/* adaptiveSupported= */ true,
new int[] {C.FORMAT_UNSUPPORTED_DRM, C.FORMAT_UNSUPPORTED_TYPE},
/* trackSelected= */ new boolean[] {false, true})));
Tracks after = Tracks.CREATOR.fromBundle(before.toBundle());
assertThat(after).isEqualTo(before);
}
@Test
public void getters_withoutTrack_returnExpectedValues() {
Tracks tracks = new Tracks(ImmutableList.of());
assertThat(tracks.containsType(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true))
.isFalse();
assertThat(tracks.isTypeSelected(C.TRACK_TYPE_AUDIO)).isFalse();
ImmutableList<Tracks.Group> trackGroups = tracks.getGroups();
assertThat(trackGroups).isEmpty();
}
@Test
public void emptyStaticInstance_isEmpty() {
Tracks tracks = Tracks.EMPTY;
assertThat(tracks.getGroups()).isEmpty();
assertThat(tracks).isEqualTo(new Tracks(ImmutableList.of()));
}
@Test
public void getters_ofComplexTracks_returnExpectedValues() {
Tracks.Group trackGroup0 =
new Tracks.Group(
new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()),
/* adaptiveSupported= */ false,
new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
/* trackSelected= */ new boolean[] {false});
Tracks.Group trackGroup1 =
new Tracks.Group(
new TrackGroup(
new Format.Builder().setSampleMimeType(VIDEO_H264).build(),
new Format.Builder().setSampleMimeType(VIDEO_H264).build()),
/* adaptiveSupported= */ true,
new int[] {C.FORMAT_UNSUPPORTED_DRM, C.FORMAT_HANDLED},
/* trackSelected= */ new boolean[] {false, true});
Tracks tracks = new Tracks(ImmutableList.of(trackGroup0, trackGroup1));
assertThat(tracks.containsType(C.TRACK_TYPE_AUDIO)).isTrue();
assertThat(tracks.containsType(C.TRACK_TYPE_VIDEO)).isTrue();
assertThat(tracks.containsType(C.TRACK_TYPE_TEXT)).isFalse();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_VIDEO)).isTrue();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_TEXT)).isFalse();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true))
.isTrue();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true))
.isTrue();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_TEXT, /* allowExceedsCapabilities= */ true))
.isFalse();
assertThat(tracks.isTypeSelected(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracks.isTypeSelected(C.TRACK_TYPE_VIDEO)).isTrue();
ImmutableList<Tracks.Group> trackGroups = tracks.getGroups();
assertThat(trackGroups).hasSize(2);
assertThat(trackGroups.get(0)).isSameInstanceAs(trackGroup0);
assertThat(trackGroups.get(1)).isSameInstanceAs(trackGroup1);
assertThat(trackGroups.get(0).isTrackSupported(0)).isFalse();
assertThat(trackGroups.get(1).isTrackSupported(0)).isFalse();
assertThat(trackGroups.get(1).isTrackSupported(1)).isTrue();
assertThat(trackGroups.get(0).getTrackSupport(0)).isEqualTo(C.FORMAT_EXCEEDS_CAPABILITIES);
assertThat(trackGroups.get(1).getTrackSupport(0)).isEqualTo(C.FORMAT_UNSUPPORTED_DRM);
assertThat(trackGroups.get(1).getTrackSupport(1)).isEqualTo(C.FORMAT_HANDLED);
assertThat(trackGroups.get(0).isTrackSelected(0)).isFalse();
assertThat(trackGroups.get(1).isTrackSelected(0)).isFalse();
assertThat(trackGroups.get(1).isTrackSelected(1)).isTrue();
assertThat(trackGroups.get(0).getType()).isEqualTo(C.TRACK_TYPE_AUDIO);
assertThat(trackGroups.get(1).getType()).isEqualTo(C.TRACK_TYPE_VIDEO);
}
/**
* Tests that {@link Tracks.Group#isAdaptiveSupported} returns false if the group only contains a
* single track, even if true is passed to the constructor.
*/
@Test
public void groupWithSingleTrack_isNotAdaptive() {
Tracks.Group trackGroup =
new Tracks.Group(
new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()),
/* adaptiveSupported= */ true,
new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
/* trackSelected= */ new boolean[] {false});
assertThat(trackGroup.isAdaptiveSupported()).isFalse();
}
}

View File

@ -21,6 +21,7 @@ import static androidx.media3.common.util.Util.escapeFileName;
import static androidx.media3.common.util.Util.getCodecsOfType;
import static androidx.media3.common.util.Util.getStringForTime;
import static androidx.media3.common.util.Util.gzip;
import static androidx.media3.common.util.Util.maxValue;
import static androidx.media3.common.util.Util.minValue;
import static androidx.media3.common.util.Util.parseXsDateTime;
import static androidx.media3.common.util.Util.parseXsDuration;
@ -747,6 +748,21 @@ public class UtilTest {
assertThrows(NoSuchElementException.class, () -> minValue(new SparseLongArray()));
}
@Test
public void sparseLongArrayMaxValue_returnsMaxValue() {
SparseLongArray sparseLongArray = new SparseLongArray();
sparseLongArray.put(0, 2);
sparseLongArray.put(25, 10);
sparseLongArray.put(42, 1);
assertThat(maxValue(sparseLongArray)).isEqualTo(10);
}
@Test
public void sparseLongArrayMaxValue_emptyArray_throws() {
assertThrows(NoSuchElementException.class, () -> maxValue(new SparseLongArray()));
}
@Test
public void parseXsDuration_returnsParsedDurationInMillis() {
assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L);

View File

@ -21,7 +21,6 @@ import androidx.media3.common.util.UnstableApi;
import java.io.IOException;
/** Used to specify reason of a DataSource error. */
@UnstableApi
public class DataSourceException extends IOException {
/**
@ -29,6 +28,7 @@ public class DataSourceException extends IOException {
* {@link #reason} is {@link PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE} in its
* cause stack.
*/
@UnstableApi
public static boolean isCausedByPositionOutOfRange(IOException e) {
@Nullable Throwable cause = e;
while (cause != null) {
@ -49,7 +49,7 @@ public class DataSourceException extends IOException {
*
* @deprecated Use {@link PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE}.
*/
@Deprecated
@UnstableApi @Deprecated
public static final int POSITION_OUT_OF_RANGE =
PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE;
@ -65,6 +65,7 @@ public class DataSourceException extends IOException {
* @param reason Reason of the error, should be one of the {@code ERROR_CODE_IO_*} in {@link
* PlaybackException.ErrorCode}.
*/
@UnstableApi
public DataSourceException(@PlaybackException.ErrorCode int reason) {
this.reason = reason;
}
@ -76,6 +77,7 @@ public class DataSourceException extends IOException {
* @param reason Reason of the error, should be one of the {@code ERROR_CODE_IO_*} in {@link
* PlaybackException.ErrorCode}.
*/
@UnstableApi
public DataSourceException(@Nullable Throwable cause, @PlaybackException.ErrorCode int reason) {
super(cause);
this.reason = reason;
@ -88,6 +90,7 @@ public class DataSourceException extends IOException {
* @param reason Reason of the error, should be one of the {@code ERROR_CODE_IO_*} in {@link
* PlaybackException.ErrorCode}.
*/
@UnstableApi
public DataSourceException(@Nullable String message, @PlaybackException.ErrorCode int reason) {
super(message);
this.reason = reason;
@ -101,6 +104,7 @@ public class DataSourceException extends IOException {
* @param reason Reason of the error, should be one of the {@code ERROR_CODE_IO_*} in {@link
* PlaybackException.ErrorCode}.
*/
@UnstableApi
public DataSourceException(
@Nullable String message,
@Nullable Throwable cause,

View File

@ -38,12 +38,12 @@ import java.util.List;
import java.util.Map;
/** An HTTP {@link DataSource}. */
@UnstableApi
public interface HttpDataSource extends DataSource {
/** A factory for {@link HttpDataSource} instances. */
interface Factory extends DataSource.Factory {
@UnstableApi
@Override
HttpDataSource createDataSource();
@ -59,6 +59,7 @@ public interface HttpDataSource extends DataSource {
* @param defaultRequestProperties The default request properties.
* @return This factory.
*/
@UnstableApi
Factory setDefaultRequestProperties(Map<String, String> defaultRequestProperties);
}
@ -67,6 +68,7 @@ public interface HttpDataSource extends DataSource {
* a thread safe way to avoid the potential of creating snapshots of an inconsistent or unintended
* state.
*/
@UnstableApi
final class RequestProperties {
private final Map<String, String> requestProperties;
@ -141,6 +143,7 @@ public interface HttpDataSource extends DataSource {
}
/** Base implementation of {@link Factory} that sets default request properties. */
@UnstableApi
abstract class BaseFactory implements Factory {
private final RequestProperties defaultRequestProperties;
@ -209,6 +212,7 @@ public interface HttpDataSource extends DataSource {
* Returns a {@code HttpDataSourceException} whose error code is assigned according to the cause
* and type.
*/
@UnstableApi
public static HttpDataSourceException createForIOException(
IOException cause, DataSpec dataSpec, @Type int type) {
@PlaybackException.ErrorCode int errorCode;
@ -232,7 +236,7 @@ public interface HttpDataSource extends DataSource {
}
/** The {@link DataSpec} associated with the current connection. */
public final DataSpec dataSpec;
@UnstableApi public final DataSpec dataSpec;
public final @Type int type;
@ -240,6 +244,7 @@ public interface HttpDataSource extends DataSource {
* @deprecated Use {@link #HttpDataSourceException(DataSpec, int, int)
* HttpDataSourceException(DataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, int)}.
*/
@UnstableApi
@Deprecated
public HttpDataSourceException(DataSpec dataSpec, @Type int type) {
this(dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, type);
@ -253,6 +258,7 @@ public interface HttpDataSource extends DataSource {
* PlaybackException.ErrorCode}.
* @param type See {@link Type}.
*/
@UnstableApi
public HttpDataSourceException(
DataSpec dataSpec, @PlaybackException.ErrorCode int errorCode, @Type int type) {
super(assignErrorCode(errorCode, type));
@ -265,6 +271,7 @@ public interface HttpDataSource extends DataSource {
* HttpDataSourceException(String, DataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
* int)}.
*/
@UnstableApi
@Deprecated
public HttpDataSourceException(String message, DataSpec dataSpec, @Type int type) {
this(message, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, type);
@ -279,6 +286,7 @@ public interface HttpDataSource extends DataSource {
* PlaybackException.ErrorCode}.
* @param type See {@link Type}.
*/
@UnstableApi
public HttpDataSourceException(
String message,
DataSpec dataSpec,
@ -294,6 +302,7 @@ public interface HttpDataSource extends DataSource {
* HttpDataSourceException(IOException, DataSpec,
* PlaybackException.ERROR_CODE_IO_UNSPECIFIED, int)}.
*/
@UnstableApi
@Deprecated
public HttpDataSourceException(IOException cause, DataSpec dataSpec, @Type int type) {
this(cause, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, type);
@ -308,6 +317,7 @@ public interface HttpDataSource extends DataSource {
* PlaybackException.ErrorCode}.
* @param type See {@link Type}.
*/
@UnstableApi
public HttpDataSourceException(
IOException cause,
DataSpec dataSpec,
@ -323,6 +333,7 @@ public interface HttpDataSource extends DataSource {
* HttpDataSourceException(String, IOException, DataSpec,
* PlaybackException.ERROR_CODE_IO_UNSPECIFIED, int)}.
*/
@UnstableApi
@Deprecated
public HttpDataSourceException(
String message, IOException cause, DataSpec dataSpec, @Type int type) {
@ -339,6 +350,7 @@ public interface HttpDataSource extends DataSource {
* PlaybackException.ErrorCode}.
* @param type See {@link Type}.
*/
@UnstableApi
public HttpDataSourceException(
String message,
@Nullable IOException cause,
@ -366,6 +378,7 @@ public interface HttpDataSource extends DataSource {
*/
final class CleartextNotPermittedException extends HttpDataSourceException {
@UnstableApi
public CleartextNotPermittedException(IOException cause, DataSpec dataSpec) {
super(
"Cleartext HTTP traffic not permitted. See"
@ -382,6 +395,7 @@ public interface HttpDataSource extends DataSource {
public final String contentType;
@UnstableApi
public InvalidContentTypeException(String contentType, DataSpec dataSpec) {
super(
"Invalid content type: " + contentType,
@ -413,6 +427,7 @@ public interface HttpDataSource extends DataSource {
* @deprecated Use {@link #InvalidResponseCodeException(int, String, IOException, Map, DataSpec,
* byte[])}.
*/
@UnstableApi
@Deprecated
public InvalidResponseCodeException(
int responseCode, Map<String, List<String>> headerFields, DataSpec dataSpec) {
@ -429,6 +444,7 @@ public interface HttpDataSource extends DataSource {
* @deprecated Use {@link #InvalidResponseCodeException(int, String, IOException, Map, DataSpec,
* byte[])}.
*/
@UnstableApi
@Deprecated
public InvalidResponseCodeException(
int responseCode,
@ -444,6 +460,7 @@ public interface HttpDataSource extends DataSource {
/* responseBody= */ Util.EMPTY_BYTE_ARRAY);
}
@UnstableApi
public InvalidResponseCodeException(
int responseCode,
@Nullable String responseMessage,
@ -471,12 +488,15 @@ public interface HttpDataSource extends DataSource {
* (in order of decreasing priority) the {@code dataSpec}, {@link #setRequestProperty} and the
* default parameters set in the {@link Factory}.
*/
@UnstableApi
@Override
long open(DataSpec dataSpec) throws HttpDataSourceException;
@UnstableApi
@Override
void close() throws HttpDataSourceException;
@UnstableApi
@Override
int read(byte[] buffer, int offset, int length) throws HttpDataSourceException;
@ -491,6 +511,7 @@ public interface HttpDataSource extends DataSource {
* @param name The name of the header field.
* @param value The value of the field.
*/
@UnstableApi
void setRequestProperty(String name, String value);
/**
@ -499,17 +520,21 @@ public interface HttpDataSource extends DataSource {
*
* @param name The name of the header field.
*/
@UnstableApi
void clearRequestProperty(String name);
/** Clears all request headers that were set by {@link #setRequestProperty(String, String)}. */
@UnstableApi
void clearAllRequestProperties();
/**
* When the source is open, returns the HTTP response status code associated with the last {@link
* #open} call. Otherwise, returns a negative value.
*/
@UnstableApi
int getResponseCode();
@UnstableApi
@Override
Map<String, List<String>> getResponseHeaders();
}

View File

@ -22,14 +22,14 @@ import java.io.IOException;
/** A DataSource which provides no data. {@link #open(DataSpec)} throws {@link IOException}. */
@UnstableApi
public final class DummyDataSource implements DataSource {
public final class PlaceholderDataSource implements DataSource {
public static final DummyDataSource INSTANCE = new DummyDataSource();
public static final PlaceholderDataSource INSTANCE = new PlaceholderDataSource();
/** A factory that produces {@link DummyDataSource}. */
public static final Factory FACTORY = DummyDataSource::new;
/** A factory that produces {@link PlaceholderDataSource}. */
public static final Factory FACTORY = PlaceholderDataSource::new;
private DummyDataSource() {}
private PlaceholderDataSource() {}
@Override
public void addTransferListener(TransferListener transferListener) {
@ -38,7 +38,7 @@ public final class DummyDataSource implements DataSource {
@Override
public long open(DataSpec dataSpec) throws IOException {
throw new IOException("DummyDataSource cannot be opened");
throw new IOException("PlaceholderDataSource cannot be opened");
}
@Override

View File

@ -36,8 +36,8 @@ import androidx.media3.datasource.DataSink;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSourceException;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.DummyDataSource;
import androidx.media3.datasource.FileDataSource;
import androidx.media3.datasource.PlaceholderDataSource;
import androidx.media3.datasource.PriorityDataSource;
import androidx.media3.datasource.TeeDataSource;
import androidx.media3.datasource.TransferListener;
@ -541,7 +541,7 @@ public final class CacheDataSource implements DataSource {
? new TeeDataSource(upstreamDataSource, cacheWriteDataSink)
: null;
} else {
this.upstreamDataSource = DummyDataSource.INSTANCE;
this.upstreamDataSource = PlaceholderDataSource.INSTANCE;
this.cacheWriteDataSource = null;
}
this.eventListener = eventListener;

View File

@ -34,6 +34,11 @@ public abstract class Buffer {
return getFlag(C.BUFFER_FLAG_DECODE_ONLY);
}
/** Returns whether the {@link C#BUFFER_FLAG_FIRST_SAMPLE} flag is set. */
public final boolean isFirstSample() {
return getFlag(C.BUFFER_FLAG_FIRST_SAMPLE);
}
/** Returns whether the {@link C#BUFFER_FLAG_END_OF_STREAM} flag is set. */
public final boolean isEndOfStream() {
return getFlag(C.BUFFER_FLAG_END_OF_STREAM);

View File

@ -235,6 +235,9 @@ public abstract class SimpleDecoder<
if (inputBuffer.isDecodeOnly()) {
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
}
if (inputBuffer.isFirstSample()) {
outputBuffer.addFlag(C.BUFFER_FLAG_FIRST_SAMPLE);
}
@Nullable E exception;
try {
exception = decode(inputBuffer, outputBuffer, resetDecoder);

View File

@ -38,7 +38,7 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.PriorityTaskManager;
import androidx.media3.common.Timeline;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.Clock;
@ -1222,8 +1222,8 @@ public interface ExoPlayer extends Player {
/**
* Returns the available track groups.
*
* @see Listener#onTracksInfoChanged(TracksInfo)
* @deprecated Use {@link #getCurrentTracksInfo()}.
* @see Listener#onTracksChanged(Tracks)
* @deprecated Use {@link #getCurrentTracks()}.
*/
@UnstableApi
@Deprecated
@ -1233,8 +1233,8 @@ public interface ExoPlayer extends Player {
* Returns the current track selections for each renderer, which may include {@code null} elements
* if some renderers do not have any selected tracks.
*
* @see Listener#onTracksInfoChanged(TracksInfo)
* @deprecated Use {@link #getCurrentTracksInfo()}.
* @see Listener#onTracksChanged(Tracks)
* @deprecated Use {@link #getCurrentTracks()}.
*/
@UnstableApi
@Deprecated

View File

@ -70,7 +70,7 @@ import androidx.media3.common.PriorityTaskManager;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.Assertions;
@ -277,7 +277,7 @@ import java.util.concurrent.TimeoutException;
new TrackSelectorResult(
new RendererConfiguration[renderers.length],
new ExoTrackSelection[renderers.length],
TracksInfo.EMPTY,
Tracks.EMPTY,
/* info= */ null);
period = new Timeline.Period();
permanentAvailableCommands =
@ -294,7 +294,7 @@ import java.util.concurrent.TimeoutException;
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_TRACK_INFOS,
COMMAND_GET_TRACKS,
COMMAND_GET_AUDIO_ATTRIBUTES,
COMMAND_GET_VOLUME,
COMMAND_GET_DEVICE_VOLUME,
@ -1147,9 +1147,9 @@ import java.util.concurrent.TimeoutException;
}
@Override
public TracksInfo getCurrentTracksInfo() {
public Tracks getCurrentTracks() {
verifyApplicationThread();
return playbackInfo.trackSelectorResult.tracksInfo;
return playbackInfo.trackSelectorResult.tracks;
}
@Override
@ -1898,7 +1898,7 @@ import java.util.concurrent.TimeoutException;
trackSelector.onSelectionActivated(newPlaybackInfo.trackSelectorResult.info);
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED,
listener -> listener.onTracksInfoChanged(newPlaybackInfo.trackSelectorResult.tracksInfo));
listener -> listener.onTracksChanged(newPlaybackInfo.trackSelectorResult.tracks));
}
if (metadataChanged) {
final MediaMetadata finalMediaMetadata = mediaMetadata;

View File

@ -36,7 +36,7 @@ import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.PriorityTaskManager;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.Clock;
@ -1059,9 +1059,9 @@ public class SimpleExoPlayer extends BasePlayer
}
@Override
public TracksInfo getCurrentTracksInfo() {
public Tracks getCurrentTracks() {
blockUntilConstructorFinished();
return player.getCurrentTracksInfo();
return player.getCurrentTracks();
}
@Override

View File

@ -46,7 +46,7 @@ import androidx.media3.common.Player.PlaybackSuppressionReason;
import androidx.media3.common.Player.TimelineChangeReason;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.UnstableApi;
@ -238,7 +238,7 @@ public interface AnalyticsListener {
* {@link Player#getCurrentMediaItem()} changed or the player started repeating the current item.
*/
int EVENT_MEDIA_ITEM_TRANSITION = Player.EVENT_MEDIA_ITEM_TRANSITION;
/** {@link Player#getCurrentTracksInfo()} changed. */
/** {@link Player#getCurrentTracks()} changed. */
int EVENT_TRACKS_CHANGED = Player.EVENT_TRACKS_CHANGED;
/** {@link Player#isLoading()} ()} changed. */
int EVENT_IS_LOADING_CHANGED = Player.EVENT_IS_LOADING_CHANGED;
@ -706,12 +706,12 @@ public interface AnalyticsListener {
default void onPlayerErrorChanged(EventTime eventTime, @Nullable PlaybackException error) {}
/**
* Called when the available or selected tracks change.
* Called when the tracks change.
*
* @param eventTime The event time.
* @param tracksInfo The available tracks information. Never null, but may be of length zero.
* @param tracks The tracks. Never null, but may be of length zero.
*/
default void onTracksInfoChanged(EventTime eventTime, TracksInfo tracksInfo) {}
default void onTracksChanged(EventTime eventTime, Tracks tracks) {}
/**
* Called when track selection parameters change.

View File

@ -39,7 +39,7 @@ import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Period;
import androidx.media3.common.Timeline.Window;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.Clock;
@ -483,12 +483,12 @@ public class DefaultAnalyticsCollector implements AnalyticsCollector {
}
@Override
public void onTracksInfoChanged(TracksInfo tracksInfo) {
public void onTracksChanged(Tracks tracks) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_TRACKS_CHANGED,
listener -> listener.onTracksInfoChanged(eventTime, tracksInfo));
listener -> listener.onTracksChanged(eventTime, tracks));
}
@SuppressWarnings("deprecation") // Implementing deprecated method.

View File

@ -49,8 +49,7 @@ import androidx.media3.common.ParserException;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.TracksInfo.TrackGroupInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.util.NetworkTypeObserver;
import androidx.media3.common.util.UnstableApi;
@ -340,8 +339,7 @@ public final class MediaMetricsListener
}
}
if (events.contains(EVENT_TRACKS_CHANGED) && metricsBuilder != null) {
@Nullable
DrmInitData drmInitData = getDrmInitData(player.getCurrentTracksInfo().getTrackGroupInfos());
@Nullable DrmInitData drmInitData = getDrmInitData(player.getCurrentTracks().getGroups());
if (drmInitData != null) {
castNonNull(metricsBuilder).setDrmType(getDrmType(drmInitData));
}
@ -372,10 +370,10 @@ public final class MediaMetricsListener
private void maybeReportTrackChanges(Player player, Events events, long realtimeMs) {
if (events.contains(EVENT_TRACKS_CHANGED)) {
TracksInfo tracksInfo = player.getCurrentTracksInfo();
boolean isVideoSelected = tracksInfo.isTypeSelected(C.TRACK_TYPE_VIDEO);
boolean isAudioSelected = tracksInfo.isTypeSelected(C.TRACK_TYPE_AUDIO);
boolean isTextSelected = tracksInfo.isTypeSelected(C.TRACK_TYPE_TEXT);
Tracks tracks = player.getCurrentTracks();
boolean isVideoSelected = tracks.isTypeSelected(C.TRACK_TYPE_VIDEO);
boolean isAudioSelected = tracks.isTypeSelected(C.TRACK_TYPE_AUDIO);
boolean isTextSelected = tracks.isTypeSelected(C.TRACK_TYPE_TEXT);
if (isVideoSelected || isAudioSelected || isTextSelected) {
// Ignore updates with insufficient information where no tracks are selected.
if (!isVideoSelected) {
@ -822,11 +820,11 @@ public final class MediaMetricsListener
}
@Nullable
private static DrmInitData getDrmInitData(ImmutableList<TrackGroupInfo> trackGroupInfos) {
for (TrackGroupInfo trackGroupInfo : trackGroupInfos) {
for (int trackIndex = 0; trackIndex < trackGroupInfo.length; trackIndex++) {
if (trackGroupInfo.isTrackSelected(trackIndex)) {
@Nullable DrmInitData drmInitData = trackGroupInfo.getTrackFormat(trackIndex).drmInitData;
private static DrmInitData getDrmInitData(ImmutableList<Tracks.Group> trackGroups) {
for (Tracks.Group trackGroup : trackGroups) {
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
if (trackGroup.isTrackSelected(trackIndex)) {
@Nullable DrmInitData drmInitData = trackGroup.getTrackFormat(trackIndex).drmInitData;
if (drmInitData != null) {
return drmInitData;
}

View File

@ -27,7 +27,7 @@ import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Period;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
@ -524,11 +524,11 @@ public final class PlaybackStatsListener
hasFatalError = false;
}
if (isForeground && !isInterruptedByAd) {
TracksInfo currentTracksInfo = player.getCurrentTracksInfo();
if (!currentTracksInfo.isTypeSelected(C.TRACK_TYPE_VIDEO)) {
Tracks currentTracks = player.getCurrentTracks();
if (!currentTracks.isTypeSelected(C.TRACK_TYPE_VIDEO)) {
maybeUpdateVideoFormat(eventTime, /* newFormat= */ null);
}
if (!currentTracksInfo.isTypeSelected(C.TRACK_TYPE_AUDIO)) {
if (!currentTracks.isTypeSelected(C.TRACK_TYPE_AUDIO)) {
maybeUpdateAudioFormat(eventTime, /* newFormat= */ null);
}
}

View File

@ -130,6 +130,7 @@ public abstract class DecoderAudioRenderer<
private int encoderPadding;
private boolean experimentalKeepAudioTrackOnSeek;
private boolean firstStreamSampleRead;
@Nullable private T decoder;
@ -389,6 +390,9 @@ public abstract class DecoderAudioRenderer<
decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
audioSink.handleDiscontinuity();
}
if (outputBuffer.isFirstSample()) {
audioSink.handleDiscontinuity();
}
}
if (outputBuffer.isEndOfStream()) {
@ -470,6 +474,10 @@ public abstract class DecoderAudioRenderer<
inputBuffer = null;
return false;
}
if (!firstStreamSampleRead) {
firstStreamSampleRead = true;
inputBuffer.addFlag(C.BUFFER_FLAG_FIRST_SAMPLE);
}
inputBuffer.flip();
inputBuffer.format = inputFormat;
onQueueInputBuffer(inputBuffer);
@ -587,6 +595,13 @@ public abstract class DecoderAudioRenderer<
}
}
@Override
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs)
throws ExoPlaybackException {
super.onStreamChanged(formats, startPositionUs, offsetUs);
firstStreamSampleRead = false;
}
@Override
public void handleMessage(@MessageType int messageType, @Nullable Object message)
throws ExoPlaybackException {

View File

@ -887,7 +887,6 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public void handleDiscontinuity() {
// Force resynchronization after a skipped buffer.
startMediaTimeUsNeedsSync = true;
}
@ -1018,7 +1017,7 @@ public final class DefaultAudioSink implements AudioSink {
if (configuration.outputMode == OUTPUT_MODE_PCM) {
submittedPcmBytes += buffer.remaining();
} else {
submittedEncodedFrames += framesPerEncodedSample * encodedAccessUnitCount;
submittedEncodedFrames += (long) framesPerEncodedSample * encodedAccessUnitCount;
}
inputBuffer = buffer;
@ -1213,7 +1212,7 @@ public final class DefaultAudioSink implements AudioSink {
// When playing non-PCM, the inputBuffer is never processed, thus the last inputBuffer
// must be the current input buffer.
Assertions.checkState(buffer == inputBuffer);
writtenEncodedFrames += framesPerEncodedSample * inputBufferAccessUnitCount;
writtenEncodedFrames += (long) framesPerEncodedSample * inputBufferAccessUnitCount;
}
outputBuffer = null;
}

View File

@ -33,7 +33,7 @@ import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
@ -546,16 +546,16 @@ public final class DownloadHelper {
}
/**
* Returns {@link TracksInfo} for the given period. Must not be called until after preparation
* Returns {@link Tracks} for the given period. Must not be called until after preparation
* completes.
*
* @param periodIndex The period index.
* @return The {@link TracksInfo} for the period. May be {@link TracksInfo#EMPTY} for single
* stream content.
* @return The {@link Tracks} for the period. May be {@link Tracks#EMPTY} for single stream
* content.
*/
public TracksInfo getTracksInfo(int periodIndex) {
public Tracks getTracks(int periodIndex) {
assertPreparedWithMedia();
return TrackSelectionUtil.buildTracksInfo(
return TrackSelectionUtil.buildTracks(
mappedTrackInfos[periodIndex], immutableTrackSelectionsByPeriodAndRenderer[periodIndex]);
}
@ -622,9 +622,13 @@ public final class DownloadHelper {
*/
public void replaceTrackSelections(
int periodIndex, TrackSelectionParameters trackSelectionParameters) {
assertPreparedWithMedia();
clearTrackSelections(periodIndex);
addTrackSelectionInternal(periodIndex, trackSelectionParameters);
try {
assertPreparedWithMedia();
clearTrackSelections(periodIndex);
addTrackSelectionInternal(periodIndex, trackSelectionParameters);
} catch (ExoPlaybackException e) {
throw new IllegalStateException(e);
}
}
/**
@ -637,8 +641,12 @@ public final class DownloadHelper {
*/
public void addTrackSelection(
int periodIndex, TrackSelectionParameters trackSelectionParameters) {
assertPreparedWithMedia();
addTrackSelectionInternal(periodIndex, trackSelectionParameters);
try {
assertPreparedWithMedia();
addTrackSelectionInternal(periodIndex, trackSelectionParameters);
} catch (ExoPlaybackException e) {
throw new IllegalStateException(e);
}
}
/**
@ -650,27 +658,31 @@ public final class DownloadHelper {
* selection, as IETF BCP 47 conformant tags.
*/
public void addAudioLanguagesToSelection(String... languages) {
assertPreparedWithMedia();
try {
assertPreparedWithMedia();
TrackSelectionParameters.Builder parametersBuilder =
DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon();
// Prefer highest supported bitrate for downloads.
parametersBuilder.setForceHighestSupportedBitrate(true);
// Disable all non-audio track types supported by the renderers.
for (RendererCapabilities capabilities : rendererCapabilities) {
@C.TrackType int trackType = capabilities.getTrackType();
parametersBuilder.setTrackTypeDisabled(
trackType, /* disabled= */ trackType != C.TRACK_TYPE_AUDIO);
}
// Add a track selection to each period for each of the languages.
int periodCount = getPeriodCount();
for (String language : languages) {
TrackSelectionParameters parameters =
parametersBuilder.setPreferredAudioLanguage(language).build();
for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
addTrackSelectionInternal(periodIndex, parameters);
TrackSelectionParameters.Builder parametersBuilder =
DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon();
// Prefer highest supported bitrate for downloads.
parametersBuilder.setForceHighestSupportedBitrate(true);
// Disable all non-audio track types supported by the renderers.
for (RendererCapabilities capabilities : rendererCapabilities) {
@C.TrackType int trackType = capabilities.getTrackType();
parametersBuilder.setTrackTypeDisabled(
trackType, /* disabled= */ trackType != C.TRACK_TYPE_AUDIO);
}
// Add a track selection to each period for each of the languages.
int periodCount = getPeriodCount();
for (String language : languages) {
TrackSelectionParameters parameters =
parametersBuilder.setPreferredAudioLanguage(language).build();
for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
addTrackSelectionInternal(periodIndex, parameters);
}
}
} catch (ExoPlaybackException e) {
throw new IllegalStateException(e);
}
}
@ -686,28 +698,32 @@ public final class DownloadHelper {
*/
public void addTextLanguagesToSelection(
boolean selectUndeterminedTextLanguage, String... languages) {
assertPreparedWithMedia();
try {
assertPreparedWithMedia();
TrackSelectionParameters.Builder parametersBuilder =
DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon();
parametersBuilder.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage);
// Prefer highest supported bitrate for downloads.
parametersBuilder.setForceHighestSupportedBitrate(true);
// Disable all non-text track types supported by the renderers.
for (RendererCapabilities capabilities : rendererCapabilities) {
@C.TrackType int trackType = capabilities.getTrackType();
parametersBuilder.setTrackTypeDisabled(
trackType, /* disabled= */ trackType != C.TRACK_TYPE_TEXT);
}
// Add a track selection to each period for each of the languages.
int periodCount = getPeriodCount();
for (String language : languages) {
TrackSelectionParameters parameters =
parametersBuilder.setPreferredTextLanguage(language).build();
for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
addTrackSelectionInternal(periodIndex, parameters);
TrackSelectionParameters.Builder parametersBuilder =
DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon();
parametersBuilder.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage);
// Prefer highest supported bitrate for downloads.
parametersBuilder.setForceHighestSupportedBitrate(true);
// Disable all non-text track types supported by the renderers.
for (RendererCapabilities capabilities : rendererCapabilities) {
@C.TrackType int trackType = capabilities.getTrackType();
parametersBuilder.setTrackTypeDisabled(
trackType, /* disabled= */ trackType != C.TRACK_TYPE_TEXT);
}
// Add a track selection to each period for each of the languages.
int periodCount = getPeriodCount();
for (String language : languages) {
TrackSelectionParameters parameters =
parametersBuilder.setPreferredTextLanguage(language).build();
for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) {
addTrackSelectionInternal(periodIndex, parameters);
}
}
} catch (ExoPlaybackException e) {
throw new IllegalStateException(e);
}
}
@ -727,19 +743,24 @@ public final class DownloadHelper {
int rendererIndex,
DefaultTrackSelector.Parameters trackSelectorParameters,
List<SelectionOverride> overrides) {
assertPreparedWithMedia();
DefaultTrackSelector.ParametersBuilder builder = trackSelectorParameters.buildUpon();
for (int i = 0; i < mappedTrackInfos[periodIndex].getRendererCount(); i++) {
builder.setRendererDisabled(/* rendererIndex= */ i, /* disabled= */ i != rendererIndex);
}
if (overrides.isEmpty()) {
addTrackSelectionInternal(periodIndex, builder.build());
} else {
TrackGroupArray trackGroupArray = mappedTrackInfos[periodIndex].getTrackGroups(rendererIndex);
for (int i = 0; i < overrides.size(); i++) {
builder.setSelectionOverride(rendererIndex, trackGroupArray, overrides.get(i));
addTrackSelectionInternal(periodIndex, builder.build());
try {
assertPreparedWithMedia();
DefaultTrackSelector.ParametersBuilder builder = trackSelectorParameters.buildUpon();
for (int i = 0; i < mappedTrackInfos[periodIndex].getRendererCount(); i++) {
builder.setRendererDisabled(/* rendererIndex= */ i, /* disabled= */ i != rendererIndex);
}
if (overrides.isEmpty()) {
addTrackSelectionInternal(periodIndex, builder.build());
} else {
TrackGroupArray trackGroupArray =
mappedTrackInfos[periodIndex].getTrackGroups(rendererIndex);
for (int i = 0; i < overrides.size(); i++) {
builder.setSelectionOverride(rendererIndex, trackGroupArray, overrides.get(i));
addTrackSelectionInternal(periodIndex, builder.build());
}
}
} catch (ExoPlaybackException e) {
throw new IllegalStateException(e);
}
}
@ -797,7 +818,8 @@ public final class DownloadHelper {
"mediaPreparer.timeline"
})
private void addTrackSelectionInternal(
int periodIndex, TrackSelectionParameters trackSelectionParameters) {
int periodIndex, TrackSelectionParameters trackSelectionParameters)
throws ExoPlaybackException {
trackSelector.setParameters(trackSelectionParameters);
runTrackSelection(periodIndex);
// TrackSelectionParameters can contain multiple overrides for each track type. The track
@ -812,7 +834,7 @@ public final class DownloadHelper {
}
@SuppressWarnings("unchecked") // Initialization of array of Lists.
private void onMediaPrepared() {
private void onMediaPrepared() throws ExoPlaybackException {
checkNotNull(mediaPreparer);
checkNotNull(mediaPreparer.mediaPeriods);
checkNotNull(mediaPreparer.timeline);
@ -882,52 +904,47 @@ public final class DownloadHelper {
"mediaPreparer",
"mediaPreparer.timeline"
})
private TrackSelectorResult runTrackSelection(int periodIndex) {
try {
TrackSelectorResult trackSelectorResult =
trackSelector.selectTracks(
rendererCapabilities,
trackGroupArrays[periodIndex],
new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)),
mediaPreparer.timeline);
for (int i = 0; i < trackSelectorResult.length; i++) {
@Nullable ExoTrackSelection newSelection = trackSelectorResult.selections[i];
if (newSelection == null) {
continue;
}
List<ExoTrackSelection> existingSelectionList =
trackSelectionsByPeriodAndRenderer[periodIndex][i];
boolean mergedWithExistingSelection = false;
for (int j = 0; j < existingSelectionList.size(); j++) {
ExoTrackSelection existingSelection = existingSelectionList.get(j);
if (existingSelection.getTrackGroup().equals(newSelection.getTrackGroup())) {
// Merge with existing selection.
scratchSet.clear();
for (int k = 0; k < existingSelection.length(); k++) {
scratchSet.put(existingSelection.getIndexInTrackGroup(k), 0);
}
for (int k = 0; k < newSelection.length(); k++) {
scratchSet.put(newSelection.getIndexInTrackGroup(k), 0);
}
int[] mergedTracks = new int[scratchSet.size()];
for (int k = 0; k < scratchSet.size(); k++) {
mergedTracks[k] = scratchSet.keyAt(k);
}
existingSelectionList.set(
j, new DownloadTrackSelection(existingSelection.getTrackGroup(), mergedTracks));
mergedWithExistingSelection = true;
break;
private TrackSelectorResult runTrackSelection(int periodIndex) throws ExoPlaybackException {
TrackSelectorResult trackSelectorResult =
trackSelector.selectTracks(
rendererCapabilities,
trackGroupArrays[periodIndex],
new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)),
mediaPreparer.timeline);
for (int i = 0; i < trackSelectorResult.length; i++) {
@Nullable ExoTrackSelection newSelection = trackSelectorResult.selections[i];
if (newSelection == null) {
continue;
}
List<ExoTrackSelection> existingSelectionList =
trackSelectionsByPeriodAndRenderer[periodIndex][i];
boolean mergedWithExistingSelection = false;
for (int j = 0; j < existingSelectionList.size(); j++) {
ExoTrackSelection existingSelection = existingSelectionList.get(j);
if (existingSelection.getTrackGroup().equals(newSelection.getTrackGroup())) {
// Merge with existing selection.
scratchSet.clear();
for (int k = 0; k < existingSelection.length(); k++) {
scratchSet.put(existingSelection.getIndexInTrackGroup(k), 0);
}
}
if (!mergedWithExistingSelection) {
existingSelectionList.add(newSelection);
for (int k = 0; k < newSelection.length(); k++) {
scratchSet.put(newSelection.getIndexInTrackGroup(k), 0);
}
int[] mergedTracks = new int[scratchSet.size()];
for (int k = 0; k < scratchSet.size(); k++) {
mergedTracks[k] = scratchSet.keyAt(k);
}
existingSelectionList.set(
j, new DownloadTrackSelection(existingSelection.getTrackGroup(), mergedTracks));
mergedWithExistingSelection = true;
break;
}
}
return trackSelectorResult;
} catch (ExoPlaybackException e) {
// DefaultTrackSelector does not throw exceptions during track selection.
throw new UnsupportedOperationException(e);
if (!mergedWithExistingSelection) {
existingSelectionList.add(newSelection);
}
}
return trackSelectorResult;
}
private static MediaSource createMediaSourceInternal(
@ -1098,7 +1115,14 @@ public final class DownloadHelper {
}
switch (msg.what) {
case DOWNLOAD_HELPER_CALLBACK_MESSAGE_PREPARED:
downloadHelper.onMediaPrepared();
try {
downloadHelper.onMediaPrepared();
} catch (ExoPlaybackException e) {
downloadHelperHandler
.obtainMessage(
DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED, /* obj= */ new IOException(e))
.sendToTarget();
}
return true;
case DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED:
release();

View File

@ -118,13 +118,11 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private boolean allowAudioMixedSampleRateAdaptiveness;
private boolean allowAudioMixedChannelCountAdaptiveness;
private boolean allowAudioMixedDecoderSupportAdaptiveness;
// Text
private @C.SelectionFlags int disabledTextTrackSelectionFlags;
// General
private boolean exceedRendererCapabilitiesIfNecessary;
private boolean tunnelingEnabled;
private boolean allowMultipleAdaptiveSelections;
// Overrides
private final SparseArray<Map<TrackGroupArray, @NullableType SelectionOverride>>
selectionOverrides;
private final SparseBooleanArray rendererDisabledFlags;
@ -160,8 +158,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
*/
private ParametersBuilder(Parameters initialValues) {
super(initialValues);
// Text
disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags;
// Video
exceedVideoConstraintsIfNecessary = initialValues.exceedVideoConstraintsIfNecessary;
allowVideoMixedMimeTypeAdaptiveness = initialValues.allowVideoMixedMimeTypeAdaptiveness;
@ -229,11 +225,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
Parameters.keyForField(
Parameters.FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS),
defaultValue.allowAudioMixedDecoderSupportAdaptiveness));
// Text
setDisabledTextTrackSelectionFlags(
bundle.getInt(
Parameters.keyForField(Parameters.FIELD_DISABLED_TEXT_TRACK_SELECTION_FLAGS),
defaultValue.disabledTextTrackSelectionFlags));
// General
setExceedRendererCapabilitiesIfNecessary(
bundle.getBoolean(
@ -247,10 +238,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
bundle.getBoolean(
Parameters.keyForField(Parameters.FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS),
defaultValue.allowMultipleAdaptiveSelections));
// Overrides
selectionOverrides = new SparseArray<>();
setSelectionOverridesFromBundle(bundle);
rendererDisabledFlags =
makeSparseBooleanArrayFromTrueKeys(
bundle.getIntArray(
@ -559,6 +549,13 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return this;
}
@Override
public ParametersBuilder setIgnoredTextSelectionFlags(
@C.SelectionFlags int ignoredTextSelectionFlags) {
super.setIgnoredTextSelectionFlags(ignoredTextSelectionFlags);
return this;
}
@Override
public ParametersBuilder setSelectUndeterminedTextLanguage(
boolean selectUndeterminedTextLanguage) {
@ -567,16 +564,12 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
/**
* Sets a bitmask of selection flags that are disabled for text track selections.
*
* @param disabledTextTrackSelectionFlags A bitmask of {@link C.SelectionFlags} that are
* disabled for text track selections.
* @return This builder.
* @deprecated Use {@link #setIgnoredTextSelectionFlags}.
*/
@Deprecated
public ParametersBuilder setDisabledTextTrackSelectionFlags(
@C.SelectionFlags int disabledTextTrackSelectionFlags) {
this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags;
return this;
return setIgnoredTextSelectionFlags(disabledTextTrackSelectionFlags);
}
// General
@ -828,8 +821,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
allowAudioMixedSampleRateAdaptiveness = false;
allowAudioMixedChannelCountAdaptiveness = false;
allowAudioMixedDecoderSupportAdaptiveness = false;
// Text
disabledTextTrackSelectionFlags = 0;
// General
exceedRendererCapabilitiesIfNecessary = true;
tunnelingEnabled = false;
@ -917,12 +908,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
*/
@Deprecated public static final Parameters DEFAULT = DEFAULT_WITHOUT_CONTEXT;
/**
* Bitmask of selection flags that are disabled for text track selections. See {@link
* C.SelectionFlags}. The default value is {@code 0} (i.e. no flags).
*/
public final @C.SelectionFlags int disabledTextTrackSelectionFlags;
/** Returns an instance configured with default values. */
public static Parameters getDefaults(Context context) {
return new ParametersBuilder(context).build();
@ -1020,8 +1005,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
allowAudioMixedSampleRateAdaptiveness = builder.allowAudioMixedSampleRateAdaptiveness;
allowAudioMixedChannelCountAdaptiveness = builder.allowAudioMixedChannelCountAdaptiveness;
allowAudioMixedDecoderSupportAdaptiveness = builder.allowAudioMixedDecoderSupportAdaptiveness;
// Text
disabledTextTrackSelectionFlags = builder.disabledTextTrackSelectionFlags;
// General
exceedRendererCapabilitiesIfNecessary = builder.exceedRendererCapabilitiesIfNecessary;
tunnelingEnabled = builder.tunnelingEnabled;
@ -1109,8 +1092,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
== other.allowAudioMixedChannelCountAdaptiveness
&& allowAudioMixedDecoderSupportAdaptiveness
== other.allowAudioMixedDecoderSupportAdaptiveness
// Text
&& disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags
// General
&& exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary
&& tunnelingEnabled == other.tunnelingEnabled
@ -1135,8 +1116,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
result = 31 * result + (allowAudioMixedSampleRateAdaptiveness ? 1 : 0);
result = 31 * result + (allowAudioMixedChannelCountAdaptiveness ? 1 : 0);
result = 31 * result + (allowAudioMixedDecoderSupportAdaptiveness ? 1 : 0);
// Text
result = 31 * result + disabledTextTrackSelectionFlags;
// General
result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0);
result = 31 * result + (tunnelingEnabled ? 1 : 0);
@ -1151,23 +1130,26 @@ public class DefaultTrackSelector extends MappingTrackSelector {
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
// Video
FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY,
FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS,
FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS,
FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS,
// Audio
FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY,
FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS,
FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS,
FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS,
FIELD_DISABLED_TEXT_TRACK_SELECTION_FLAGS,
FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS,
// General
FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY,
FIELD_TUNNELING_ENABLED,
FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS,
// Overrides
FIELD_SELECTION_OVERRIDES_RENDERER_INDICES,
FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS,
FIELD_SELECTION_OVERRIDES,
FIELD_RENDERER_DISABLED_INDICES,
FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS,
FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS
})
private @interface FieldNumber {}
@ -1179,16 +1161,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private static final int FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS = 1004;
private static final int FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS = 1005;
private static final int FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS = 1006;
private static final int FIELD_DISABLED_TEXT_TRACK_SELECTION_FLAGS = 1007;
private static final int FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY = 1008;
private static final int FIELD_TUNNELING_ENABLED = 1009;
private static final int FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS = 1010;
private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDICES = 1011;
private static final int FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS = 1012;
private static final int FIELD_SELECTION_OVERRIDES = 1013;
private static final int FIELD_RENDERER_DISABLED_INDICES = 1014;
private static final int FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = 1015;
private static final int FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = 1016;
private static final int FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY = 1007;
private static final int FIELD_TUNNELING_ENABLED = 1008;
private static final int FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS = 1009;
private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDICES = 1010;
private static final int FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS = 1011;
private static final int FIELD_SELECTION_OVERRIDES = 1012;
private static final int FIELD_RENDERER_DISABLED_INDICES = 1013;
private static final int FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = 1014;
private static final int FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = 1015;
@Override
public Bundle toBundle() {
@ -1223,9 +1204,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
bundle.putBoolean(
keyForField(FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS),
allowAudioMixedDecoderSupportAdaptiveness);
// Text
bundle.putInt(
keyForField(FIELD_DISABLED_TEXT_TRACK_SELECTION_FLAGS), disabledTextTrackSelectionFlags);
// General
bundle.putBoolean(
keyForField(FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY),
@ -1736,7 +1714,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* renderer index, or null if no selection was made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
@SuppressLint("WrongConstant") // Lint doesn't understand arrays of IntDefs.
@Nullable
protected Pair<ExoTrackSelection.Definition, Integer> selectVideoTrack(
MappedTrackInfo mappedTrackInfo,
@ -1748,7 +1725,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
C.TRACK_TYPE_VIDEO,
mappedTrackInfo,
rendererFormatSupports,
(rendererIndex, group, support) ->
(int rendererIndex, TrackGroup group, @Capabilities int[] support) ->
VideoTrackInfo.createForTrackGroup(
rendererIndex, group, params, support, mixedMimeTypeSupports[rendererIndex]),
VideoTrackInfo::compareSelections);
@ -1770,7 +1747,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* renderer index, or null if no selection was made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
@SuppressLint("WrongConstant") // Lint doesn't understand arrays of IntDefs.
@Nullable
protected Pair<ExoTrackSelection.Definition, Integer> selectAudioTrack(
MappedTrackInfo mappedTrackInfo,
@ -1791,7 +1767,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
C.TRACK_TYPE_AUDIO,
mappedTrackInfo,
rendererFormatSupports,
(rendererIndex, group, support) ->
(int rendererIndex, TrackGroup group, @Capabilities int[] support) ->
AudioTrackInfo.createForTrackGroup(
rendererIndex, group, params, support, hasVideoRendererWithMappedTracksFinal),
AudioTrackInfo::compareSelections);
@ -1813,7 +1789,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* renderer index, or null if no selection was made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/
@SuppressLint("WrongConstant") // Lint doesn't understand arrays of IntDefs.
@Nullable
protected Pair<ExoTrackSelection.Definition, Integer> selectTextTrack(
MappedTrackInfo mappedTrackInfo,
@ -1825,7 +1800,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
C.TRACK_TYPE_TEXT,
mappedTrackInfo,
rendererFormatSupports,
(rendererIndex, group, support) ->
(int rendererIndex, TrackGroup group, @Capabilities int[] support) ->
TextTrackInfo.createForTrackGroup(
rendererIndex, group, params, support, selectedAudioLanguage),
TextTrackInfo::compareSelections);
@ -1959,11 +1934,11 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// want the renderer to be enabled at all, so clear any existing selection.
@Nullable ExoTrackSelection.Definition selection;
if (!overrideForType.trackIndices.isEmpty()
&& mappedTrackInfo.getTrackGroups(rendererIndex).indexOf(overrideForType.trackGroup)
&& mappedTrackInfo.getTrackGroups(rendererIndex).indexOf(overrideForType.mediaTrackGroup)
!= -1) {
selection =
new ExoTrackSelection.Definition(
overrideForType.trackGroup, Ints.toArray(overrideForType.trackIndices));
overrideForType.mediaTrackGroup, Ints.toArray(overrideForType.trackIndices));
} else {
selection = null;
}
@ -1987,12 +1962,11 @@ public class DefaultTrackSelector extends MappingTrackSelector {
if (override == null) {
continue;
}
@Nullable
TrackSelectionOverride existingOverride = overridesByType.get(override.getTrackType());
@Nullable TrackSelectionOverride existingOverride = overridesByType.get(override.getType());
// Only replace an existing override if it's empty and the one being considered is not.
if (existingOverride == null
|| (existingOverride.trackIndices.isEmpty() && !override.trackIndices.isEmpty())) {
overridesByType.put(override.getTrackType(), override);
overridesByType.put(override.getType(), override);
}
}
}
@ -2748,8 +2722,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
super(rendererIndex, trackGroup, trackIndex);
isWithinRendererCapabilities =
isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false);
int maskedSelectionFlags =
format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags;
int maskedSelectionFlags = format.selectionFlags & ~parameters.ignoredTextSelectionFlags;
isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0;
int bestLanguageIndex = Integer.MAX_VALUE;

View File

@ -31,7 +31,7 @@ import androidx.media3.common.C;
import androidx.media3.common.C.FormatSupport;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.ExoPlaybackException;
@ -427,9 +427,9 @@ public abstract class MappingTrackSelector extends TrackSelector {
periodId,
timeline);
TracksInfo tracksInfo = TrackSelectionUtil.buildTracksInfo(mappedTrackInfo, result.second);
Tracks tracks = TrackSelectionUtil.buildTracks(mappedTrackInfo, result.second);
return new TrackSelectorResult(result.first, result.second, tracksInfo, mappedTrackInfo);
return new TrackSelectorResult(result.first, result.second, tracks, mappedTrackInfo);
}
/**

View File

@ -19,8 +19,7 @@ import android.os.SystemClock;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.TracksInfo.TrackGroupInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.RendererCapabilities;
import androidx.media3.exoplayer.source.TrackGroupArray;
@ -136,16 +135,16 @@ public final class TrackSelectionUtil {
}
/**
* Returns {@link TracksInfo} built from {@link MappingTrackSelector.MappedTrackInfo} and {@link
* Returns {@link Tracks} built from {@link MappingTrackSelector.MappedTrackInfo} and {@link
* TrackSelection TrackSelections} for each renderer.
*
* @param mappedTrackInfo The {@link MappingTrackSelector.MappedTrackInfo}
* @param selections The track selections, indexed by renderer. A null entry indicates that a
* renderer does not have any selected tracks.
* @return The corresponding {@link TracksInfo}.
* @return The corresponding {@link Tracks}.
*/
@SuppressWarnings({"unchecked", "rawtypes"}) // Initialization of array of Lists.
public static TracksInfo buildTracksInfo(
public static Tracks buildTracks(
MappingTrackSelector.MappedTrackInfo mappedTrackInfo,
@NullableType TrackSelection[] selections) {
List<? extends TrackSelection>[] listSelections = new List[selections.length];
@ -153,22 +152,22 @@ public final class TrackSelectionUtil {
@Nullable TrackSelection selection = selections[i];
listSelections[i] = selection != null ? ImmutableList.of(selection) : ImmutableList.of();
}
return buildTracksInfo(mappedTrackInfo, listSelections);
return buildTracks(mappedTrackInfo, listSelections);
}
/**
* Returns {@link TracksInfo} built from {@link MappingTrackSelector.MappedTrackInfo} and {@link
* Returns {@link Tracks} built from {@link MappingTrackSelector.MappedTrackInfo} and {@link
* TrackSelection TrackSelections} for each renderer.
*
* @param mappedTrackInfo The {@link MappingTrackSelector.MappedTrackInfo}
* @param selections The track selections, indexed by renderer. Null entries are not permitted. An
* empty list indicates that a renderer does not have any selected tracks.
* @return The corresponding {@link TracksInfo}.
* @return The corresponding {@link Tracks}.
*/
public static TracksInfo buildTracksInfo(
public static Tracks buildTracks(
MappingTrackSelector.MappedTrackInfo mappedTrackInfo,
List<? extends TrackSelection>[] selections) {
ImmutableList.Builder<TrackGroupInfo> trackGroupInfos = new ImmutableList.Builder<>();
ImmutableList.Builder<Tracks.Group> trackGroups = new ImmutableList.Builder<>();
for (int rendererIndex = 0;
rendererIndex < mappedTrackInfo.getRendererCount();
rendererIndex++) {
@ -196,8 +195,7 @@ public final class TrackSelectionUtil {
}
selected[trackIndex] = isTrackSelected;
}
trackGroupInfos.add(
new TrackGroupInfo(trackGroup, adaptiveSupported, trackSupport, selected));
trackGroups.add(new Tracks.Group(trackGroup, adaptiveSupported, trackSupport, selected));
}
}
TrackGroupArray unmappedTrackGroups = mappedTrackInfo.getUnmappedTrackGroups();
@ -206,9 +204,9 @@ public final class TrackSelectionUtil {
@C.FormatSupport int[] trackSupport = new int[trackGroup.length];
Arrays.fill(trackSupport, C.FORMAT_UNSUPPORTED_TYPE);
boolean[] selected = new boolean[trackGroup.length];
trackGroupInfos.add(
new TrackGroupInfo(trackGroup, /* adaptiveSupported= */ false, trackSupport, selected));
trackGroups.add(
new Tracks.Group(trackGroup, /* adaptiveSupported= */ false, trackSupport, selected));
}
return new TracksInfo(trackGroupInfos.build());
return new Tracks(trackGroups.build());
}
}

View File

@ -16,7 +16,7 @@
package androidx.media3.exoplayer.trackselection;
import androidx.annotation.Nullable;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.RendererConfiguration;
@ -36,7 +36,7 @@ public final class TrackSelectorResult {
/** A {@link ExoTrackSelection} array containing the track selection for each renderer. */
public final @NullableType ExoTrackSelection[] selections;
/** Describe the tracks and which one were selected. */
public final TracksInfo tracksInfo;
public final Tracks tracks;
/**
* An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)}
* should the selections be activated.
@ -51,21 +51,21 @@ public final class TrackSelectorResult {
* TrackSelector#onSelectionActivated(Object)} should the selection be activated. May be
* {@code null}.
* @deprecated Use {@link #TrackSelectorResult(RendererConfiguration[], ExoTrackSelection[],
* TracksInfo, Object)}.
* Tracks, Object)}.
*/
@Deprecated
public TrackSelectorResult(
@NullableType RendererConfiguration[] rendererConfigurations,
@NullableType ExoTrackSelection[] selections,
@Nullable Object info) {
this(rendererConfigurations, selections, TracksInfo.EMPTY, info);
this(rendererConfigurations, selections, Tracks.EMPTY, info);
}
/**
* @param rendererConfigurations A {@link RendererConfiguration} for each renderer. A null entry
* indicates the corresponding renderer should be disabled.
* @param selections A {@link ExoTrackSelection} array containing the selection for each renderer.
* @param tracksInfo Description of the available tracks and which one were selected.
* @param tracks Description of the available tracks and which one were selected.
* @param info An opaque object that will be returned to {@link
* TrackSelector#onSelectionActivated(Object)} should the selection be activated. May be
* {@code null}.
@ -73,11 +73,11 @@ public final class TrackSelectorResult {
public TrackSelectorResult(
@NullableType RendererConfiguration[] rendererConfigurations,
@NullableType ExoTrackSelection[] selections,
TracksInfo tracksInfo,
Tracks tracks,
@Nullable Object info) {
this.rendererConfigurations = rendererConfigurations;
this.selections = selections.clone();
this.tracksInfo = tracksInfo;
this.tracks = tracks;
this.info = info;
length = rendererConfigurations.length;
}

View File

@ -31,8 +31,7 @@ import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Player.PlaybackSuppressionReason;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
@ -262,23 +261,23 @@ public class EventLogger implements AnalyticsListener {
}
@Override
public void onTracksInfoChanged(EventTime eventTime, TracksInfo tracksInfo) {
public void onTracksChanged(EventTime eventTime, Tracks tracks) {
logd("tracks [" + getEventTimeString(eventTime));
// Log tracks associated to renderers.
ImmutableList<TracksInfo.TrackGroupInfo> trackGroupInfos = tracksInfo.getTrackGroupInfos();
for (int groupIndex = 0; groupIndex < trackGroupInfos.size(); groupIndex++) {
TracksInfo.TrackGroupInfo trackGroupInfo = trackGroupInfos.get(groupIndex);
ImmutableList<Tracks.Group> trackGroups = tracks.getGroups();
for (int groupIndex = 0; groupIndex < trackGroups.size(); groupIndex++) {
Tracks.Group trackGroup = trackGroups.get(groupIndex);
logd(" group [");
for (int trackIndex = 0; trackIndex < trackGroupInfo.length; trackIndex++) {
String status = getTrackStatusString(trackGroupInfo.isTrackSelected(trackIndex));
String formatSupport = getFormatSupportString(trackGroupInfo.getTrackSupport(trackIndex));
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
String status = getTrackStatusString(trackGroup.isTrackSelected(trackIndex));
String formatSupport = getFormatSupportString(trackGroup.getTrackSupport(trackIndex));
logd(
" "
+ status
+ " Track:"
+ trackIndex
+ ", "
+ Format.toLogString(trackGroupInfo.getTrackFormat(trackIndex))
+ Format.toLogString(trackGroup.getTrackFormat(trackIndex))
+ ", supported="
+ formatSupport);
}
@ -287,12 +286,11 @@ public class EventLogger implements AnalyticsListener {
// TODO: Replace this with an override of onMediaMetadataChanged.
// Log metadata for at most one of the selected tracks.
boolean loggedMetadata = false;
for (int groupIndex = 0; !loggedMetadata && groupIndex < trackGroupInfos.size(); groupIndex++) {
TracksInfo.TrackGroupInfo trackGroupInfo = trackGroupInfos.get(groupIndex);
TrackGroup trackGroup = trackGroupInfo.getTrackGroup();
for (int groupIndex = 0; !loggedMetadata && groupIndex < trackGroups.size(); groupIndex++) {
Tracks.Group trackGroup = trackGroups.get(groupIndex);
for (int trackIndex = 0; !loggedMetadata && trackIndex < trackGroup.length; trackIndex++) {
if (trackGroupInfo.isTrackSelected(trackIndex)) {
@Nullable Metadata metadata = trackGroup.getFormat(trackIndex).metadata;
if (trackGroup.isTrackSelected(trackIndex)) {
@Nullable Metadata metadata = trackGroup.getTrackFormat(trackIndex).metadata;
if (metadata != null && metadata.length() > 0) {
logd(" Metadata [");
printMetadata(metadata, " ");

View File

@ -129,7 +129,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private boolean codecHandlesHdr10PlusOutOfBandMetadata;
@Nullable private Surface surface;
@Nullable private DummySurface dummySurface;
@Nullable private PlaceholderSurface placeholderSurface;
private boolean haveReportedFirstFrameRenderedForCurrentSurface;
private @C.VideoScalingMode int scalingMode;
private boolean renderedFirstFrameAfterReset;
@ -515,7 +515,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
public boolean isReady() {
if (super.isReady()
&& (renderedFirstFrameAfterReset
|| (dummySurface != null && surface == dummySurface)
|| (placeholderSurface != null && surface == placeholderSurface)
|| getCodec() == null
|| tunneling)) {
// Ready. If we were joining then we've now joined, so clear the joining deadline.
@ -567,14 +567,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
}
@TargetApi(17) // Needed for dummySurface usage. dummySurface is always null on API level 16.
@TargetApi(17) // Needed for placeholderSurface usage, as it is always null on API level 16.
@Override
protected void onReset() {
try {
super.onReset();
} finally {
if (dummySurface != null) {
releaseDummySurface();
if (placeholderSurface != null) {
releasePlaceholderSurface();
}
}
}
@ -624,14 +624,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Nullable Surface surface = output instanceof Surface ? (Surface) output : null;
if (surface == null) {
// Use a dummy surface if possible.
if (dummySurface != null) {
surface = dummySurface;
// Use a placeholder surface if possible.
if (placeholderSurface != null) {
surface = placeholderSurface;
} else {
MediaCodecInfo codecInfo = getCodecInfo();
if (codecInfo != null && shouldUseDummySurface(codecInfo)) {
dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure);
surface = dummySurface;
placeholderSurface = PlaceholderSurface.newInstanceV17(context, codecInfo.secure);
surface = placeholderSurface;
}
}
}
@ -652,7 +652,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
maybeInitCodecOrBypass();
}
}
if (surface != null && surface != dummySurface) {
if (surface != null && surface != placeholderSurface) {
// If we know the video size, report it again immediately.
maybeRenotifyVideoSizeChanged();
// We haven't rendered to the new surface yet.
@ -665,7 +665,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
clearReportedVideoSize();
clearRenderedFirstFrame();
}
} else if (surface != null && surface != dummySurface) {
} else if (surface != null && surface != placeholderSurface) {
// The surface is set and unchanged. If we know the video size and/or have already rendered to
// the surface, report these again immediately.
maybeRenotifyVideoSizeChanged();
@ -684,16 +684,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return tunneling && Util.SDK_INT < 23;
}
@TargetApi(17) // Needed for dummySurface usage. dummySurface is always null on API level 16.
@TargetApi(17) // Needed for placeHolderSurface usage, as it is always null on API level 16.
@Override
protected MediaCodecAdapter.Configuration getMediaCodecConfiguration(
MediaCodecInfo codecInfo,
Format format,
@Nullable MediaCrypto crypto,
float codecOperatingRate) {
if (dummySurface != null && dummySurface.secure != codecInfo.secure) {
if (placeholderSurface != null && placeholderSurface.secure != codecInfo.secure) {
// We can't re-use the current DummySurface instance with the new decoder.
releaseDummySurface();
releasePlaceholderSurface();
}
String codecMimeType = codecInfo.codecMimeType;
codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats());
@ -709,10 +709,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
if (!shouldUseDummySurface(codecInfo)) {
throw new IllegalStateException();
}
if (dummySurface == null) {
dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure);
if (placeholderSurface == null) {
placeholderSurface = PlaceholderSurface.newInstanceV17(context, codecInfo.secure);
}
surface = dummySurface;
surface = placeholderSurface;
}
return MediaCodecAdapter.Configuration.createForVideoDecoding(
codecInfo, mediaFormat, format, surface, crypto);
@ -753,6 +753,82 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
frameReleaseHelper.onPlaybackSpeed(currentPlaybackSpeed);
}
/**
* Returns a maximum input size for a given codec and format.
*
* @param codecInfo Information about the {@link MediaCodec} being configured.
* @param format The format.
* @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be
* determined.
*/
public static int getCodecMaxInputSize(MediaCodecInfo codecInfo, Format format) {
int width = format.width;
int height = format.height;
if (width == Format.NO_VALUE || height == Format.NO_VALUE) {
// We can't infer a maximum input size without video dimensions.
return Format.NO_VALUE;
}
String sampleMimeType = format.sampleMimeType;
if (MimeTypes.VIDEO_DOLBY_VISION.equals(sampleMimeType)) {
// Dolby vision can be a wrapper around H264 or H265. We assume it's wrapping H265 by default
// because it's the common case, and because some devices may fail to allocate the codec when
// the larger buffer size required for H264 is requested. We size buffers for H264 only if the
// format contains sufficient information for us to determine unambiguously that it's a H264
// profile.
sampleMimeType = MimeTypes.VIDEO_H265;
@Nullable
Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format);
if (codecProfileAndLevel != null) {
int profile = codecProfileAndLevel.first;
if (profile == CodecProfileLevel.DolbyVisionProfileDvavSe
|| profile == CodecProfileLevel.DolbyVisionProfileDvavPer
|| profile == CodecProfileLevel.DolbyVisionProfileDvavPen) {
sampleMimeType = MimeTypes.VIDEO_H264;
}
}
}
// Attempt to infer a maximum input size from the format.
int maxPixels;
int minCompressionRatio;
switch (sampleMimeType) {
case MimeTypes.VIDEO_H263:
case MimeTypes.VIDEO_MP4V:
maxPixels = width * height;
minCompressionRatio = 2;
break;
case MimeTypes.VIDEO_H264:
if ("BRAVIA 4K 2015".equals(Util.MODEL) // Sony Bravia 4K
|| ("Amazon".equals(Util.MANUFACTURER)
&& ("KFSOWI".equals(Util.MODEL) // Kindle Soho
|| ("AFTS".equals(Util.MODEL) && codecInfo.secure)))) { // Fire TV Gen 2
// Use the default value for cases where platform limitations may prevent buffers of the
// calculated maximum input size from being allocated.
return Format.NO_VALUE;
}
// Round up width/height to an integer number of macroblocks.
maxPixels = Util.ceilDivide(width, 16) * Util.ceilDivide(height, 16) * 16 * 16;
minCompressionRatio = 2;
break;
case MimeTypes.VIDEO_VP8:
// VPX does not specify a ratio so use the values from the platform's SoftVPX.cpp.
maxPixels = width * height;
minCompressionRatio = 2;
break;
case MimeTypes.VIDEO_H265:
case MimeTypes.VIDEO_VP9:
maxPixels = width * height;
minCompressionRatio = 4;
break;
default:
// Leave the default max input size.
return Format.NO_VALUE;
}
// Estimate the maximum input size assuming three channel 4:2:0 subsampled input frames.
return (maxPixels * 3) / (2 * minCompressionRatio);
}
@Override
protected float getCodecOperatingRateV23(
float targetPlaybackSpeed, Format format, Format[] streamFormats) {
@ -949,7 +1025,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
earlyUs -= elapsedRealtimeNowUs - elapsedRealtimeUs;
}
if (surface == dummySurface) {
if (surface == placeholderSurface) {
// Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
if (isBufferLate(earlyUs)) {
skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
@ -1259,16 +1335,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return Util.SDK_INT >= 23
&& !tunneling
&& !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name)
&& (!codecInfo.secure || DummySurface.isSecureSupported(context));
&& (!codecInfo.secure || PlaceholderSurface.isSecureSupported(context));
}
@RequiresApi(17)
private void releaseDummySurface() {
if (surface == dummySurface) {
private void releasePlaceholderSurface() {
if (surface == placeholderSurface) {
surface = null;
}
dummySurface.release();
dummySurface = null;
placeholderSurface.release();
placeholderSurface = null;
}
private void setJoiningDeadlineMs() {
@ -1585,82 +1661,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
}
/**
* Returns a maximum input size for a given codec and format.
*
* @param codecInfo Information about the {@link MediaCodec} being configured.
* @param format The format.
* @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be
* determined.
*/
private static int getCodecMaxInputSize(MediaCodecInfo codecInfo, Format format) {
int width = format.width;
int height = format.height;
if (width == Format.NO_VALUE || height == Format.NO_VALUE) {
// We can't infer a maximum input size without video dimensions.
return Format.NO_VALUE;
}
String sampleMimeType = format.sampleMimeType;
if (MimeTypes.VIDEO_DOLBY_VISION.equals(sampleMimeType)) {
// Dolby vision can be a wrapper around H264 or H265. We assume it's wrapping H265 by default
// because it's the common case, and because some devices may fail to allocate the codec when
// the larger buffer size required for H264 is requested. We size buffers for H264 only if the
// format contains sufficient information for us to determine unambiguously that it's a H264
// profile.
sampleMimeType = MimeTypes.VIDEO_H265;
@Nullable
Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format);
if (codecProfileAndLevel != null) {
int profile = codecProfileAndLevel.first;
if (profile == CodecProfileLevel.DolbyVisionProfileDvavSe
|| profile == CodecProfileLevel.DolbyVisionProfileDvavPer
|| profile == CodecProfileLevel.DolbyVisionProfileDvavPen) {
sampleMimeType = MimeTypes.VIDEO_H264;
}
}
}
// Attempt to infer a maximum input size from the format.
int maxPixels;
int minCompressionRatio;
switch (sampleMimeType) {
case MimeTypes.VIDEO_H263:
case MimeTypes.VIDEO_MP4V:
maxPixels = width * height;
minCompressionRatio = 2;
break;
case MimeTypes.VIDEO_H264:
if ("BRAVIA 4K 2015".equals(Util.MODEL) // Sony Bravia 4K
|| ("Amazon".equals(Util.MANUFACTURER)
&& ("KFSOWI".equals(Util.MODEL) // Kindle Soho
|| ("AFTS".equals(Util.MODEL) && codecInfo.secure)))) { // Fire TV Gen 2
// Use the default value for cases where platform limitations may prevent buffers of the
// calculated maximum input size from being allocated.
return Format.NO_VALUE;
}
// Round up width/height to an integer number of macroblocks.
maxPixels = Util.ceilDivide(width, 16) * Util.ceilDivide(height, 16) * 16 * 16;
minCompressionRatio = 2;
break;
case MimeTypes.VIDEO_VP8:
// VPX does not specify a ratio so use the values from the platform's SoftVPX.cpp.
maxPixels = width * height;
minCompressionRatio = 2;
break;
case MimeTypes.VIDEO_H265:
case MimeTypes.VIDEO_VP9:
maxPixels = width * height;
minCompressionRatio = 4;
break;
default:
// Leave the default max input size.
return Format.NO_VALUE;
}
// Estimate the maximum input size assuming three channel 4:2:0 subsampled input frames.
return (maxPixels * 3) / (2 * minCompressionRatio);
}
/**
* Returns whether the device is known to do post processing by default that isn't compatible with
* ExoPlayer.

View File

@ -36,12 +36,12 @@ import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A dummy {@link Surface}. */
/** A placeholder {@link Surface}. */
@RequiresApi(17)
@UnstableApi
public final class DummySurface extends Surface {
public final class PlaceholderSurface extends Surface {
private static final String TAG = "DummySurface";
private static final String TAG = "PlaceholderSurface";
/** Whether the surface is secure. */
public final boolean secure;
@ -49,14 +49,14 @@ public final class DummySurface extends Surface {
private static @SecureMode int secureMode;
private static boolean secureModeInitialized;
private final DummySurfaceThread thread;
private final PlaceholderSurfaceThread thread;
private boolean threadReleased;
/**
* Returns whether the device supports secure dummy surfaces.
* Returns whether the device supports secure placeholder surfaces.
*
* @param context Any {@link Context}.
* @return Whether the device supports secure dummy surfaces.
* @return Whether the device supports secure placeholder surfaces.
*/
public static synchronized boolean isSecureSupported(Context context) {
if (!secureModeInitialized) {
@ -67,8 +67,8 @@ public final class DummySurface extends Surface {
}
/**
* Returns a newly created dummy surface. The surface must be released by calling {@link #release}
* when it's no longer required.
* Returns a newly created placeholder surface. The surface must be released by calling {@link
* #release} when it's no longer required.
*
* <p>Must only be called if {@link Util#SDK_INT} is 17 or higher.
*
@ -78,13 +78,14 @@ public final class DummySurface extends Surface {
* @throws IllegalStateException If a secure surface is requested on a device for which {@link
* #isSecureSupported(Context)} returns {@code false}.
*/
public static DummySurface newInstanceV17(Context context, boolean secure) {
public static PlaceholderSurface newInstanceV17(Context context, boolean secure) {
Assertions.checkState(!secure || isSecureSupported(context));
DummySurfaceThread thread = new DummySurfaceThread();
PlaceholderSurfaceThread thread = new PlaceholderSurfaceThread();
return thread.init(secure ? secureMode : SECURE_MODE_NONE);
}
private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) {
private PlaceholderSurface(
PlaceholderSurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) {
super(surfaceTexture);
this.thread = thread;
this.secure = secure;
@ -121,7 +122,7 @@ public final class DummySurface extends Surface {
}
}
private static class DummySurfaceThread extends HandlerThread implements Handler.Callback {
private static class PlaceholderSurfaceThread extends HandlerThread implements Handler.Callback {
private static final int MSG_INIT = 1;
private static final int MSG_RELEASE = 2;
@ -130,13 +131,13 @@ public final class DummySurface extends Surface {
private @MonotonicNonNull Handler handler;
@Nullable private Error initError;
@Nullable private RuntimeException initException;
@Nullable private DummySurface surface;
@Nullable private PlaceholderSurface surface;
public DummySurfaceThread() {
super("ExoPlayer:DummySurface");
public PlaceholderSurfaceThread() {
super("ExoPlayer:PlaceholderSurface");
}
public DummySurface init(@SecureMode int secureMode) {
public PlaceholderSurface init(@SecureMode int secureMode) {
start();
handler = new Handler(getLooper(), /* callback= */ this);
eglSurfaceTexture = new EGLSurfaceTexture(handler);
@ -176,10 +177,10 @@ public final class DummySurface extends Surface {
try {
initInternal(/* secureMode= */ msg.arg1);
} catch (RuntimeException e) {
Log.e(TAG, "Failed to initialize dummy surface", e);
Log.e(TAG, "Failed to initialize placeholder surface", e);
initException = e;
} catch (Error e) {
Log.e(TAG, "Failed to initialize dummy surface", e);
Log.e(TAG, "Failed to initialize placeholder surface", e);
initError = e;
} finally {
synchronized (this) {
@ -191,7 +192,7 @@ public final class DummySurface extends Surface {
try {
releaseInternal();
} catch (Throwable e) {
Log.e(TAG, "Failed to release dummy surface", e);
Log.e(TAG, "Failed to release placeholder surface", e);
} finally {
quit();
}
@ -205,7 +206,7 @@ public final class DummySurface extends Surface {
Assertions.checkNotNull(eglSurfaceTexture);
eglSurfaceTexture.init(secureMode);
this.surface =
new DummySurface(
new PlaceholderSurface(
this, eglSurfaceTexture.getSurfaceTexture(), secureMode != SECURE_MODE_NONE);
}

View File

@ -168,7 +168,7 @@ public final class VideoFrameReleaseHelper {
* @param surface The new {@link Surface}, or {@code null} if the renderer does not have one.
*/
public void onSurfaceChanged(@Nullable Surface surface) {
if (surface instanceof DummySurface) {
if (surface instanceof PlaceholderSurface) {
// We don't care about dummy surfaces for release timing, since they're not visible.
surface = null;
}

View File

@ -23,7 +23,7 @@ import static androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA;
import static androidx.media3.common.Player.COMMAND_GET_TEXT;
import static androidx.media3.common.Player.COMMAND_GET_TIMELINE;
import static androidx.media3.common.Player.COMMAND_GET_TRACK_INFOS;
import static androidx.media3.common.Player.COMMAND_GET_TRACKS;
import static androidx.media3.common.Player.COMMAND_GET_VOLUME;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.common.Player.COMMAND_PREPARE;
@ -108,8 +108,7 @@ import androidx.media3.common.Player.PositionInfo;
import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Window;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.TracksInfo.TrackGroupInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.Util;
@ -277,15 +276,15 @@ public final class ExoPlayerTest {
argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
.verify(mockListener)
.onTracksInfoChanged(
.onTracksChanged(
eq(
new TracksInfo(
new Tracks(
ImmutableList.of(
new TrackGroupInfo(
new Tracks.Group(
new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT),
/* adaptiveSupported= */ false,
new int[] {C.FORMAT_HANDLED},
/* tracksSelected= */ new boolean[] {true})))));
/* trackSelected= */ new boolean[] {true})))));
inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
assertThat(renderer.getFormatsRead()).containsExactly(ExoPlayerTestRunner.VIDEO_FORMAT);
@ -656,15 +655,15 @@ public final class ExoPlayerTest {
argThat(noUid(thirdTimeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
.verify(mockPlayerListener)
.onTracksInfoChanged(
.onTracksChanged(
eq(
new TracksInfo(
new Tracks(
ImmutableList.of(
new TrackGroupInfo(
new Tracks.Group(
new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT),
/* adaptiveSupported= */ false,
new int[] {C.FORMAT_HANDLED},
/* tracksSelected= */ new boolean[] {true})))));
/* trackSelected= */ new boolean[] {true})))));
assertThat(renderer.isEnded).isTrue();
}
@ -3426,7 +3425,7 @@ public final class ExoPlayerTest {
.waitForPendingPlayerCommands()
.play()
.build();
List<TracksInfo> tracksInfoList = new ArrayList<>();
List<Tracks> tracksList = new ArrayList<>();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setSupportedFormats(ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT)
@ -3434,21 +3433,21 @@ public final class ExoPlayerTest {
.setPlayerListener(
new Player.Listener() {
@Override
public void onTracksInfoChanged(TracksInfo tracksInfo) {
tracksInfoList.add(tracksInfo);
public void onTracksChanged(Tracks tracks) {
tracksList.add(tracks);
}
})
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(tracksInfoList).hasSize(3);
assertThat(tracksList).hasSize(3);
// First track groups of the 1st period are reported.
// Then the seek to an unprepared period will result in empty track groups being returned.
// Then the track groups of the 2nd period are reported.
assertThat(tracksInfoList.get(0).getTrackGroupInfos().get(0).getTrackFormat(0))
assertThat(tracksList.get(0).getGroups().get(0).getTrackFormat(0))
.isEqualTo(ExoPlayerTestRunner.VIDEO_FORMAT);
assertThat(tracksInfoList.get(1)).isEqualTo(TracksInfo.EMPTY);
assertThat(tracksInfoList.get(2).getTrackGroupInfos().get(0).getTrackFormat(0))
assertThat(tracksList.get(1)).isEqualTo(Tracks.EMPTY);
assertThat(tracksList.get(2).getGroups().get(0).getTrackFormat(0))
.isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT);
}
@ -7993,7 +7992,7 @@ public final class ExoPlayerTest {
}
};
AtomicReference<Timeline> timelineAfterError = new AtomicReference<>();
AtomicReference<TracksInfo> trackInfosAfterError = new AtomicReference<>();
AtomicReference<Tracks> trackInfosAfterError = new AtomicReference<>();
AtomicInteger mediaItemIndexAfterError = new AtomicInteger();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
@ -8006,7 +8005,7 @@ public final class ExoPlayerTest {
@Override
public void onPlayerError(EventTime eventTime, PlaybackException error) {
timelineAfterError.set(player.getCurrentTimeline());
trackInfosAfterError.set(player.getCurrentTracksInfo());
trackInfosAfterError.set(player.getCurrentTracks());
mediaItemIndexAfterError.set(player.getCurrentMediaItemIndex());
}
});
@ -8035,8 +8034,8 @@ public final class ExoPlayerTest {
assertThat(timelineAfterError.get().getWindowCount()).isEqualTo(1);
assertThat(mediaItemIndexAfterError.get()).isEqualTo(0);
assertThat(trackInfosAfterError.get().getTrackGroupInfos()).hasSize(1);
assertThat(trackInfosAfterError.get().getTrackGroupInfos().get(0).getTrackFormat(0))
assertThat(trackInfosAfterError.get().getGroups()).hasSize(1);
assertThat(trackInfosAfterError.get().getGroups().get(0).getTrackFormat(0))
.isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT);
assertThat(trackInfosAfterError.get().isTypeSelected(C.TRACK_TYPE_VIDEO)).isFalse();
assertThat(trackInfosAfterError.get().isTypeSelected(C.TRACK_TYPE_AUDIO)).isTrue();
@ -8989,7 +8988,7 @@ public final class ExoPlayerTest {
assertThat(player.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_GET_TEXT)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_GET_TRACK_INFOS)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_GET_TRACKS)).isTrue();
}
@Test
@ -10423,7 +10422,7 @@ public final class ExoPlayerTest {
verify(listener, atLeastOnce()).onShuffleModeEnabledChanged(anyBoolean());
verify(listener, atLeastOnce()).onPlaybackStateChanged(anyInt());
verify(listener, atLeastOnce()).onIsLoadingChanged(anyBoolean());
verify(listener, atLeastOnce()).onTracksInfoChanged(any());
verify(listener, atLeastOnce()).onTracksChanged(any());
verify(listener, atLeastOnce()).onMediaMetadataChanged(any());
verify(listener, atLeastOnce()).onPlayWhenReadyChanged(anyBoolean(), anyInt());
verify(listener, atLeastOnce()).onIsPlayingChanged(anyBoolean());
@ -12138,7 +12137,7 @@ public final class ExoPlayerTest {
COMMAND_SET_VIDEO_SURFACE,
COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACK_INFOS);
COMMAND_GET_TRACKS);
if (!isTimelineEmpty) {
builder.add(COMMAND_SEEK_TO_PREVIOUS);
}

View File

@ -34,7 +34,7 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Clock;
import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector;
@ -1141,7 +1141,7 @@ public final class MediaPeriodQueueTest {
new TrackSelectorResult(
new RendererConfiguration[0],
new ExoTrackSelection[0],
TracksInfo.EMPTY,
Tracks.EMPTY,
/* info= */ null));
}

View File

@ -81,7 +81,7 @@ import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Window;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ConditionVariable;
@ -1729,7 +1729,7 @@ public final class DefaultAnalyticsCollectorTest {
ArgumentCaptor<AnalyticsListener.EventTime> individualTracksChangedEventTimes =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(listener, atLeastOnce())
.onTracksInfoChanged(individualTracksChangedEventTimes.capture(), any());
.onTracksChanged(individualTracksChangedEventTimes.capture(), any());
ArgumentCaptor<AnalyticsListener.EventTime> individualPlayWhenReadyChangedEventTimes =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(listener, atLeastOnce())
@ -2243,7 +2243,7 @@ public final class DefaultAnalyticsCollectorTest {
}
@Override
public void onTracksInfoChanged(EventTime eventTime, TracksInfo tracksInfo) {
public void onTracksChanged(EventTime eventTime, Tracks tracks) {
reportedEvents.add(new ReportedEvent(EVENT_TRACKS_CHANGED, eventTime));
}

View File

@ -21,7 +21,12 @@ import static androidx.media3.exoplayer.RendererCapabilities.DECODER_SUPPORT_PRI
import static androidx.media3.exoplayer.RendererCapabilities.TUNNELING_NOT_SUPPORTED;
import static androidx.media3.exoplayer.RendererCapabilities.TUNNELING_SUPPORTED;
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -46,6 +51,7 @@ import com.google.common.collect.ImmutableList;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
@ -139,6 +145,100 @@ public class DecoderAudioRendererTest {
verify(mockAudioSink, times(1)).reset();
}
@Test
public void firstSampleOfStreamSignalsDiscontinuityToAudioSink() throws Exception {
when(mockAudioSink.handleBuffer(any(), anyLong(), anyInt())).thenReturn(true);
when(mockAudioSink.isEnded()).thenReturn(true);
InOrder inOrderAudioSink = inOrder(mockAudioSink);
FakeSampleStream fakeSampleStream =
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
FORMAT,
ImmutableList.of(
oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME),
oneByteSample(/* timeUs= */ 1_000),
END_OF_STREAM_ITEM));
fakeSampleStream.writeData(/* startPositionUs= */ 0);
audioRenderer.enable(
RendererConfiguration.DEFAULT,
new Format[] {FORMAT},
fakeSampleStream,
/* positionUs= */ 0,
/* joining= */ false,
/* mayRenderStartOfStream= */ true,
/* startPositionUs= */ 0,
/* offsetUs= */ 0);
audioRenderer.setCurrentStreamFinal();
while (!audioRenderer.isEnded()) {
audioRenderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
}
inOrderAudioSink.verify(mockAudioSink, times(1)).handleDiscontinuity();
inOrderAudioSink.verify(mockAudioSink, times(2)).handleBuffer(any(), anyLong(), anyInt());
}
@Test
public void firstSampleOfReplacementStreamSignalsDiscontinuityToAudioSink() throws Exception {
when(mockAudioSink.handleBuffer(any(), anyLong(), anyInt())).thenReturn(true);
when(mockAudioSink.isEnded()).thenReturn(true);
InOrder inOrderAudioSink = inOrder(mockAudioSink);
FakeSampleStream fakeSampleStream1 =
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
FORMAT,
ImmutableList.of(
oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME),
oneByteSample(/* timeUs= */ 1_000),
END_OF_STREAM_ITEM));
fakeSampleStream1.writeData(/* startPositionUs= */ 0);
FakeSampleStream fakeSampleStream2 =
new FakeSampleStream(
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
/* mediaSourceEventDispatcher= */ null,
DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
FORMAT,
ImmutableList.of(
oneByteSample(/* timeUs= */ 1_000_000, C.BUFFER_FLAG_KEY_FRAME),
oneByteSample(/* timeUs= */ 1_001_000),
END_OF_STREAM_ITEM));
fakeSampleStream2.writeData(/* startPositionUs= */ 0);
audioRenderer.enable(
RendererConfiguration.DEFAULT,
new Format[] {FORMAT},
fakeSampleStream1,
/* positionUs= */ 0,
/* joining= */ false,
/* mayRenderStartOfStream= */ true,
/* startPositionUs= */ 0,
/* offsetUs= */ 0);
while (!audioRenderer.hasReadStreamToEnd()) {
audioRenderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
}
audioRenderer.replaceStream(
new Format[] {FORMAT},
fakeSampleStream2,
/* startPositionUs= */ 1_000_000,
/* offsetUs= */ 1_000_000);
audioRenderer.setCurrentStreamFinal();
while (!audioRenderer.isEnded()) {
audioRenderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
}
inOrderAudioSink.verify(mockAudioSink, times(1)).handleDiscontinuity();
inOrderAudioSink.verify(mockAudioSink, times(2)).handleBuffer(any(), anyLong(), anyInt());
inOrderAudioSink.verify(mockAudioSink, times(1)).handleDiscontinuity();
inOrderAudioSink.verify(mockAudioSink, times(2)).handleBuffer(any(), anyLong(), anyInt());
}
private static final class FakeDecoder
extends SimpleDecoder<DecoderInputBuffer, SimpleDecoderOutputBuffer, DecoderException> {

View File

@ -18,7 +18,7 @@ package androidx.media3.exoplayer.offline;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.media3.datasource.DummyDataSource;
import androidx.media3.datasource.PlaceholderDataSource;
import androidx.media3.datasource.cache.Cache;
import androidx.media3.datasource.cache.CacheDataSource;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -35,7 +35,7 @@ public final class DefaultDownloaderFactoryTest {
CacheDataSource.Factory cacheDataSourceFactory =
new CacheDataSource.Factory()
.setCache(Mockito.mock(Cache.class))
.setUpstreamDataSourceFactory(DummyDataSource.FACTORY);
.setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY);
DownloaderFactory factory =
new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run);

View File

@ -41,7 +41,7 @@ import androidx.media3.common.MimeTypes;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.RendererCapabilities;
@ -1130,7 +1130,7 @@ public final class DefaultTrackSelectorTest {
// selected.
trackGroups = wrapFormats(defaultOnly, noFlag, forcedOnly, forcedDefault);
trackSelector.setParameters(
defaultParameters.buildUpon().setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT));
defaultParameters.buildUpon().setIgnoredTextSelectionFlags(C.SELECTION_FLAG_DEFAULT));
result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);
assertNoSelection(result.selections[0]);
@ -1141,8 +1141,7 @@ public final class DefaultTrackSelectorTest {
trackSelector
.getParameters()
.buildUpon()
.setDisabledTextTrackSelectionFlags(
C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED));
.setIgnoredTextSelectionFlags(C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED));
result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);
assertNoSelection(result.selections[0]);
@ -1160,7 +1159,7 @@ public final class DefaultTrackSelectorTest {
trackSelector
.getParameters()
.buildUpon()
.setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT));
.setIgnoredTextSelectionFlags(C.SELECTION_FLAG_DEFAULT));
result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);
assertFixedSelection(result.selections[0], trackGroups, noFlag);
}
@ -2225,10 +2224,10 @@ public final class DefaultTrackSelectorTest {
public void selectTracks_multipleRenderer_allSelected() throws Exception {
RendererCapabilities[] rendererCapabilities =
new RendererCapabilities[] {VIDEO_CAPABILITIES, AUDIO_CAPABILITIES, AUDIO_CAPABILITIES};
TrackGroupArray trackGroups = new TrackGroupArray(AUDIO_TRACK_GROUP);
TrackGroupArray trackGroupArray = new TrackGroupArray(AUDIO_TRACK_GROUP);
TrackSelectorResult result =
trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE);
trackSelector.selectTracks(rendererCapabilities, trackGroupArray, periodId, TIMELINE);
assertThat(result.length).isEqualTo(3);
assertThat(result.rendererConfigurations)
@ -2236,14 +2235,14 @@ public final class DefaultTrackSelectorTest {
.containsExactly(null, DEFAULT, null)
.inOrder();
assertThat(result.selections[0]).isNull();
assertFixedSelection(result.selections[1], trackGroups, trackGroups.get(0).getFormat(0));
assertFixedSelection(
result.selections[1], trackGroupArray, trackGroupArray.get(0).getFormat(0));
assertThat(result.selections[2]).isNull();
ImmutableList<TracksInfo.TrackGroupInfo> trackGroupInfos =
result.tracksInfo.getTrackGroupInfos();
assertThat(trackGroupInfos).hasSize(1);
assertThat(trackGroupInfos.get(0).getTrackGroup()).isEqualTo(AUDIO_TRACK_GROUP);
assertThat(trackGroupInfos.get(0).isTrackSelected(0)).isTrue();
assertThat(trackGroupInfos.get(0).getTrackSupport(0)).isEqualTo(FORMAT_HANDLED);
ImmutableList<Tracks.Group> trackGroups = result.tracks.getGroups();
assertThat(trackGroups).hasSize(1);
assertThat(trackGroups.get(0).getMediaTrackGroup()).isEqualTo(AUDIO_TRACK_GROUP);
assertThat(trackGroups.get(0).isTrackSelected(0)).isTrue();
assertThat(trackGroups.get(0).getTrackSupport(0)).isEqualTo(FORMAT_HANDLED);
}
/** Tests {@link SelectionOverride}'s {@link Bundleable} implementation. */
@ -2371,7 +2370,7 @@ public final class DefaultTrackSelectorTest {
.setPreferredTextLanguages("de", "en")
.setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
.setSelectUndeterminedTextLanguage(true)
.setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_AUTOSELECT)
.setIgnoredTextSelectionFlags(C.SELECTION_FLAG_AUTOSELECT)
// General
.setForceLowestBitrate(false)
.setForceHighestSupportedBitrate(true)

View File

@ -32,8 +32,7 @@ import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.Format;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.TracksInfo.TrackGroupInfo;
import androidx.media3.common.Tracks;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
@ -73,32 +72,32 @@ public class TrackSelectionUtilTest {
new FixedTrackSelection(mappedTrackInfo.getTrackGroups(1).get(0), 1)
};
TracksInfo tracksInfo = TrackSelectionUtil.buildTracksInfo(mappedTrackInfo, selections);
Tracks tracks = TrackSelectionUtil.buildTracks(mappedTrackInfo, selections);
ImmutableList<TracksInfo.TrackGroupInfo> trackGroupInfos = tracksInfo.getTrackGroupInfos();
assertThat(trackGroupInfos).hasSize(4);
assertThat(trackGroupInfos.get(0).getTrackGroup())
ImmutableList<Tracks.Group> trackGroups = tracks.getGroups();
assertThat(trackGroups).hasSize(4);
assertThat(trackGroups.get(0).getMediaTrackGroup())
.isEqualTo(mappedTrackInfo.getTrackGroups(0).get(0));
assertThat(trackGroupInfos.get(1).getTrackGroup())
assertThat(trackGroups.get(1).getMediaTrackGroup())
.isEqualTo(mappedTrackInfo.getTrackGroups(0).get(1));
assertThat(trackGroupInfos.get(2).getTrackGroup())
assertThat(trackGroups.get(2).getMediaTrackGroup())
.isEqualTo(mappedTrackInfo.getTrackGroups(1).get(0));
assertThat(trackGroupInfos.get(3).getTrackGroup())
assertThat(trackGroups.get(3).getMediaTrackGroup())
.isEqualTo(mappedTrackInfo.getUnmappedTrackGroups().get(0));
assertThat(trackGroupInfos.get(0).getTrackSupport(0)).isEqualTo(FORMAT_HANDLED);
assertThat(trackGroupInfos.get(1).getTrackSupport(0)).isEqualTo(FORMAT_UNSUPPORTED_SUBTYPE);
assertThat(trackGroupInfos.get(2).getTrackSupport(0)).isEqualTo(FORMAT_UNSUPPORTED_DRM);
assertThat(trackGroupInfos.get(2).getTrackSupport(1)).isEqualTo(FORMAT_EXCEEDS_CAPABILITIES);
assertThat(trackGroupInfos.get(3).getTrackSupport(0)).isEqualTo(FORMAT_UNSUPPORTED_TYPE);
assertThat(trackGroupInfos.get(0).isTrackSelected(0)).isFalse();
assertThat(trackGroupInfos.get(1).isTrackSelected(0)).isTrue();
assertThat(trackGroupInfos.get(2).isTrackSelected(0)).isFalse();
assertThat(trackGroupInfos.get(2).isTrackSelected(1)).isTrue();
assertThat(trackGroupInfos.get(3).isTrackSelected(0)).isFalse();
assertThat(trackGroupInfos.get(0).getTrackType()).isEqualTo(TRACK_TYPE_AUDIO);
assertThat(trackGroupInfos.get(1).getTrackType()).isEqualTo(TRACK_TYPE_AUDIO);
assertThat(trackGroupInfos.get(2).getTrackType()).isEqualTo(TRACK_TYPE_VIDEO);
assertThat(trackGroupInfos.get(3).getTrackType()).isEqualTo(TRACK_TYPE_UNKNOWN);
assertThat(trackGroups.get(0).getTrackSupport(0)).isEqualTo(FORMAT_HANDLED);
assertThat(trackGroups.get(1).getTrackSupport(0)).isEqualTo(FORMAT_UNSUPPORTED_SUBTYPE);
assertThat(trackGroups.get(2).getTrackSupport(0)).isEqualTo(FORMAT_UNSUPPORTED_DRM);
assertThat(trackGroups.get(2).getTrackSupport(1)).isEqualTo(FORMAT_EXCEEDS_CAPABILITIES);
assertThat(trackGroups.get(3).getTrackSupport(0)).isEqualTo(FORMAT_UNSUPPORTED_TYPE);
assertThat(trackGroups.get(0).isTrackSelected(0)).isFalse();
assertThat(trackGroups.get(1).isTrackSelected(0)).isTrue();
assertThat(trackGroups.get(2).isTrackSelected(0)).isFalse();
assertThat(trackGroups.get(2).isTrackSelected(1)).isTrue();
assertThat(trackGroups.get(3).isTrackSelected(0)).isFalse();
assertThat(trackGroups.get(0).getType()).isEqualTo(TRACK_TYPE_AUDIO);
assertThat(trackGroups.get(1).getType()).isEqualTo(TRACK_TYPE_AUDIO);
assertThat(trackGroups.get(2).getType()).isEqualTo(TRACK_TYPE_VIDEO);
assertThat(trackGroups.get(3).getType()).isEqualTo(TRACK_TYPE_UNKNOWN);
}
@Test
@ -132,21 +131,21 @@ public class TrackSelectionUtilTest {
ImmutableList.of()
};
TracksInfo tracksInfo = TrackSelectionUtil.buildTracksInfo(mappedTrackInfo, selections);
Tracks tracks = TrackSelectionUtil.buildTracks(mappedTrackInfo, selections);
ImmutableList<TrackGroupInfo> trackGroupInfos = tracksInfo.getTrackGroupInfos();
assertThat(trackGroupInfos).hasSize(2);
assertThat(trackGroupInfos.get(0).getTrackGroup())
ImmutableList<Tracks.Group> trackGroups = tracks.getGroups();
assertThat(trackGroups).hasSize(2);
assertThat(trackGroups.get(0).getMediaTrackGroup())
.isEqualTo(mappedTrackInfo.getTrackGroups(0).get(0));
assertThat(trackGroupInfos.get(1).getTrackGroup())
assertThat(trackGroups.get(1).getMediaTrackGroup())
.isEqualTo(mappedTrackInfo.getTrackGroups(0).get(1));
assertThat(trackGroupInfos.get(0).getTrackSupport(0)).isEqualTo(FORMAT_HANDLED);
assertThat(trackGroupInfos.get(1).getTrackSupport(0)).isEqualTo(FORMAT_HANDLED);
assertThat(trackGroupInfos.get(1).getTrackSupport(1)).isEqualTo(FORMAT_HANDLED);
assertThat(trackGroupInfos.get(0).isTrackSelected(0)).isTrue();
assertThat(trackGroupInfos.get(1).isTrackSelected(0)).isFalse();
assertThat(trackGroupInfos.get(1).isTrackSelected(1)).isTrue();
assertThat(trackGroupInfos.get(0).getTrackType()).isEqualTo(TRACK_TYPE_AUDIO);
assertThat(trackGroupInfos.get(1).getTrackType()).isEqualTo(TRACK_TYPE_AUDIO);
assertThat(trackGroups.get(0).getTrackSupport(0)).isEqualTo(FORMAT_HANDLED);
assertThat(trackGroups.get(1).getTrackSupport(0)).isEqualTo(FORMAT_HANDLED);
assertThat(trackGroups.get(1).getTrackSupport(1)).isEqualTo(FORMAT_HANDLED);
assertThat(trackGroups.get(0).isTrackSelected(0)).isTrue();
assertThat(trackGroups.get(1).isTrackSelected(0)).isFalse();
assertThat(trackGroups.get(1).isTrackSelected(1)).isTrue();
assertThat(trackGroups.get(0).getType()).isEqualTo(TRACK_TYPE_AUDIO);
assertThat(trackGroups.get(1).getType()).isEqualTo(TRACK_TYPE_AUDIO);
}
}

View File

@ -763,7 +763,7 @@ public final class DefaultBandwidthMeterTest {
networkInfo.getType(), networkTypeOverride);
Shadows.shadowOf(telephonyManager).setTelephonyDisplayInfo(displayInfo);
}
// Create a sticky broadcast for the connectivity action because Roboletric isn't replying with
// Create a sticky broadcast for the connectivity action because Robolectric isn't replying with
// the current network state if a receiver for this intent is registered.
ApplicationProvider.getApplicationContext()
.sendStickyBroadcast(new Intent(ConnectivityManager.CONNECTIVITY_ACTION));

View File

@ -22,7 +22,7 @@ import androidx.media3.common.DrmInitData;
import androidx.media3.common.DrmInitData.SchemeData;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.datasource.DummyDataSource;
import androidx.media3.datasource.PlaceholderDataSource;
import androidx.media3.exoplayer.dash.manifest.AdaptationSet;
import androidx.media3.exoplayer.dash.manifest.BaseUrl;
import androidx.media3.exoplayer.dash.manifest.Period;
@ -43,28 +43,28 @@ public final class DashUtilTest {
@Test
public void loadDrmInitDataFromManifest() throws Exception {
Period period = newPeriod(newAdaptationSet(newRepresentation(newDrmInitData())));
Format format = DashUtil.loadFormatWithDrmInitData(DummyDataSource.INSTANCE, period);
Format format = DashUtil.loadFormatWithDrmInitData(PlaceholderDataSource.INSTANCE, period);
assertThat(format.drmInitData).isEqualTo(newDrmInitData());
}
@Test
public void loadDrmInitDataMissing() throws Exception {
Period period = newPeriod(newAdaptationSet(newRepresentation(null /* no init data */)));
Format format = DashUtil.loadFormatWithDrmInitData(DummyDataSource.INSTANCE, period);
Format format = DashUtil.loadFormatWithDrmInitData(PlaceholderDataSource.INSTANCE, period);
assertThat(format.drmInitData).isNull();
}
@Test
public void loadDrmInitDataNoRepresentations() throws Exception {
Period period = newPeriod(newAdaptationSet(/* no representation */ ));
Format format = DashUtil.loadFormatWithDrmInitData(DummyDataSource.INSTANCE, period);
Format format = DashUtil.loadFormatWithDrmInitData(PlaceholderDataSource.INSTANCE, period);
assertThat(format).isNull();
}
@Test
public void loadDrmInitDataNoAdaptationSets() throws Exception {
Period period = newPeriod(/* no adaptation set */ );
Format format = DashUtil.loadFormatWithDrmInitData(DummyDataSource.INSTANCE, period);
Format format = DashUtil.loadFormatWithDrmInitData(PlaceholderDataSource.INSTANCE, period);
assertThat(format).isNull();
}

View File

@ -31,7 +31,7 @@ import androidx.media3.common.MimeTypes;
import androidx.media3.common.StreamKey;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.DummyDataSource;
import androidx.media3.datasource.PlaceholderDataSource;
import androidx.media3.datasource.cache.Cache;
import androidx.media3.datasource.cache.CacheDataSource;
import androidx.media3.datasource.cache.NoOpCacheEvictor;
@ -86,7 +86,7 @@ public class DashDownloaderTest {
CacheDataSource.Factory cacheDataSourceFactory =
new CacheDataSource.Factory()
.setCache(Mockito.mock(Cache.class))
.setUpstreamDataSourceFactory(DummyDataSource.FACTORY);
.setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY);
DownloaderFactory factory =
new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run);
@ -96,7 +96,7 @@ public class DashDownloaderTest {
.setMimeType(MimeTypes.APPLICATION_MPD)
.setStreamKeys(
Collections.singletonList(
new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0)))
new StreamKey(/* groupIndex= */ 0, /* streamIndex= */ 0)))
.build());
assertThat(downloader).isInstanceOf(DashDownloader.class);
}

View File

@ -646,7 +646,8 @@ public final class HlsMediaPeriod
int numberOfVideoCodecs = Util.getCodecCountOfType(codecs, C.TRACK_TYPE_VIDEO);
int numberOfAudioCodecs = Util.getCodecCountOfType(codecs, C.TRACK_TYPE_AUDIO);
boolean codecsStringAllowsChunklessPreparation =
numberOfAudioCodecs <= 1
(numberOfAudioCodecs == 1
|| (numberOfAudioCodecs == 0 && multivariantPlaylist.audios.isEmpty()))
&& numberOfVideoCodecs <= 1
&& numberOfAudioCodecs + numberOfVideoCodecs > 0;
@C.TrackType

View File

@ -39,7 +39,7 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.StreamKey;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DummyDataSource;
import androidx.media3.datasource.PlaceholderDataSource;
import androidx.media3.datasource.cache.Cache;
import androidx.media3.datasource.cache.CacheDataSource;
import androidx.media3.datasource.cache.NoOpCacheEvictor;
@ -104,7 +104,7 @@ public class HlsDownloaderTest {
CacheDataSource.Factory cacheDataSourceFactory =
new CacheDataSource.Factory()
.setCache(Mockito.mock(Cache.class))
.setUpstreamDataSourceFactory(DummyDataSource.FACTORY);
.setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY);
DownloaderFactory factory =
new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run);
@ -114,7 +114,7 @@ public class HlsDownloaderTest {
.setMimeType(MimeTypes.APPLICATION_M3U8)
.setStreamKeys(
Collections.singletonList(
new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0)))
new StreamKey(/* groupIndex= */ 0, /* streamIndex= */ 0)))
.build());
assertThat(downloader).isInstanceOf(HlsDownloader.class);
}

View File

@ -84,12 +84,14 @@ import java.util.Map;
private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = MediaLibraryInfo.VERSION;
/**
* Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is
* the interval recommended by the IMA documentation.
* Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 200 ms is
* the interval recommended by the Media Rating Council (MRC) for minimum polling of viewable
* video impressions.
* http://www.mediaratingcouncil.org/063014%20Viewable%20Ad%20Impression%20Guideline_Final.pdf.
*
* @see VideoAdPlayer.VideoAdPlayerCallback
*/
private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100;
private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 200;
/** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */
private static final long IMA_DURATION_UNSET = -1L;
@ -708,7 +710,7 @@ import java.util.Map;
}
// Check for a selected track using an audio renderer.
return player.getCurrentTracksInfo().isTypeSelected(C.TRACK_TYPE_AUDIO) ? 100 : 0;
return player.getCurrentTracks().isTypeSelected(C.TRACK_TYPE_AUDIO) ? 100 : 0;
}
private void handleAdEvent(AdEvent adEvent) {

View File

@ -24,7 +24,7 @@ import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.Util;
@ -266,8 +266,8 @@ import androidx.media3.test.utils.StubExoPlayer;
}
@Override
public TracksInfo getCurrentTracksInfo() {
return TracksInfo.EMPTY;
public Tracks getCurrentTracks() {
return Tracks.EMPTY;
}
@Override

View File

@ -15,7 +15,10 @@
*/
package androidx.media3.exoplayer.rtsp;
import static androidx.media3.common.util.Assertions.checkArgument;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi;
@ -37,21 +40,37 @@ import java.util.Map;
public final class RtpPayloadFormat {
private static final String RTP_MEDIA_AC3 = "AC3";
private static final String RTP_MEDIA_AMR = "AMR";
private static final String RTP_MEDIA_AMR_WB = "AMR-WB";
private static final String RTP_MEDIA_MPEG4_GENERIC = "MPEG4-GENERIC";
private static final String RTP_MEDIA_MPEG4_VIDEO = "MP4V-ES";
private static final String RTP_MEDIA_H263_1998 = "H263-1998";
private static final String RTP_MEDIA_H263_2000 = "H263-2000";
private static final String RTP_MEDIA_H264 = "H264";
private static final String RTP_MEDIA_H265 = "H265";
private static final String RTP_MEDIA_PCM_L8 = "L8";
private static final String RTP_MEDIA_PCM_L16 = "L16";
private static final String RTP_MEDIA_PCMA = "PCMA";
private static final String RTP_MEDIA_PCMU = "PCMU";
private static final String RTP_MEDIA_VP8 = "VP8";
/** Returns whether the format of a {@link MediaDescription} is supported. */
public static boolean isFormatSupported(MediaDescription mediaDescription) {
switch (Ascii.toUpperCase(mediaDescription.rtpMapAttribute.mediaEncoding)) {
case RTP_MEDIA_AC3:
case RTP_MEDIA_AMR:
case RTP_MEDIA_AMR_WB:
case RTP_MEDIA_H263_1998:
case RTP_MEDIA_H263_2000:
case RTP_MEDIA_H264:
case RTP_MEDIA_H265:
case RTP_MEDIA_MPEG4_VIDEO:
case RTP_MEDIA_MPEG4_GENERIC:
case RTP_MEDIA_PCM_L8:
case RTP_MEDIA_PCM_L16:
case RTP_MEDIA_PCMA:
case RTP_MEDIA_PCMU:
case RTP_MEDIA_VP8:
return true;
default:
return false;
@ -69,6 +88,19 @@ public final class RtpPayloadFormat {
switch (Ascii.toUpperCase(mediaType)) {
case RTP_MEDIA_AC3:
return MimeTypes.AUDIO_AC3;
case RTP_MEDIA_AMR:
return MimeTypes.AUDIO_AMR_NB;
case RTP_MEDIA_AMR_WB:
return MimeTypes.AUDIO_AMR_WB;
case RTP_MEDIA_MPEG4_GENERIC:
return MimeTypes.AUDIO_AAC;
case RTP_MEDIA_PCM_L8:
case RTP_MEDIA_PCM_L16:
return MimeTypes.AUDIO_RAW;
case RTP_MEDIA_PCMA:
return MimeTypes.AUDIO_ALAW;
case RTP_MEDIA_PCMU:
return MimeTypes.AUDIO_MLAW;
case RTP_MEDIA_H263_1998:
case RTP_MEDIA_H263_2000:
return MimeTypes.VIDEO_H263;
@ -76,13 +108,24 @@ public final class RtpPayloadFormat {
return MimeTypes.VIDEO_H264;
case RTP_MEDIA_H265:
return MimeTypes.VIDEO_H265;
case RTP_MEDIA_MPEG4_GENERIC:
return MimeTypes.AUDIO_AAC;
case RTP_MEDIA_MPEG4_VIDEO:
return MimeTypes.VIDEO_MP4V;
case RTP_MEDIA_VP8:
return MimeTypes.VIDEO_VP8;
default:
throw new IllegalArgumentException(mediaType);
}
}
/** Returns the PCM encoding type for {@code mediaEncoding}. */
public static @C.PcmEncoding int getRawPcmEncodingType(String mediaEncoding) {
checkArgument(
mediaEncoding.equals(RTP_MEDIA_PCM_L8) || mediaEncoding.equals(RTP_MEDIA_PCM_L16));
return mediaEncoding.equals(RtpPayloadFormat.RTP_MEDIA_PCM_L8)
? C.ENCODING_PCM_8BIT
: C.ENCODING_PCM_16BIT_BIG_ENDIAN;
}
/** The payload type associated with this format. */
public final int rtpPayloadType;
/** The clock rate in Hertz, associated with the format. */

View File

@ -47,9 +47,14 @@ import java.security.NoSuchAlgorithmException;
/** HTTP digest authentication (RFC2069). */
public static final int DIGEST = 2;
private static final String DIGEST_FORMAT =
/** Basic authorization header format, see RFC7617. */
private static final String BASIC_AUTHORIZATION_HEADER_FORMAT = "Basic %s";
/** Digest authorization header format, see RFC7616. */
private static final String DIGEST_AUTHORIZATION_HEADER_FORMAT =
"Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"";
private static final String DIGEST_FORMAT_WITH_OPAQUE =
private static final String DIGEST_AUTHORIZATION_HEADER_FORMAT_WITH_OPAQUE =
"Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\","
+ " opaque=\"%s\"";
@ -109,9 +114,11 @@ import java.security.NoSuchAlgorithmException;
}
private String getBasicAuthorizationHeaderValue(RtspAuthUserInfo authUserInfo) {
return Base64.encodeToString(
RtspMessageUtil.getStringBytes(authUserInfo.username + ":" + authUserInfo.password),
Base64.DEFAULT);
return Util.formatInvariant(
BASIC_AUTHORIZATION_HEADER_FORMAT,
Base64.encodeToString(
RtspMessageUtil.getStringBytes(authUserInfo.username + ":" + authUserInfo.password),
Base64.DEFAULT));
}
private String getDigestAuthorizationHeaderValue(
@ -139,10 +146,16 @@ import java.security.NoSuchAlgorithmException;
if (opaque.isEmpty()) {
return Util.formatInvariant(
DIGEST_FORMAT, authUserInfo.username, realm, nonce, uri, response);
DIGEST_AUTHORIZATION_HEADER_FORMAT, authUserInfo.username, realm, nonce, uri, response);
} else {
return Util.formatInvariant(
DIGEST_FORMAT_WITH_OPAQUE, authUserInfo.username, realm, nonce, uri, response, opaque);
DIGEST_AUTHORIZATION_HEADER_FORMAT_WITH_OPAQUE,
authUserInfo.username,
realm,
nonce,
uri,
response,
opaque);
}
} catch (NoSuchAlgorithmException e) {
throw ParserException.createForManifestWithUnsupportedFeature(/* message= */ null, e);

View File

@ -25,6 +25,7 @@ import static androidx.media3.extractor.NalUnitUtil.NAL_START_CODE;
import android.net.Uri;
import android.util.Base64;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
@ -44,17 +45,61 @@ import com.google.common.collect.ImmutableMap;
// Format specific parameter names.
private static final String PARAMETER_PROFILE_LEVEL_ID = "profile-level-id";
private static final String PARAMETER_SPROP_PARAMS = "sprop-parameter-sets";
private static final String PARAMETER_AMR_OCTET_ALIGN = "octet-align";
private static final String PARAMETER_AMR_INTERLEAVING = "interleaving";
private static final String PARAMETER_H265_SPROP_SPS = "sprop-sps";
private static final String PARAMETER_H265_SPROP_PPS = "sprop-pps";
private static final String PARAMETER_H265_SPROP_VPS = "sprop-vps";
private static final String PARAMETER_H265_SPROP_MAX_DON_DIFF = "sprop-max-don-diff";
private static final String PARAMETER_MP4V_CONFIG = "config";
/** Prefix for the RFC6381 codecs string for AAC formats. */
private static final String AAC_CODECS_PREFIX = "mp4a.40.";
/** Prefix for the RFC6381 codecs string for AVC formats. */
private static final String H264_CODECS_PREFIX = "avc1.";
/** Prefix for the RFC6416 codecs string for MPEG4V-ES formats. */
private static final String MPEG4_CODECS_PREFIX = "mp4v.";
private static final String GENERIC_CONTROL_ATTR = "*";
/**
* Default height for MP4V.
*
* <p>RFC6416 does not mandate codec specific data (like width and height) in the fmtp attribute.
* These values are taken from <a
* href=https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/codec2/components/mpeg4_h263/C2SoftMpeg4Dec.cpp;l=130
* >Android's software MP4V decoder</a>.
*/
private static final int DEFAULT_MP4V_WIDTH = 352;
/**
* Default height for MP4V.
*
* <p>RFC6416 does not mandate codec specific data (like width and height) in the fmtp attribute.
* These values are taken from <a
* href=https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/codec2/components/mpeg4_h263/C2SoftMpeg4Dec.cpp;l=130
* >Android's software MP4V decoder</a>.
*/
private static final int DEFAULT_MP4V_HEIGHT = 288;
/**
* Default width for VP8.
*
* <p>RFC7741 never uses codec specific data (like width and height) in the fmtp attribute. These
* values are taken from <a
* href=https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/codec2/components/vpx/C2SoftVpxDec.cpp;drc=749a74cc3e081c16ea0e8c530953d0a247177867;l=70>Android's
* software VP8 decoder</a>.
*/
private static final int DEFAULT_VP8_WIDTH = 320;
/**
* Default height for VP8.
*
* <p>RFC7741 never uses codec specific data (like width and height) in the fmtp attribute. These
* values are taken from <a
* href=https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/codec2/components/vpx/C2SoftVpxDec.cpp;drc=749a74cc3e081c16ea0e8c530953d0a247177867;l=70>Android's
* software VP8 decoder</a>.
*/
private static final int DEFAULT_VP8_HEIGHT = 240;
/** Default width and height for H263. */
private static final int DEFAULT_H263_WIDTH = 352;
@ -106,8 +151,9 @@ import com.google.common.collect.ImmutableMap;
}
int rtpPayloadType = mediaDescription.rtpMapAttribute.payloadType;
String mediaEncoding = mediaDescription.rtpMapAttribute.mediaEncoding;
String mimeType = getMimeTypeFromRtpMediaType(mediaDescription.rtpMapAttribute.mediaEncoding);
String mimeType = getMimeTypeFromRtpMediaType(mediaEncoding);
formatBuilder.setSampleMimeType(mimeType);
int clockRate = mediaDescription.rtpMapAttribute.clockRate;
@ -125,6 +171,23 @@ import com.google.common.collect.ImmutableMap;
checkArgument(!fmtpParameters.isEmpty());
processAacFmtpAttribute(formatBuilder, fmtpParameters, channelCount, clockRate);
break;
case MimeTypes.AUDIO_AMR_NB:
case MimeTypes.AUDIO_AMR_WB:
checkArgument(channelCount == 1, "Multi channel AMR is not currently supported.");
checkArgument(
!fmtpParameters.isEmpty(),
"fmtp parameters must include " + PARAMETER_AMR_OCTET_ALIGN + ".");
checkArgument(
fmtpParameters.containsKey(PARAMETER_AMR_OCTET_ALIGN),
"Only octet aligned mode is currently supported.");
checkArgument(
!fmtpParameters.containsKey(PARAMETER_AMR_INTERLEAVING),
"Interleaving mode is not currently supported.");
break;
case MimeTypes.VIDEO_MP4V:
checkArgument(!fmtpParameters.isEmpty());
processMPEG4FmtpAttribute(formatBuilder, fmtpParameters);
break;
case MimeTypes.VIDEO_H263:
// H263 does not require a FMTP attribute. So Setting default width and height.
formatBuilder.setWidth(DEFAULT_H263_WIDTH).setHeight(DEFAULT_H263_HEIGHT);
@ -137,8 +200,18 @@ import com.google.common.collect.ImmutableMap;
checkArgument(!fmtpParameters.isEmpty());
processH265FmtpAttribute(formatBuilder, fmtpParameters);
break;
case MimeTypes.VIDEO_VP8:
// VP8 never uses fmtp width and height attributes (RFC7741 Section 6.2), setting default
// width and height.
formatBuilder.setWidth(DEFAULT_VP8_WIDTH).setHeight(DEFAULT_VP8_HEIGHT);
break;
case MimeTypes.AUDIO_RAW:
formatBuilder.setPcmEncoding(RtpPayloadFormat.getRawPcmEncodingType(mediaEncoding));
break;
case MimeTypes.AUDIO_AC3:
// AC3 does not require a FMTP attribute. Fall through.
case MimeTypes.AUDIO_ALAW:
case MimeTypes.AUDIO_MLAW:
// Does not require a fmtp attribute. Fall through.
default:
// Do nothing.
}
@ -177,6 +250,23 @@ import com.google.common.collect.ImmutableMap;
AacUtil.buildAacLcAudioSpecificConfig(sampleRate, channelCount)));
}
private static void processMPEG4FmtpAttribute(
Format.Builder formatBuilder, ImmutableMap<String, String> fmtpAttributes) {
@Nullable String configInput = fmtpAttributes.get(PARAMETER_MP4V_CONFIG);
if (configInput != null) {
byte[] configBuffer = Util.getBytesFromHexString(configInput);
formatBuilder.setInitializationData(ImmutableList.of(configBuffer));
Pair<Integer, Integer> resolution =
CodecSpecificDataUtil.getVideoResolutionFromMpeg4VideoConfig(configBuffer);
formatBuilder.setWidth(resolution.first).setHeight(resolution.second);
} else {
// set the default width and height
formatBuilder.setWidth(DEFAULT_MP4V_WIDTH).setHeight(DEFAULT_MP4V_HEIGHT);
}
@Nullable String profileLevel = fmtpAttributes.get(PARAMETER_PROFILE_LEVEL_ID);
formatBuilder.setCodecs(MPEG4_CODECS_PREFIX + (profileLevel == null ? "1" : profileLevel));
}
/** Returns H264/H265 initialization data from the RTP parameter set. */
private static byte[] getInitializationDataFromParameterSet(String parameterSet) {
byte[] decodedParameterNalData = Base64.decode(parameterSet, Base64.DEFAULT);

View File

@ -459,6 +459,21 @@ import java.util.regex.Pattern;
"Invalid WWW-Authenticate header " + headerValue, /* cause= */ null);
}
/**
* Throws {@link ParserException#createForMalformedManifest ParserException} if {@code expression}
* evaluates to false.
*
* @param expression The expression to evaluate.
* @param message The error message.
* @throws ParserException If {@code expression} is false.
*/
public static void checkManifestExpression(boolean expression, @Nullable String message)
throws ParserException {
if (!expression) {
throw ParserException.createForMalformedManifest(message, /* cause= */ null);
}
}
private static String getRtspStatusReasonPhrase(int statusCode) {
switch (statusCode) {
case 200:

View File

@ -15,8 +15,8 @@
*/
package androidx.media3.exoplayer.rtsp;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.exoplayer.rtsp.RtspMessageUtil.checkManifestExpression;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
@ -38,8 +38,9 @@ import java.util.regex.Pattern;
new RtspSessionTiming(/* startTimeMs= */ 0, /* stopTimeMs= */ C.TIME_UNSET);
// We only support npt=xxx-[xxx], but not npt=-xxx. See RFC2326 Section 3.6.
// Supports both npt= and npt: identifier.
private static final Pattern NPT_RANGE_PATTERN =
Pattern.compile("npt=([.\\d]+|now)\\s?-\\s?([.\\d]+)?");
Pattern.compile("npt[:=]([.\\d]+|now)\\s?-\\s?([.\\d]+)?");
private static final String START_TIMING_NTP_FORMAT = "npt=%.3f-";
private static final long LIVE_START_TIME = 0;
@ -49,10 +50,11 @@ import java.util.regex.Pattern;
long startTimeMs;
long stopTimeMs;
Matcher matcher = NPT_RANGE_PATTERN.matcher(sdpRangeAttribute);
checkArgument(matcher.matches());
checkManifestExpression(matcher.matches(), /* message= */ sdpRangeAttribute);
String startTimeString = checkNotNull(matcher.group(1));
if (startTimeString.equals("now")) {
@Nullable String startTimeString = matcher.group(1);
checkManifestExpression(startTimeString != null, /* message= */ sdpRangeAttribute);
if (castNonNull(startTimeString).equals("now")) {
startTimeMs = LIVE_START_TIME;
} else {
startTimeMs = (long) (Float.parseFloat(startTimeString) * C.MILLIS_PER_SECOND);
@ -65,7 +67,7 @@ import java.util.regex.Pattern;
} catch (NumberFormatException e) {
throw ParserException.createForMalformedManifest(stopTimeString, e);
}
checkArgument(stopTimeMs > startTimeMs);
checkManifestExpression(stopTimeMs >= startTimeMs, /* message= */ sdpRangeAttribute);
} else {
stopTimeMs = C.TIME_UNSET;
}

View File

@ -36,12 +36,23 @@ import androidx.media3.exoplayer.rtsp.RtpPayloadFormat;
return new RtpAc3Reader(payloadFormat);
case MimeTypes.AUDIO_AAC:
return new RtpAacReader(payloadFormat);
case MimeTypes.AUDIO_AMR_NB:
case MimeTypes.AUDIO_AMR_WB:
return new RtpAmrReader(payloadFormat);
case MimeTypes.AUDIO_RAW:
case MimeTypes.AUDIO_ALAW:
case MimeTypes.AUDIO_MLAW:
return new RtpPcmReader(payloadFormat);
case MimeTypes.VIDEO_H263:
return new RtpH263Reader(payloadFormat);
case MimeTypes.VIDEO_H264:
return new RtpH264Reader(payloadFormat);
case MimeTypes.VIDEO_H265:
return new RtpH265Reader(payloadFormat);
case MimeTypes.VIDEO_MP4V:
return new RtpMpeg4Reader(payloadFormat);
case MimeTypes.VIDEO_VP8:
return new RtpVp8Reader(payloadFormat);
default:
// No supported reader, returning null.
}

View File

@ -0,0 +1,196 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.rtsp.reader;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import androidx.media3.common.C;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.rtsp.RtpPacket;
import androidx.media3.exoplayer.rtsp.RtpPayloadFormat;
import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.TrackOutput;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Parses an AMR byte stream carried on RTP packets and extracts individual samples. Interleaving
* mode is not supported. Refer to RFC4867 for more details.
*/
/* package */ final class RtpAmrReader implements RtpPayloadReader {
private static final String TAG = "RtpAmrReader";
/**
* The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR-NB
* (narrow band). AMR-NB supports eight narrow band speech encoding modes with bit rates between
* 4.75 and 12.2 kbps defined in RFC4867 Section 3.1. Refer to table 1a in 3GPP TS 26.101 for the
* mapping definition.
*/
private static final int[] AMR_NB_FRAME_TYPE_INDEX_TO_FRAME_SIZE = {
13, // 4.75kbps
14, // 5.15kbps
16, // 5.90kbps
18, // 6.70kbps PDC-EFR
20, // 7.40kbps TDMA-EFR
21, // 7.95kbps
27, // 10.2kbps
32, // 12.2kbps GSM-EFR
6, // AMR SID
7, // GSM-EFR SID
6, // TDMA-EFR SID
6, // PDC-EFR SID
1, // Future use
1, // Future use
1, // Future use
1 // No data
};
/**
* The frame size in bytes, including header (1 byte), for each of the 16 frame types for AMR-WB
* (wide band). AMR-WB supports nine wide band speech encoding modes with bit rates between 6.6 to
* 23.85 kbps defined in RFC4867 Section 3.2. Refer to table 1a in 3GPP TS 26.201. for the mapping
* definition.
*/
private static final int[] AMR_WB_FRAME_TYPE_INDEX_TO_FRAME_SIZE = {
18, // 6.60kbps
24, // 8.85kbps
33, // 12.65kbps
37, // 14.25kbps
41, // 15.85kbps
47, // 18.25kbps
51, // 19.85kbps
59, // 23.05kbps
61, // 23.85kbps
6, // AMR-WB SID
1, // Future use
1, // Future use
1, // Future use
1, // Future use
1, // speech lost
1 // No data
};
private final RtpPayloadFormat payloadFormat;
private final boolean isWideBand;
private final int sampleRate;
private @MonotonicNonNull TrackOutput trackOutput;
private long firstReceivedTimestamp;
private long startTimeOffsetUs;
private int previousSequenceNumber;
public RtpAmrReader(RtpPayloadFormat payloadFormat) {
this.payloadFormat = payloadFormat;
this.isWideBand =
MimeTypes.AUDIO_AMR_WB.equals(checkNotNull(payloadFormat.format.sampleMimeType));
this.sampleRate = payloadFormat.clockRate;
this.firstReceivedTimestamp = C.TIME_UNSET;
this.previousSequenceNumber = C.INDEX_UNSET;
// Start time offset must be 0 before the first seek.
this.startTimeOffsetUs = 0;
}
// RtpPayloadReader implementation.
@Override
public void createTracks(ExtractorOutput extractorOutput, int trackId) {
trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_AUDIO);
trackOutput.format(payloadFormat.format);
}
@Override
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {
this.firstReceivedTimestamp = timestamp;
}
@Override
public void consume(
ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) {
checkStateNotNull(trackOutput);
// Check that this packet is in the sequence of the previous packet.
if (previousSequenceNumber != C.INDEX_UNSET) {
int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber);
if (sequenceNumber != expectedSequenceNumber) {
Log.w(
TAG,
Util.formatInvariant(
"Received RTP packet with unexpected sequence number. Expected: %d; received: %d.",
expectedSequenceNumber, sequenceNumber));
}
}
//
// AMR as RTP payload (RFC4867 Section 4.2).
//
// +----------------+-------------------+----------------
// | payload header | table of contents | speech data ...
// +----------------+-------------------+----------------
//
// Payload header (RFC4867 Section 4.4.1).
//
// The header won't contain ILL and ILP, as interleaving is not currently supported.
// +-+-+-+-+-+-+-+- - - - - - - -
// | CMR |R|R|R|R| ILL | ILP |
// +-+-+-+-+-+-+-+- - - - - - - -
//
// Skip CMR and reserved bits.
data.skipBytes(1);
// Loop over sampleSize to send multiple frames along with appropriate timestamp when compound
// payload support is added.
int frameType = (data.peekUnsignedByte() >> 3) & 0x0f;
int frameSize = getFrameSize(frameType, isWideBand);
int sampleSize = data.bytesLeft();
checkArgument(sampleSize == frameSize, "compound payload not supported currently");
trackOutput.sampleData(data, sampleSize);
long sampleTimeUs =
toSampleTimeUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp, sampleRate);
trackOutput.sampleMetadata(
sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, /* offset= */ 0, /* cryptoData= */ null);
previousSequenceNumber = sequenceNumber;
}
@Override
public void seek(long nextRtpTimestamp, long timeUs) {
firstReceivedTimestamp = nextRtpTimestamp;
startTimeOffsetUs = timeUs;
}
// Internal methods.
public static int getFrameSize(int frameType, boolean isWideBand) {
checkArgument(
// Valid frame types are defined in RFC4867 Section 4.3.1.
(frameType >= 0 && frameType <= 8) || frameType == 15,
"Illegal AMR " + (isWideBand ? "WB" : "NB") + " frame type " + frameType);
return isWideBand
? AMR_WB_FRAME_TYPE_INDEX_TO_FRAME_SIZE[frameType]
: AMR_NB_FRAME_TYPE_INDEX_TO_FRAME_SIZE[frameType];
}
/** Returns the correct sample time from RTP timestamp, accounting for the AMR sampling rate. */
private static long toSampleTimeUs(
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp, int sampleRate) {
return startTimeOffsetUs
+ Util.scaleLargeTimestamp(
rtpTimestamp - firstReceivedRtpTimestamp,
/* multiplier= */ C.MICROS_PER_SECOND,
/* divisor= */ sampleRate);
}
}

View File

@ -0,0 +1,150 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.rtsp.reader;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import androidx.media3.common.C;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.rtsp.RtpPacket;
import androidx.media3.exoplayer.rtsp.RtpPayloadFormat;
import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.TrackOutput;
import com.google.common.primitives.Bytes;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Parses an MPEG4 byte stream carried on RTP packets, and extracts MPEG4 Access Units. Refer to
* RFC6416 for more details.
*/
@UnstableApi
/* package */ final class RtpMpeg4Reader implements RtpPayloadReader {
private static final String TAG = "RtpMpeg4Reader";
private static final long MEDIA_CLOCK_FREQUENCY = 90_000;
/** VOP (Video Object Plane) unit type. */
private static final int I_VOP = 0;
private final RtpPayloadFormat payloadFormat;
private @MonotonicNonNull TrackOutput trackOutput;
private @C.BufferFlags int bufferFlags;
/**
* First received RTP timestamp. All RTP timestamps are dimension-less, the time base is defined
* by {@link #MEDIA_CLOCK_FREQUENCY}.
*/
private long firstReceivedTimestamp;
private int previousSequenceNumber;
private long startTimeOffsetUs;
private int sampleLength;
/** Creates an instance. */
public RtpMpeg4Reader(RtpPayloadFormat payloadFormat) {
this.payloadFormat = payloadFormat;
firstReceivedTimestamp = C.TIME_UNSET;
previousSequenceNumber = C.INDEX_UNSET;
sampleLength = 0;
}
@Override
public void createTracks(ExtractorOutput extractorOutput, int trackId) {
trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_VIDEO);
castNonNull(trackOutput).format(payloadFormat.format);
}
@Override
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {}
@Override
public void consume(
ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) {
checkStateNotNull(trackOutput);
// Check that this packet is in the sequence of the previous packet.
if (previousSequenceNumber != C.INDEX_UNSET) {
int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber);
if (sequenceNumber != expectedSequenceNumber) {
Log.w(
TAG,
Util.formatInvariant(
"Received RTP packet with unexpected sequence number. Expected: %d; received: %d."
+ " Dropping packet.",
expectedSequenceNumber, sequenceNumber));
}
}
// Parse VOP Type and get the buffer flags
int limit = data.bytesLeft();
trackOutput.sampleData(data, limit);
if (sampleLength == 0) {
bufferFlags = getBufferFlagsFromVop(data);
}
sampleLength += limit;
// RTP marker indicates the last packet carrying a VOP.
if (rtpMarker) {
if (firstReceivedTimestamp == C.TIME_UNSET) {
firstReceivedTimestamp = timestamp;
}
long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
trackOutput.sampleMetadata(timeUs, bufferFlags, sampleLength, 0, null);
sampleLength = 0;
}
previousSequenceNumber = sequenceNumber;
}
@Override
public void seek(long nextRtpTimestamp, long timeUs) {
firstReceivedTimestamp = nextRtpTimestamp;
startTimeOffsetUs = timeUs;
sampleLength = 0;
}
// Internal methods.
/**
* Returns VOP (Video Object Plane) Coding type.
*
* <p>Sets {@link #bufferFlags} according to the VOP Coding type.
*/
private static @C.BufferFlags int getBufferFlagsFromVop(ParsableByteArray data) {
// search for VOP_START_CODE (00 00 01 B6)
byte[] inputData = data.getData();
byte[] startCode = new byte[] {0x0, 0x0, 0x1, (byte) 0xB6};
int vopStartCodePos = Bytes.indexOf(inputData, startCode);
if (vopStartCodePos != -1) {
data.setPosition(vopStartCodePos + 4);
int vopType = data.peekUnsignedByte() >> 6;
return vopType == I_VOP ? C.BUFFER_FLAG_KEY_FRAME : 0;
}
return 0;
}
private static long toSampleUs(
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) {
return startTimeOffsetUs
+ Util.scaleLargeTimestamp(
(rtpTimestamp - firstReceivedRtpTimestamp),
/* multiplier= */ C.MICROS_PER_SECOND,
/* divisor= */ MEDIA_CLOCK_FREQUENCY);
}
}

View File

@ -0,0 +1,106 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.rtsp.reader;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.util.Log;
import androidx.media3.common.C;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.rtsp.RtpPacket;
import androidx.media3.exoplayer.rtsp.RtpPayloadFormat;
import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.TrackOutput;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Parses byte stream carried on RTP packets, and extracts PCM frames. Refer to RFC3551 for more
* details.
*/
@UnstableApi
/* package */ public final class RtpPcmReader implements RtpPayloadReader {
private static final String TAG = "RtpPcmReader";
private final RtpPayloadFormat payloadFormat;
private @MonotonicNonNull TrackOutput trackOutput;
private long firstReceivedTimestamp;
private long startTimeOffsetUs;
private int previousSequenceNumber;
public RtpPcmReader(RtpPayloadFormat payloadFormat) {
this.payloadFormat = payloadFormat;
firstReceivedTimestamp = C.TIME_UNSET;
// Start time offset must be 0 before the first seek.
startTimeOffsetUs = 0;
previousSequenceNumber = C.INDEX_UNSET;
}
@Override
public void createTracks(ExtractorOutput extractorOutput, int trackId) {
trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_AUDIO);
trackOutput.format(payloadFormat.format);
}
@Override
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {
firstReceivedTimestamp = timestamp;
}
@Override
public void consume(
ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) {
checkNotNull(trackOutput);
if (previousSequenceNumber != C.INDEX_UNSET) {
int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber);
if (sequenceNumber != expectedSequenceNumber) {
Log.w(
TAG,
Util.formatInvariant(
"Received RTP packet with unexpected sequence number. Expected: %d; received: %d.",
expectedSequenceNumber, sequenceNumber));
}
}
long sampleTimeUs =
toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp, payloadFormat.clockRate);
int size = data.bytesLeft();
trackOutput.sampleData(data, size);
trackOutput.sampleMetadata(
sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* cryptoData= */ null);
previousSequenceNumber = sequenceNumber;
}
@Override
public void seek(long nextRtpTimestamp, long timeUs) {
// TODO(b/198620566) Rename firstReceivedTimestamp to timestampBase for all RtpPayloadReaders.
firstReceivedTimestamp = nextRtpTimestamp;
startTimeOffsetUs = timeUs;
}
/** Returns the correct sample time from RTP timestamp, accounting for the given clock rate. */
private static long toSampleUs(
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp, int clockRate) {
return startTimeOffsetUs
+ Util.scaleLargeTimestamp(
rtpTimestamp - firstReceivedRtpTimestamp,
/* multiplier= */ C.MICROS_PER_SECOND,
/* divisor= */ clockRate);
}
}

View File

@ -0,0 +1,206 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.rtsp.reader;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import androidx.media3.common.C;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.rtsp.RtpPacket;
import androidx.media3.exoplayer.rtsp.RtpPayloadFormat;
import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.TrackOutput;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Parses a VP8 byte stream carried on RTP packets, and extracts VP8 individual video frames as
* defined in RFC7741.
*/
/* package */ final class RtpVp8Reader implements RtpPayloadReader {
private static final String TAG = "RtpVP8Reader";
/** VP9 uses a 90 KHz media clock (RFC7741 Section 4.1). */
private static final long MEDIA_CLOCK_FREQUENCY = 90_000;
private final RtpPayloadFormat payloadFormat;
private @MonotonicNonNull TrackOutput trackOutput;
/**
* First received RTP timestamp. All RTP timestamps are dimension-less, the time base is defined
* by {@link #MEDIA_CLOCK_FREQUENCY}.
*/
private long firstReceivedTimestamp;
private int previousSequenceNumber;
/** The combined size of a sample that is fragmented into multiple RTP packets. */
private int fragmentedSampleSizeBytes;
private long startTimeOffsetUs;
/**
* Whether the first packet of one VP8 frame is received. A VP8 frame can be split into two RTP
* packets.
*/
private boolean gotFirstPacketOfVp8Frame;
private boolean isKeyFrame;
private boolean isOutputFormatSet;
/** Creates an instance. */
public RtpVp8Reader(RtpPayloadFormat payloadFormat) {
this.payloadFormat = payloadFormat;
firstReceivedTimestamp = C.TIME_UNSET;
previousSequenceNumber = C.INDEX_UNSET;
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
// The start time offset must be 0 until the first seek.
startTimeOffsetUs = 0;
gotFirstPacketOfVp8Frame = false;
isKeyFrame = false;
isOutputFormatSet = false;
}
@Override
public void createTracks(ExtractorOutput extractorOutput, int trackId) {
trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_VIDEO);
trackOutput.format(payloadFormat.format);
}
@Override
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {}
@Override
public void consume(
ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) {
checkStateNotNull(trackOutput);
boolean isValidVP8Descriptor = validateVp8Descriptor(data, sequenceNumber);
if (isValidVP8Descriptor) {
// VP8 Payload Header is defined in RFC7741 Section 4.3.
if (fragmentedSampleSizeBytes == C.LENGTH_UNSET && gotFirstPacketOfVp8Frame) {
isKeyFrame = (data.peekUnsignedByte() & 0x01) == 0;
}
if (!isOutputFormatSet) {
// Parsing frame data to get width and height, RFC6386 Section 19.1.
int currPosition = data.getPosition();
// Skips the frame_tag and start_code.
data.setPosition(currPosition + 6);
// RFC6386 Section 19.1 specifically uses little endian.
int width = data.readLittleEndianUnsignedShort() & 0x3fff;
int height = data.readLittleEndianUnsignedShort() & 0x3fff;
data.setPosition(currPosition);
if (width != payloadFormat.format.width || height != payloadFormat.format.height) {
trackOutput.format(
payloadFormat.format.buildUpon().setWidth(width).setHeight(height).build());
}
isOutputFormatSet = true;
}
int fragmentSize = data.bytesLeft();
trackOutput.sampleData(data, fragmentSize);
fragmentedSampleSizeBytes += fragmentSize;
if (rtpMarker) {
if (firstReceivedTimestamp == C.TIME_UNSET) {
firstReceivedTimestamp = timestamp;
}
long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
trackOutput.sampleMetadata(
timeUs,
isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
fragmentedSampleSizeBytes,
/* offset= */ 0,
/* cryptoData= */ null);
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
gotFirstPacketOfVp8Frame = false;
}
previousSequenceNumber = sequenceNumber;
}
}
@Override
public void seek(long nextRtpTimestamp, long timeUs) {
firstReceivedTimestamp = nextRtpTimestamp;
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
startTimeOffsetUs = timeUs;
}
/**
* Returns {@code true} and sets the {@link ParsableByteArray#getPosition() payload.position} to
* the end of the descriptor, if a valid VP8 descriptor is present.
*/
private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSequenceNumber) {
// VP8 Payload Descriptor is defined in RFC7741 Section 4.2.
int header = payload.readUnsignedByte();
if (!gotFirstPacketOfVp8Frame) {
// TODO(b/198620566) Consider using ParsableBitArray.
// For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2.
if ((header & 0x10) != 0x1 || (header & 0x07) != 0) {
Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping.");
return false;
}
gotFirstPacketOfVp8Frame = true;
} else {
// Check that this packet is in the sequence of the previous packet.
int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber);
if (packetSequenceNumber != expectedSequenceNumber) {
Log.w(
TAG,
Util.formatInvariant(
"Received RTP packet with unexpected sequence number. Expected: %d; received: %d."
+ " Dropping packet.",
expectedSequenceNumber, packetSequenceNumber));
return false;
}
}
// Check if optional X header is present.
if ((header & 0x80) != 0) {
int xHeader = payload.readUnsignedByte();
// Check if optional I header is present.
if ((xHeader & 0x80) != 0) {
int iHeader = payload.readUnsignedByte();
// Check if I header's M bit is present.
if ((iHeader & 0x80) != 0) {
payload.skipBytes(1);
}
}
// Check if optional L header is present.
if ((xHeader & 0x40) != 0) {
payload.skipBytes(1);
}
// Check if optional T or K header(s) is present.
if ((xHeader & 0x20) != 0 || (xHeader & 0x10) != 0) {
payload.skipBytes(1);
}
}
return true;
}
private static long toSampleUs(
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) {
return startTimeOffsetUs
+ Util.scaleLargeTimestamp(
(rtpTimestamp - firstReceivedRtpTimestamp),
/* multiplier= */ C.MICROS_PER_SECOND,
/* divisor= */ MEDIA_CLOCK_FREQUENCY);
}
}

View File

@ -33,7 +33,7 @@ public class RtspAuthenticationInfoTest {
String authenticationRealm = "WallyWorld";
String username = "Aladdin";
String password = "open sesame";
String expectedAuthorizationHeaderValue = "QWxhZGRpbjpvcGVuIHNlc2FtZQ==\n";
String expectedAuthorizationHeaderValue = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==\n";
RtspAuthenticationInfo authenticator =
new RtspAuthenticationInfo(
RtspAuthenticationInfo.BASIC, authenticationRealm, /* nonce= */ "", /* opaque= */ "");

View File

@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.media3.common.C;
import androidx.media3.common.ParserException;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -55,8 +56,14 @@ public class RtspSessionTimingTest {
}
@Test
public void parseTiming_withInvalidRangeTiming_throwsIllegalArgumentException() {
assertThrows(
IllegalArgumentException.class, () -> RtspSessionTiming.parseTiming("npt=10.000-2.054"));
public void parseTiming_withRangeTimingAndColonSeparator() throws Exception {
RtspSessionTiming sessionTiming = RtspSessionTiming.parseTiming("npt:0.000-32.054");
assertThat(sessionTiming.getDurationMs()).isEqualTo(32054);
assertThat(sessionTiming.isLive()).isFalse();
}
@Test
public void parseTiming_withInvalidRangeTiming_throwsParserException() {
assertThrows(ParserException.class, () -> RtspSessionTiming.parseTiming("npt=10.000-2.054"));
}
}

View File

@ -0,0 +1,183 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.rtsp.reader;
import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.exoplayer.rtsp.RtpPacket;
import androidx.media3.exoplayer.rtsp.RtpPayloadFormat;
import androidx.media3.test.utils.FakeExtractorOutput;
import androidx.media3.test.utils.FakeTrackOutput;
import androidx.media3.test.utils.TestUtil;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableMap;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtpPcmReader}. */
@RunWith(AndroidJUnit4.class)
public final class RtpPcmReaderTest {
// A typical RTP payload type for audio.
private static final int RTP_PAYLOAD_TYPE = 97;
private static final byte[] FRAME_1_PAYLOAD = TestUtil.buildTestData(/* length= */ 4);
private static final byte[] FRAME_2_PAYLOAD = TestUtil.buildTestData(/* length= */ 4);
private static final RtpPacket PACKET_1 =
createRtpPacket(/* timestamp= */ 2599168056L, /* sequenceNumber= */ 40289, FRAME_1_PAYLOAD);
private static final RtpPacket PACKET_2 =
createRtpPacket(/* timestamp= */ 2599169592L, /* sequenceNumber= */ 40290, FRAME_2_PAYLOAD);
private ParsableByteArray packetData;
private FakeExtractorOutput extractorOutput;
private RtpPcmReader pcmReader;
@Before
public void setUp() {
packetData = new ParsableByteArray();
extractorOutput = new FakeExtractorOutput();
}
@Test
public void consume_twoDualChannelWav8bitPackets() {
pcmReader =
new RtpPcmReader(
new RtpPayloadFormat(
new Format.Builder()
.setChannelCount(2)
.setSampleMimeType(MimeTypes.AUDIO_WAV)
.setPcmEncoding(C.ENCODING_PCM_8BIT)
.setSampleRate(48_000)
.build(),
/* rtpPayloadType= */ RTP_PAYLOAD_TYPE,
/* clockRate= */ 48_000,
/* fmtpParameters= */ ImmutableMap.of()));
pcmReader.createTracks(extractorOutput, /* trackId= */ 0);
pcmReader.onReceivingFirstPacket(PACKET_1.timestamp, PACKET_1.sequenceNumber);
consume(PACKET_1);
consume(PACKET_2);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(FRAME_1_PAYLOAD);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_PAYLOAD);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(32000);
}
@Test
public void consume_twoSingleChannelWav16bitPackets() {
pcmReader =
new RtpPcmReader(
new RtpPayloadFormat(
new Format.Builder()
.setChannelCount(1)
.setSampleMimeType(MimeTypes.AUDIO_WAV)
.setPcmEncoding(C.ENCODING_PCM_16BIT_BIG_ENDIAN)
.setSampleRate(60_000)
.build(),
/* rtpPayloadType= */ RTP_PAYLOAD_TYPE,
/* clockRate= */ 60_000,
/* fmtpParameters= */ ImmutableMap.of()));
pcmReader.createTracks(extractorOutput, /* trackId= */ 0);
pcmReader.onReceivingFirstPacket(PACKET_1.timestamp, PACKET_1.sequenceNumber);
consume(PACKET_1);
consume(PACKET_2);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(FRAME_1_PAYLOAD);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_PAYLOAD);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(25600);
}
@Test
public void consume_twoDualChannelAlawPackets() {
pcmReader =
new RtpPcmReader(
new RtpPayloadFormat(
new Format.Builder()
.setChannelCount(2)
.setSampleMimeType(MimeTypes.AUDIO_ALAW)
.setSampleRate(16_000)
.build(),
/* rtpPayloadType= */ RTP_PAYLOAD_TYPE,
/* clockRate= */ 16_000,
/* fmtpParameters= */ ImmutableMap.of()));
pcmReader.createTracks(extractorOutput, /* trackId= */ 0);
pcmReader.onReceivingFirstPacket(PACKET_1.timestamp, PACKET_1.sequenceNumber);
consume(PACKET_1);
consume(PACKET_2);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(FRAME_1_PAYLOAD);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_PAYLOAD);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(96000);
}
@Test
public void consume_twoDualChannelMlawPackets() {
pcmReader =
new RtpPcmReader(
new RtpPayloadFormat(
new Format.Builder()
.setChannelCount(2)
.setSampleMimeType(MimeTypes.AUDIO_MLAW)
.setSampleRate(24_000)
.build(),
/* rtpPayloadType= */ RTP_PAYLOAD_TYPE,
/* clockRate= */ 24_000,
/* fmtpParameters= */ ImmutableMap.of()));
pcmReader.createTracks(extractorOutput, /* trackId= */ 0);
pcmReader.onReceivingFirstPacket(PACKET_1.timestamp, PACKET_1.sequenceNumber);
consume(PACKET_1);
consume(PACKET_2);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(FRAME_1_PAYLOAD);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_PAYLOAD);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(64000);
}
private static RtpPacket createRtpPacket(long timestamp, int sequenceNumber, byte[] payloadData) {
return new RtpPacket.Builder()
.setTimestamp(timestamp)
.setSequenceNumber(sequenceNumber)
// RFC3551 Section 4.1.
.setMarker(false)
.setPayloadData(payloadData)
.build();
}
private void consume(RtpPacket frame) {
packetData.reset(frame.payloadData);
pcmReader.consume(packetData, frame.timestamp, frame.sequenceNumber, frame.marker);
}
}

View File

@ -20,7 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.StreamKey;
import androidx.media3.datasource.DummyDataSource;
import androidx.media3.datasource.PlaceholderDataSource;
import androidx.media3.datasource.cache.Cache;
import androidx.media3.datasource.cache.CacheDataSource;
import androidx.media3.exoplayer.offline.DefaultDownloaderFactory;
@ -42,7 +42,7 @@ public final class SsDownloaderTest {
CacheDataSource.Factory cacheDataSourceFactory =
new CacheDataSource.Factory()
.setCache(Mockito.mock(Cache.class))
.setUpstreamDataSourceFactory(DummyDataSource.FACTORY);
.setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY);
DownloaderFactory factory =
new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run);
@ -52,7 +52,7 @@ public final class SsDownloaderTest {
.setMimeType(MimeTypes.APPLICATION_SS)
.setStreamKeys(
Collections.singletonList(
new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0)))
new StreamKey(/* groupIndex= */ 0, /* streamIndex= */ 0)))
.build());
assertThat(downloader).isInstanceOf(SsDownloader.class);
}

View File

@ -1116,6 +1116,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Nullable String codecs = null;
@Nullable byte[] projectionData = null;
@C.StereoMode int stereoMode = Format.NO_VALUE;
@Nullable EsdsData esdsData = null;
// HDR related metadata.
@C.ColorSpace int colorSpace = Format.NO_VALUE;
@ -1168,6 +1169,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} else if (childAtomType == Atom.TYPE_av1C) {
ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null);
mimeType = MimeTypes.VIDEO_AV1;
int childAtomBodySize = childAtomSize - Atom.HEADER_SIZE;
byte[] onlyInitializationDataChunk = new byte[childAtomBodySize];
parent.readBytes(onlyInitializationDataChunk, /* offset= */ 0, childAtomBodySize);
initializationData = ImmutableList.of(onlyInitializationDataChunk);
} else if (childAtomType == Atom.TYPE_clli) {
if (hdrStaticInfo == null) {
hdrStaticInfo = allocateHdrStaticInfo();
@ -1210,10 +1216,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
mimeType = MimeTypes.VIDEO_H263;
} else if (childAtomType == Atom.TYPE_esds) {
ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null);
Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationDataBytes =
parseEsdsFromParent(parent, childStartPosition);
mimeType = mimeTypeAndInitializationDataBytes.first;
@Nullable byte[] initializationDataBytes = mimeTypeAndInitializationDataBytes.second;
esdsData = parseEsdsFromParent(parent, childStartPosition);
mimeType = esdsData.mimeType;
@Nullable byte[] initializationDataBytes = esdsData.initializationData;
if (initializationDataBytes != null) {
initializationData = ImmutableList.of(initializationDataBytes);
}
@ -1301,6 +1306,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
colorTransfer,
hdrStaticInfo != null ? hdrStaticInfo.array() : null));
}
if (esdsData != null) {
formatBuilder.setAverageBitrate(esdsData.bitrate).setPeakBitrate(esdsData.peakBitrate);
}
out.format = formatBuilder.build();
}
@ -1391,6 +1401,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int sampleRateMlp = 0;
@C.PcmEncoding int pcmEncoding = Format.NO_VALUE;
@Nullable String codecs = null;
@Nullable EsdsData esdsData = null;
if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) {
channelCount = parent.readUnsignedShort();
@ -1507,10 +1518,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
? childPosition
: findBoxPosition(parent, Atom.TYPE_esds, childPosition, childAtomSize);
if (esdsAtomPosition != C.POSITION_UNSET) {
Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationData =
parseEsdsFromParent(parent, esdsAtomPosition);
mimeType = mimeTypeAndInitializationData.first;
@Nullable byte[] initializationDataBytes = mimeTypeAndInitializationData.second;
esdsData = parseEsdsFromParent(parent, esdsAtomPosition);
mimeType = esdsData.mimeType;
@Nullable byte[] initializationDataBytes = esdsData.initializationData;
if (initializationDataBytes != null) {
if (MimeTypes.AUDIO_AAC.equals(mimeType)) {
// Update sampleRate and channelCount from the AudioSpecificConfig initialization
@ -1591,7 +1601,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
if (out.format == null && mimeType != null) {
out.format =
Format.Builder formatBuilder =
new Format.Builder()
.setId(trackId)
.setSampleMimeType(mimeType)
@ -1601,8 +1611,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
.setPcmEncoding(pcmEncoding)
.setInitializationData(initializationData)
.setDrmInitData(drmInitData)
.setLanguage(language)
.build();
.setLanguage(language);
if (esdsData != null) {
formatBuilder.setAverageBitrate(esdsData.bitrate).setPeakBitrate(esdsData.peakBitrate);
}
out.format = formatBuilder.build();
}
}
@ -1637,8 +1652,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
/** Returns codec-specific initialization data contained in an esds box. */
private static Pair<@NullableType String, byte @NullableType []> parseEsdsFromParent(
ParsableByteArray parent, int position) {
private static EsdsData parseEsdsFromParent(ParsableByteArray parent, int position) {
parent.setPosition(position + Atom.HEADER_SIZE + 4);
// Start of the ES_Descriptor (defined in ISO/IEC 14496-1)
parent.skipBytes(1); // ES_Descriptor tag
@ -1666,17 +1680,29 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
if (MimeTypes.AUDIO_MPEG.equals(mimeType)
|| MimeTypes.AUDIO_DTS.equals(mimeType)
|| MimeTypes.AUDIO_DTS_HD.equals(mimeType)) {
return Pair.create(mimeType, null);
return new EsdsData(
mimeType,
/* initializationData= */ null,
/* bitrate= */ Format.NO_VALUE,
/* peakBitrate= */ Format.NO_VALUE);
}
parent.skipBytes(12);
parent.skipBytes(4);
int peakBitrate = parent.readUnsignedIntToInt();
int bitrate = parent.readUnsignedIntToInt();
// Start of the DecoderSpecificInfo.
parent.skipBytes(1); // DecoderSpecificInfo tag
int initializationDataSize = parseExpandableClassSize(parent);
byte[] initializationData = new byte[initializationDataSize];
parent.readBytes(initializationData, 0, initializationDataSize);
return Pair.create(mimeType, initializationData);
// Skipping zero values as unknown.
return new EsdsData(
mimeType,
/* initializationData= */ initializationData,
/* bitrate= */ bitrate > 0 ? bitrate : Format.NO_VALUE,
/* peakBitrate= */ peakBitrate > 0 ? peakBitrate : Format.NO_VALUE);
}
/**
@ -1918,6 +1944,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
}
/** Data parsed from an esds box. */
private static final class EsdsData {
private final @NullableType String mimeType;
private final byte @NullableType [] initializationData;
private final int bitrate;
private final int peakBitrate;
public EsdsData(
@NullableType String mimeType,
byte @NullableType [] initializationData,
int bitrate,
int peakBitrate) {
this.mimeType = mimeType;
this.initializationData = initializationData;
this.bitrate = bitrate;
this.peakBitrate = peakBitrate;
}
}
/** A box containing sample sizes (e.g. stsz, stz2). */
private interface SampleSizeBox {

View File

@ -281,6 +281,22 @@ public final class Mp4Extractor implements Extractor, SeekMap {
@Override
public SeekPoints getSeekPoints(long timeUs) {
return getSeekPoints(timeUs, /* trackId= */ C.INDEX_UNSET);
}
// Non-inherited public methods.
/**
* Equivalent to {@link SeekMap#getSeekPoints(long)}, except it adds the {@code trackId}
* parameter.
*
* @param timeUs A seek time in microseconds.
* @param trackId The id of the track on which to seek for {@link SeekPoints}. May be {@link
* C#INDEX_UNSET} if the extractor is expected to define the strategy for generating {@link
* SeekPoints}.
* @return The corresponding seek points.
*/
public SeekPoints getSeekPoints(long timeUs, int trackId) {
if (tracks.length == 0) {
return new SeekPoints(SeekPoint.START);
}
@ -290,9 +306,11 @@ public final class Mp4Extractor implements Extractor, SeekMap {
long secondTimeUs = C.TIME_UNSET;
long secondOffset = C.POSITION_UNSET;
// Note that the id matches the index in tracks.
int mainTrackIndex = trackId != C.INDEX_UNSET ? trackId : firstVideoTrackIndex;
// If we have a video track, use it to establish one or two seek points.
if (firstVideoTrackIndex != C.INDEX_UNSET) {
TrackSampleTable sampleTable = tracks[firstVideoTrackIndex].sampleTable;
if (mainTrackIndex != C.INDEX_UNSET) {
TrackSampleTable sampleTable = tracks[mainTrackIndex].sampleTable;
int sampleIndex = getSynchronizationSampleIndex(sampleTable, timeUs);
if (sampleIndex == C.INDEX_UNSET) {
return new SeekPoints(SeekPoint.START);
@ -312,13 +330,15 @@ public final class Mp4Extractor implements Extractor, SeekMap {
firstOffset = Long.MAX_VALUE;
}
// Take into account other tracks.
for (int i = 0; i < tracks.length; i++) {
if (i != firstVideoTrackIndex) {
TrackSampleTable sampleTable = tracks[i].sampleTable;
firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset);
if (secondTimeUs != C.TIME_UNSET) {
secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset);
if (trackId == C.INDEX_UNSET) {
// Take into account other tracks, but only if the caller has not specified a trackId.
for (int i = 0; i < tracks.length; i++) {
if (i != firstVideoTrackIndex) {
TrackSampleTable sampleTable = tracks[i].sampleTable;
firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset);
if (secondTimeUs != C.TIME_UNSET) {
secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset);
}
}
}
}

View File

@ -97,6 +97,12 @@ public final class Mp4ExtractorTest {
Mp4Extractor::new, "media/mp4/sample_mpegh_mhm1.mp4", simulationConfig);
}
@Test
public void mp4SampleWithAv1Track() throws Exception {
ExtractorAsserts.assertBehavior(
Mp4Extractor::new, "media/mp4/sample_av1.mp4", simulationConfig);
}
@Test
public void mp4SampleWithColorInfo() throws Exception {
ExtractorAsserts.assertBehavior(

View File

@ -30,6 +30,7 @@ import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.media.app.NotificationCompat.MediaStyle;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.util.Consumer;
@ -114,35 +115,48 @@ public final class DefaultMediaNotificationProvider implements MediaNotification
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
// TODO(b/193193926): Filter actions depending on the player's available commands.
Player.Commands availableCommands = mediaController.getAvailableCommands();
// Skip to previous action.
builder.addAction(
actionFactory.createMediaAction(
IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_previous),
context.getString(R.string.media3_controls_seek_to_previous_description),
MediaNotification.ActionFactory.COMMAND_SKIP_TO_PREVIOUS));
if (mediaController.getPlaybackState() == Player.STATE_ENDED
|| !mediaController.getPlayWhenReady()) {
// Play action.
boolean skipToPreviousAdded = false;
if (availableCommands.containsAny(
Player.COMMAND_SEEK_TO_PREVIOUS, Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)) {
skipToPreviousAdded = true;
builder.addAction(
actionFactory.createMediaAction(
IconCompat.createWithResource(context, R.drawable.media3_notification_play),
context.getString(R.string.media3_controls_play_description),
MediaNotification.ActionFactory.COMMAND_PLAY));
} else {
// Pause action.
builder.addAction(
actionFactory.createMediaAction(
IconCompat.createWithResource(context, R.drawable.media3_notification_pause),
context.getString(R.string.media3_controls_pause_description),
MediaNotification.ActionFactory.COMMAND_PAUSE));
IconCompat.createWithResource(
context, R.drawable.media3_notification_seek_to_previous),
context.getString(R.string.media3_controls_seek_to_previous_description),
MediaNotification.ActionFactory.COMMAND_SKIP_TO_PREVIOUS));
}
boolean playPauseAdded = false;
if (availableCommands.contains(Player.COMMAND_PLAY_PAUSE)) {
playPauseAdded = true;
if (mediaController.getPlaybackState() == Player.STATE_ENDED
|| !mediaController.getPlayWhenReady()) {
// Play action.
builder.addAction(
actionFactory.createMediaAction(
IconCompat.createWithResource(context, R.drawable.media3_notification_play),
context.getString(R.string.media3_controls_play_description),
MediaNotification.ActionFactory.COMMAND_PLAY));
} else {
// Pause action.
builder.addAction(
actionFactory.createMediaAction(
IconCompat.createWithResource(context, R.drawable.media3_notification_pause),
context.getString(R.string.media3_controls_pause_description),
MediaNotification.ActionFactory.COMMAND_PAUSE));
}
}
// Skip to next action.
builder.addAction(
actionFactory.createMediaAction(
IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_next),
context.getString(R.string.media3_controls_seek_to_next_description),
MediaNotification.ActionFactory.COMMAND_SKIP_TO_NEXT));
if (availableCommands.containsAny(
Player.COMMAND_SEEK_TO_NEXT, Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)) {
builder.addAction(
actionFactory.createMediaAction(
IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_next),
context.getString(R.string.media3_controls_seek_to_next_description),
MediaNotification.ActionFactory.COMMAND_SKIP_TO_NEXT));
}
// Set metadata info in the notification.
MediaMetadata metadata = mediaController.getMediaMetadata();
@ -171,12 +185,17 @@ public final class DefaultMediaNotificationProvider implements MediaNotification
}
}
androidx.media.app.NotificationCompat.MediaStyle mediaStyle =
new androidx.media.app.NotificationCompat.MediaStyle()
.setCancelButtonIntent(
actionFactory.createMediaActionPendingIntent(
MediaNotification.ActionFactory.COMMAND_STOP))
.setShowActionsInCompactView(1 /* Show play/pause button only in compact view */);
MediaStyle mediaStyle = new MediaStyle();
if (mediaController.isCommandAvailable(Player.COMMAND_STOP) || Util.SDK_INT < 21) {
// We must include a cancel intent for pre-L devices.
mediaStyle.setCancelButtonIntent(
actionFactory.createMediaActionPendingIntent(
MediaNotification.ActionFactory.COMMAND_STOP));
}
if (playPauseAdded) {
// Show play/pause button only in compact view.
mediaStyle.setShowActionsInCompactView(skipToPreviousAdded ? 1 : 0);
}
Notification notification =
builder

Some files were not shown because too many files have changed in this diff Show More