mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Merge branch 'main' into rtp-h263
This commit is contained in:
commit
ffa04ea949
42
.github/ISSUE_TEMPLATE/bug.md
vendored
42
.github/ISSUE_TEMPLATE/bug.md
vendored
@ -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 you’re 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
99
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal 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
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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')
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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"
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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]) {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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) {
|
@ -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.
|
||||
*
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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>:
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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, " ");
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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> {
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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. */
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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= */ "");
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user