Commit 867b1078 by ybai001 Committed by GitHub

Merge pull request #24 from google/dev-v2

Dev v2
parents a308c691 a056f08a
Showing with 2139 additions and 757 deletions
......@@ -17,7 +17,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.3'
classpath 'com.android.tools.build:gradle:7.2.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.2'
}
}
......
......@@ -29,5 +29,10 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
testOptions.unitTests.includeAndroidResources = true
testOptions {
unitTests.all {
jvmArgs "-Xmx2g"
}
unitTests.includeAndroidResources true
}
}
......@@ -20,14 +20,14 @@ 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:
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
guavaVersion = '31.0.1-android'
mockitoVersion = '3.12.4'
robolectricVersion = '4.6.1'
robolectricVersion = '4.8.1'
// Keep this in sync with Google's internal Checker Framework version.
checkerframeworkVersion = '3.13.0'
checkerframeworkCompatVersion = '2.5.5'
......
......@@ -230,8 +230,8 @@ public class MainActivity extends AppCompatActivity
@Override
public boolean onMove(
RecyclerView list, RecyclerView.ViewHolder origin, RecyclerView.ViewHolder target) {
int fromPosition = origin.getAdapterPosition();
int toPosition = target.getAdapterPosition();
int fromPosition = origin.getBindingAdapterPosition();
int toPosition = target.getBindingAdapterPosition();
if (draggingFromPosition == C.INDEX_UNSET) {
// A drag has started, but changes to the media queue will be reflected in clearView().
draggingFromPosition = fromPosition;
......@@ -243,7 +243,7 @@ public class MainActivity extends AppCompatActivity
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
int position = viewHolder.getBindingAdapterPosition();
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
if (playerManager.removeItem(queueItemHolder.item)) {
mediaQueueListAdapter.notifyItemRemoved(position);
......@@ -282,7 +282,7 @@ public class MainActivity extends AppCompatActivity
@Override
public void onClick(View v) {
playerManager.selectQueueItem(getAdapterPosition());
playerManager.selectQueueItem(getBindingAdapterPosition());
}
}
......
......@@ -25,7 +25,7 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.TracksInfo;
import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
import com.google.android.exoplayer2.ui.StyledPlayerControlView;
......@@ -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.isTypeSupportedOrEmpty(
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.isTypeSupportedOrEmpty(
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.
......
......@@ -27,6 +27,7 @@ import android.graphics.drawable.BitmapDrawable;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.GlProgram;
import com.google.android.exoplayer2.util.GlUtil;
import java.io.IOException;
import java.util.Locale;
......@@ -50,7 +51,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Bitmap logoBitmap;
private final Canvas overlayCanvas;
private GlUtil.@MonotonicNonNull Program program;
private @MonotonicNonNull GlProgram program;
private float bitmapScaleX;
private float bitmapScaleY;
......@@ -78,7 +79,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void initialize() {
try {
program =
new GlUtil.Program(
new GlProgram(
context,
/* vertexShaderFilePath= */ "bitmap_overlay_video_processor_vertex.glsl",
/* fragmentShaderFilePath= */ "bitmap_overlay_video_processor_fragment.glsl");
......@@ -86,9 +87,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
throw new IllegalStateException(e);
}
program.setBufferAttribute(
"aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
program.setBufferAttribute(
"aTexCoords", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
"aTexCoords",
GlUtil.getTextureCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
......@@ -117,9 +122,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
GlUtil.checkGlError();
// Run the shader program.
GlUtil.Program program = checkNotNull(this.program);
program.setSamplerTexIdUniform("uTexSampler0", frameTexture, /* unit= */ 0);
program.setSamplerTexIdUniform("uTexSampler1", textures[0], /* unit= */ 1);
GlProgram program = checkNotNull(this.program);
program.setSamplerTexIdUniform("uTexSampler0", frameTexture, /* texUnitIndex= */ 0);
program.setSamplerTexIdUniform("uTexSampler1", textures[0], /* texUnitIndex= */ 1);
program.setFloatUniform("uScaleX", bitmapScaleX);
program.setFloatUniform("uScaleY", bitmapScaleY);
program.setFloatsUniform("uTexTransform", transformMatrix);
......
......@@ -20,6 +20,7 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.Nullable;
......@@ -38,7 +39,6 @@ import com.google.android.exoplayer2.ui.StyledPlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.EventLogger;
import com.google.android.exoplayer2.util.GlUtil;
......@@ -144,7 +144,7 @@ public final class MainActivity extends Activity {
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory();
DataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory();
HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
drmSessionManager =
......@@ -157,13 +157,18 @@ public final class MainActivity extends Activity {
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this);
MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
if (type == C.TYPE_DASH) {
@Nullable String fileExtension = intent.getStringExtra(EXTENSION_EXTRA);
@C.ContentType
int type =
TextUtils.isEmpty(fileExtension)
? Util.inferContentType(uri)
: Util.inferContentTypeForExtension(fileExtension);
if (type == C.CONTENT_TYPE_DASH) {
mediaSource =
new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
.createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) {
} else if (type == C.CONTENT_TYPE_OTHER) {
mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
......@@ -181,7 +186,7 @@ public final class MainActivity extends Activity {
Assertions.checkNotNull(this.videoProcessingGLSurfaceView);
videoProcessingGLSurfaceView.setPlayer(player);
Assertions.checkNotNull(playerView).setPlayer(player);
player.addAnalyticsListener(new EventLogger(/* trackSelector= */ null));
player.addAnalyticsListener(new EventLogger());
this.player = player;
}
......
......@@ -27,7 +27,6 @@ import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
......@@ -59,7 +58,7 @@ public final class DemoUtil {
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static DataSource.@MonotonicNonNull Factory dataSourceFactory;
private static HttpDataSource.@MonotonicNonNull Factory httpDataSourceFactory;
private static DataSource.@MonotonicNonNull Factory httpDataSourceFactory;
private static @MonotonicNonNull DatabaseProvider databaseProvider;
private static @MonotonicNonNull File downloadDirectory;
private static @MonotonicNonNull Cache downloadCache;
......@@ -85,7 +84,7 @@ public final class DemoUtil {
.setExtensionRendererMode(extensionRendererMode);
}
public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) {
public static synchronized DataSource.Factory getHttpDataSourceFactory(Context context) {
if (httpDataSourceFactory == null) {
if (USE_CRONET_FOR_NETWORKING) {
context = context.getApplicationContext();
......
......@@ -15,8 +15,7 @@
*/
package com.google.android.exoplayer2.demo;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import static com.google.common.base.Preconditions.checkNotNull;
import android.content.Context;
import android.content.DialogInterface;
......@@ -29,6 +28,7 @@ import androidx.fragment.app.FragmentManager;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
......@@ -43,9 +43,9 @@ import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.DownloadService;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
......@@ -65,31 +65,26 @@ public class DownloadTracker {
private static final String TAG = "DownloadTracker";
private final Context context;
private final HttpDataSource.Factory httpDataSourceFactory;
private final DataSource.Factory dataSourceFactory;
private final CopyOnWriteArraySet<Listener> listeners;
private final HashMap<Uri, Download> downloads;
private final DownloadIndex downloadIndex;
private final DefaultTrackSelector.Parameters trackSelectorParameters;
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
public DownloadTracker(
Context context,
HttpDataSource.Factory httpDataSourceFactory,
DownloadManager downloadManager) {
Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
this.context = context.getApplicationContext();
this.httpDataSourceFactory = httpDataSourceFactory;
this.dataSourceFactory = dataSourceFactory;
listeners = new CopyOnWriteArraySet<>();
downloads = new HashMap<>();
downloadIndex = downloadManager.getDownloadIndex();
trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context);
downloadManager.addListener(new DownloadManagerListener());
loadDownloads();
}
public void addListener(Listener listener) {
checkNotNull(listener);
listeners.add(listener);
listeners.add(checkNotNull(listener));
}
public void removeListener(Listener listener) {
......@@ -120,8 +115,7 @@ public class DownloadTracker {
startDownloadDialogHelper =
new StartDownloadDialogHelper(
fragmentManager,
DownloadHelper.forMediaItem(
context, mediaItem, renderersFactory, httpDataSourceFactory),
DownloadHelper.forMediaItem(context, mediaItem, renderersFactory, dataSourceFactory),
mediaItem);
}
}
......@@ -159,7 +153,7 @@ public class DownloadTracker {
private final class StartDownloadDialogHelper
implements DownloadHelper.Callback,
DialogInterface.OnClickListener,
TrackSelectionDialog.TrackSelectionListener,
DialogInterface.OnDismissListener {
private final FragmentManager fragmentManager;
......@@ -167,7 +161,6 @@ public class DownloadTracker {
private final MediaItem mediaItem;
private TrackSelectionDialog trackSelectionDialog;
private MappedTrackInfo mappedTrackInfo;
private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask;
@Nullable private byte[] keySetId;
......@@ -220,7 +213,7 @@ public class DownloadTracker {
new WidevineOfflineLicenseFetchTask(
format,
mediaItem.localConfiguration.drmConfiguration,
httpDataSourceFactory,
dataSourceFactory,
/* dialogHelper= */ this,
helper);
widevineOfflineLicenseFetchTask.execute();
......@@ -237,21 +230,13 @@ public class DownloadTracker {
Log.e(TAG, logMessage, e);
}
// DialogInterface.OnClickListener implementation.
// TrackSelectionListener implementation.
@Override
public void onClick(DialogInterface dialog, int which) {
public void onTracksSelected(TrackSelectionParameters trackSelectionParameters) {
for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) {
downloadHelper.clearTrackSelections(periodIndex);
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) {
downloadHelper.addTrackSelectionForSingleRenderer(
periodIndex,
/* rendererIndex= */ i,
trackSelectorParameters,
trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
}
}
downloadHelper.addTrackSelection(periodIndex, trackSelectionParameters);
}
DownloadRequest downloadRequest = buildDownloadRequest();
if (downloadRequest.streamKeys.isEmpty()) {
......@@ -316,21 +301,21 @@ public class DownloadTracker {
return;
}
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0);
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) {
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.createForMappedTrackInfoAndParameters(
TrackSelectionDialog.createForTracksAndParameters(
/* titleId= */ R.string.exo_download_description,
mappedTrackInfo,
trackSelectorParameters,
tracks,
DownloadHelper.getDefaultTrackSelectorParameters(context),
/* allowAdaptiveSelections= */ false,
/* allowMultipleOverrides= */ true,
/* onClickListener= */ this,
/* onTracksSelectedListener= */ this,
/* onDismissListener= */ this);
trackSelectionDialog.show(fragmentManager, /* tag= */ null);
}
......@@ -371,7 +356,7 @@ public class DownloadTracker {
private final Format format;
private final MediaItem.DrmConfiguration drmConfiguration;
private final HttpDataSource.Factory httpDataSourceFactory;
private final DataSource.Factory dataSourceFactory;
private final StartDownloadDialogHelper dialogHelper;
private final DownloadHelper downloadHelper;
......@@ -381,12 +366,12 @@ public class DownloadTracker {
public WidevineOfflineLicenseFetchTask(
Format format,
MediaItem.DrmConfiguration drmConfiguration,
HttpDataSource.Factory httpDataSourceFactory,
DataSource.Factory dataSourceFactory,
StartDownloadDialogHelper dialogHelper,
DownloadHelper downloadHelper) {
this.format = format;
this.drmConfiguration = drmConfiguration;
this.httpDataSourceFactory = httpDataSourceFactory;
this.dataSourceFactory = dataSourceFactory;
this.dialogHelper = dialogHelper;
this.downloadHelper = downloadHelper;
}
......@@ -397,7 +382,7 @@ public class DownloadTracker {
OfflineLicenseHelper.newWidevineInstance(
drmConfiguration.licenseUri.toString(),
drmConfiguration.forceDefaultLicenseUri,
httpDataSourceFactory,
dataSourceFactory,
drmConfiguration.licenseRequestHeaders,
new DrmSessionEventListener.EventDispatcher());
try {
......@@ -415,7 +400,7 @@ public class DownloadTracker {
if (drmSessionException != null) {
dialogHelper.onOfflineLicenseFetchedError(drmSessionException);
} else {
dialogHelper.onOfflineLicenseFetched(downloadHelper, checkStateNotNull(keySetId));
dialogHelper.onOfflineLicenseFetched(downloadHelper, checkNotNull(keySetId));
}
}
}
......
......@@ -15,8 +15,9 @@
*/
package com.google.android.exoplayer2.demo;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import android.content.Intent;
import android.net.Uri;
......@@ -26,7 +27,6 @@ import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.MediaItem.ClippingConfiguration;
import com.google.android.exoplayer2.MediaItem.SubtitleConfiguration;
import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
......@@ -87,7 +87,7 @@ public class IntentUtil {
/** Populates the intent with the given list of {@link MediaItem media items}. */
public static void addToIntent(List<MediaItem> mediaItems, Intent intent) {
Assertions.checkArgument(!mediaItems.isEmpty());
checkArgument(!mediaItems.isEmpty());
if (mediaItems.size() == 1) {
MediaItem mediaItem = mediaItems.get(0);
MediaItem.LocalConfiguration localConfiguration = checkNotNull(mediaItem.localConfiguration);
......@@ -178,7 +178,7 @@ public class IntentUtil {
headers.put(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
}
}
@Nullable UUID drmUuid = Util.getDrmUuid(Util.castNonNull(drmSchemeExtra));
@Nullable UUID drmUuid = Util.getDrmUuid(drmSchemeExtra);
if (drmUuid != null) {
builder.setDrmConfiguration(
new MediaItem.DrmConfiguration.Builder(drmUuid)
......@@ -189,7 +189,7 @@ public class IntentUtil {
intent.getBooleanExtra(
DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, false))
.setLicenseRequestHeaders(headers)
.forceSessionsForAudioAndVideoTracks(
.setForceSessionsForAudioAndVideoTracks(
intent.getBooleanExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, false))
.build());
}
......@@ -242,7 +242,7 @@ public class IntentUtil {
drmConfiguration.forcedSessionTrackTypes;
if (!forcedDrmSessionTrackTypes.isEmpty()) {
// Only video and audio together are supported.
Assertions.checkState(
checkState(
forcedDrmSessionTrackTypes.size() == 2
&& forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_VIDEO)
&& forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_AUDIO));
......
......@@ -15,9 +15,9 @@
*/
package com.google.android.exoplayer2.demo;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import android.content.Context;
import android.content.Intent;
......@@ -27,6 +27,7 @@ import android.content.res.AssetManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.JsonReader;
import android.view.Menu;
import android.view.MenuInflater;
......@@ -53,6 +54,7 @@ import com.google.android.exoplayer2.upstream.DataSourceUtil;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
......@@ -116,8 +118,11 @@ public class SampleChooserActivity extends AppCompatActivity
useExtensionRenderers = DemoUtil.useExtensionRenderers();
downloadTracker = DemoUtil.getDownloadTracker(/* context= */ this);
loadSample();
startDownloadService();
}
// Start the download service if it should be running but it's not currently.
/** Start the download service if it should be running but it's not currently. */
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
// (e.g. if device screen is locked).
......@@ -436,7 +441,10 @@ public class SampleChooserActivity extends AppCompatActivity
} else {
@Nullable
String adaptiveMimeType =
Util.getAdaptiveMimeTypeForContentType(Util.inferContentType(uri, extension));
Util.getAdaptiveMimeTypeForContentType(
TextUtils.isEmpty(extension)
? Util.inferContentType(uri)
: Util.inferContentTypeForExtension(extension));
mediaItem
.setUri(uri)
.setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build())
......@@ -447,7 +455,7 @@ public class SampleChooserActivity extends AppCompatActivity
new MediaItem.DrmConfiguration.Builder(drmUuid)
.setLicenseUri(drmLicenseUri)
.setLicenseRequestHeaders(drmLicenseRequestHeaders)
.forceSessionsForAudioAndVideoTracks(drmSessionForClearContent)
.setForceSessionsForAudioAndVideoTracks(drmSessionForClearContent)
.setMultiSession(drmMultiSession)
.setForceDefaultLicenseUri(drmForceDefaultLicenseUri)
.build());
......@@ -481,7 +489,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);
}
}
......
......@@ -19,6 +19,7 @@ import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
......@@ -42,7 +43,6 @@ import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.UUID;
......@@ -189,7 +189,7 @@ public final class MainActivity extends Activity {
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory();
DataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory();
HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
drmSessionManager =
......@@ -202,13 +202,18 @@ public final class MainActivity extends Activity {
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this);
MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA));
if (type == C.TYPE_DASH) {
@Nullable String fileExtension = intent.getStringExtra(EXTENSION_EXTRA);
@C.ContentType
int type =
TextUtils.isEmpty(fileExtension)
? Util.inferContentType(uri)
: Util.inferContentTypeForExtension(fileExtension);
if (type == C.CONTENT_TYPE_DASH) {
mediaSource =
new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
.createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) {
} else if (type == C.CONTENT_TYPE_OTHER) {
mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
......
# Build targets for a demo MediaPipe graph.
# See README.md for instructions on using MediaPipe in the demo.
load("//mediapipe/java/com/google/mediapipe:mediapipe_aar.bzl", "mediapipe_aar")
load(
"//mediapipe/framework/tool:mediapipe_graph.bzl",
"mediapipe_binary_graph",
)
mediapipe_aar(
name = "edge_detector_mediapipe_aar",
calculators = [
"//mediapipe/calculators/image:luminance_calculator",
"//mediapipe/calculators/image:sobel_edges_calculator",
],
)
mediapipe_binary_graph(
name = "edge_detector_binary_graph",
graph = "edge_detector_mediapipe_graph.pbtxt",
output_name = "edge_detector_mediapipe_graph.binarypb",
)
......@@ -6,4 +6,61 @@ example by removing audio or video.
See the [demos README](../README.md) for instructions on how to build and run
this demo.
## MediaPipe frame processing demo
Building the demo app with [MediaPipe][] integration enabled requires some extra
manual steps.
1. Follow the
[instructions](https://google.github.io/mediapipe/getting_started/install.html)
to install MediaPipe.
1. Copy the Transformer demo's build configuration and MediaPipe graph text
protocol buffer under the MediaPipe source tree. This makes it easy to
[build an AAR][] with bazel by reusing MediaPipe's workspace.
```sh
cd "<path to MediaPipe checkout>"
MEDIAPIPE_ROOT="$(pwd)"
MEDIAPIPE_TRANSFORMER_ROOT="${MEDIAPIPE_ROOT}/mediapipe/java/com/google/mediapipe/transformer"
cd "<path to the transformer demo (containing this readme)>"
TRANSFORMER_DEMO_ROOT="$(pwd)"
mkdir -p "${MEDIAPIPE_TRANSFORMER_ROOT}"
mkdir -p "${TRANSFORMER_DEMO_ROOT}/libs"
cp ${TRANSFORMER_DEMO_ROOT}/BUILD.bazel ${MEDIAPIPE_TRANSFORMER_ROOT}/BUILD
cp ${TRANSFORMER_DEMO_ROOT}/src/withMediaPipe/assets/edge_detector_mediapipe_graph.pbtxt \
${MEDIAPIPE_TRANSFORMER_ROOT}
```
1. Build the AAR and the binary proto for the demo's MediaPipe graph, then copy
them to Transformer.
```sh
cd ${MEDIAPIPE_ROOT}
bazel build -c opt --strip=ALWAYS \
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
--fat_apk_cpu=arm64-v8a,armeabi-v7a \
--legacy_whole_archive=0 \
--features=-legacy_whole_archive \
--copt=-fvisibility=hidden \
--copt=-ffunction-sections \
--copt=-fdata-sections \
--copt=-fstack-protector \
--copt=-Oz \
--copt=-fomit-frame-pointer \
--copt=-DABSL_MIN_LOG_LEVEL=2 \
--linkopt=-Wl,--gc-sections,--strip-all \
mediapipe/java/com/google/mediapipe/transformer:edge_detector_mediapipe_aar.aar
cp bazel-bin/mediapipe/java/com/google/mediapipe/transformer/edge_detector_mediapipe_aar.aar \
${TRANSFORMER_DEMO_ROOT}/libs
bazel build mediapipe/java/com/google/mediapipe/transformer:edge_detector_binary_graph
cp bazel-bin/mediapipe/java/com/google/mediapipe/transformer/edge_detector_mediapipe_graph.binarypb \
${TRANSFORMER_DEMO_ROOT}/src/withMediaPipe/assets
```
1. In Android Studio, gradle sync and select the `withMediaPipe` build variant
(this will only appear if the AAR is present), then build and run the demo
app and select a MediaPipe-based effect.
[Transformer]: https://exoplayer.dev/transforming-media.html
[MediaPipe]: https://google.github.io/mediapipe/
[build an AAR]: https://google.github.io/mediapipe/getting_started/android_archive_library.html
......@@ -45,6 +45,27 @@ android {
// This demo app isn't indexed and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
flavorDimensions "mediaPipe"
productFlavors {
noMediaPipe {
dimension "mediaPipe"
}
withMediaPipe {
dimension "mediaPipe"
}
}
// Ignore the withMediaPipe variant if the MediaPipe AAR is not present.
if (!project.file("libs/edge_detector_mediapipe_aar.aar").exists()) {
variantFilter { variant ->
def names = variant.flavors*.name
if (names.contains("withMediaPipe")) {
setIgnore(true)
}
}
}
}
dependencies {
......@@ -56,6 +77,14 @@ dependencies {
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-transformer')
implementation project(modulePrefix + 'library-ui')
// For MediaPipe and its dependencies:
withMediaPipeImplementation fileTree(dir: 'libs', include: ['*.aar'])
withMediaPipeImplementation 'com.google.flogger:flogger:latest.release'
withMediaPipeImplementation 'com.google.flogger:flogger-system-backend:latest.release'
withMediaPipeImplementation 'com.google.code.findbugs:jsr305:latest.release'
withMediaPipeImplementation 'com.google.protobuf:protobuf-javalite:3.19.1'
}
#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;
}
#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);
}
#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;
varying vec2 vTexSamplingCoord;
void main() {
gl_Position = aFramePosition;
vTexSamplingCoord = vec2(aFramePosition.x * 0.5 + 0.5, aFramePosition.y * 0.5 + 0.5);
}
/*
* 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 com.google.android.exoplayer2.transformerdemo;
import static com.google.android.exoplayer2.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 com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.transformer.FrameProcessingException;
import com.google.android.exoplayer2.transformer.SingleFrameGlTextureProcessor;
import com.google.android.exoplayer2.util.GlProgram;
import com.google.android.exoplayer2.util.GlUtil;
import java.io.IOException;
import java.util.Locale;
/**
* A {@link SingleFrameGlTextureProcessor} 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 texture processor from the Transformer library,
// once overlaying a bitmap and text is supported in Transformer.
/* package */ final class BitmapOverlayProcessor implements SingleFrameGlTextureProcessor {
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 Bitmap logoBitmap;
private final Canvas overlayCanvas;
private final GlProgram glProgram;
private float bitmapScaleX;
private float bitmapScaleY;
private int bitmapTexId;
/**
* Creates a new instance.
*
* @throws IOException If a problem occurs while reading shader files.
*/
public BitmapOverlayProcessor(Context context) throws IOException {
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);
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.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
glProgram.setSamplerTexIdUniform("uTexSampler1", bitmapTexId, /* texUnitIndex= */ 1);
}
@Override
public Size configure(int inputWidth, int inputHeight) {
if (inputWidth > inputHeight) {
bitmapScaleX = inputWidth / (float) inputHeight;
bitmapScaleY = 1f;
} else {
bitmapScaleX = 1f;
bitmapScaleY = inputHeight / (float) inputWidth;
}
glProgram.setFloatUniform("uScaleX", bitmapScaleX);
glProgram.setFloatUniform("uScaleY", bitmapScaleY);
return new Size(inputWidth, inputHeight);
}
@Override
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
try {
checkStateNotNull(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.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
}
@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);
}
}
/*
* 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 com.google.android.exoplayer2.transformerdemo;
import android.graphics.Matrix;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.transformer.GlMatrixTransformation;
import com.google.android.exoplayer2.transformer.MatrixTransformation;
import com.google.android.exoplayer2.util.Util;
/**
* Factory for {@link GlMatrixTransformation GlMatrixTransformations} and {@link
* MatrixTransformation MatrixTransformations} that create video effects by applying transformation
* matrices to the individual video frames.
*/
/* package */ final class MatrixTransformationFactory {
/**
* Returns a {@link MatrixTransformation} 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 MatrixTransformation createZoomInTransition() {
return MatrixTransformationFactory::calculateZoomInTransitionMatrix;
}
/**
* Returns a {@link MatrixTransformation} that crops frames to a rectangle that moves on an
* ellipse.
*/
public static MatrixTransformation createDizzyCropEffect() {
return MatrixTransformationFactory::calculateDizzyCropMatrix;
}
/**
* Returns a {@link GlMatrixTransformation} that rotates a frame in 3D around the y-axis and
* applies perspective projection to 2D.
*/
public static GlMatrixTransformation createSpin3dEffect() {
return MatrixTransformationFactory::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;
}
}
/*
* 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 com.google.android.exoplayer2.transformerdemo;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import android.content.Context;
import android.opengl.GLES20;
import android.util.Size;
import com.google.android.exoplayer2.transformer.FrameProcessingException;
import com.google.android.exoplayer2.transformer.SingleFrameGlTextureProcessor;
import com.google.android.exoplayer2.util.GlProgram;
import com.google.android.exoplayer2.util.GlUtil;
import java.io.IOException;
/**
* A {@link SingleFrameGlTextureProcessor} that periodically dims the frames such that pixels are
* darker the further they are away from the frame center.
*/
/* package */ final class PeriodicVignetteProcessor implements SingleFrameGlTextureProcessor {
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 final GlProgram glProgram;
private final float minInnerRadius;
private final float deltaInnerRadius;
/**
* 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.
* @throws IOException If a problem occurs while reading shader files.
*/
public PeriodicVignetteProcessor(
Context context,
float centerX,
float centerY,
float minInnerRadius,
float maxInnerRadius,
float outerRadius)
throws IOException {
checkArgument(minInnerRadius <= maxInnerRadius);
checkArgument(maxInnerRadius <= outerRadius);
this.minInnerRadius = minInnerRadius;
this.deltaInnerRadius = maxInnerRadius - minInnerRadius;
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
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.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
}
@Override
public Size configure(int inputWidth, int inputHeight) {
return new Size(inputWidth, inputHeight);
}
@Override
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
try {
glProgram.use();
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
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);
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
}
@Override
public void release() {
if (glProgram != null) {
glProgram.delete();
}
}
}
......@@ -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_effects_button">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
......@@ -141,17 +141,6 @@
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/translate"
android:text="@string/translate"/>
<Spinner
android:id="@+id/translate_spinner"
android:layout_gravity="right|center_vertical"
android:gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/scale"
android:text="@string/scale"/>
<Spinner
......@@ -174,6 +163,36 @@
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/trim"
android:text="@string/trim" />
<CheckBox
android:id="@+id/trim_checkbox"
android:layout_gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/enable_fallback" />
<CheckBox
android:id="@+id/enable_fallback_checkbox"
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" >
<TextView
android:id="@+id/hdr_editing"
android:text="@string/hdr_editing" />
<CheckBox
......@@ -183,6 +202,17 @@
</TableLayout>
</androidx.core.widget.NestedScrollView>
<Button
android:id="@+id/select_demo_effects_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_effects"
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"
android:layout_height="wrap_content"
......
<?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>
<?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/trim_range" />
<com.google.android.material.slider.RangeSlider
android:id="@+id/trim_bounds_range_slider"
android:valueFrom="0.0"
android:valueTo="60.0"
android:layout_gravity="right"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
......@@ -17,22 +17,31 @@
<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>
<string name="audio_mime" translatable="false">Output audio MIME type</string>
<string name="video_mime" translatable="false">Output video MIME type</string>
<string name="resolution_height" translatable="false">Output video resolution</string>
<string name="translate" translatable="false">Translate video</string>
<string name="scale" translatable="false">Scale video</string>
<string name="rotate" translatable="false">Rotate video (degrees)</string>
<string name="transform" translatable="false">Transform</string>
<string name="enable_fallback" translatable="false">Enable fallback</string>
<string name="trim" translatable="false">Trim</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_effects" translatable="false">Add demo effects</string>
<string name="periodic_vignette_options" translatable="false">Periodic vignette options</string>
<string name="no_media_pipe_error" translatable="false">Failed to load MediaPipe processor. Check the README for instructions.</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>
<string name="trim_range">Bounds in seconds</string>
</resources>
<?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.
-->
<manifest package="com.google.android.exoplayer2.transformerdemo">
<uses-sdk />
</manifest>
# Demo MediaPipe graph that shows edges using a SobelEdgesCalculator.
input_stream: "input_video"
output_stream: "output_video"
node: {
calculator: "LuminanceCalculator"
input_stream: "input_video"
output_stream: "luma_video"
}
node: {
calculator: "SobelEdgesCalculator"
input_stream: "luma_video"
output_stream: "output_video"
}
/*
* 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 com.google.android.exoplayer2.transformerdemo;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.opengl.EGL14;
import android.opengl.GLES20;
import android.util.Size;
import com.google.android.exoplayer2.transformer.FrameProcessingException;
import com.google.android.exoplayer2.transformer.SingleFrameGlTextureProcessor;
import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.GlProgram;
import com.google.android.exoplayer2.util.GlUtil;
import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.mediapipe.components.FrameProcessor;
import com.google.mediapipe.framework.AndroidAssetUtil;
import com.google.mediapipe.framework.AppTextureFrame;
import com.google.mediapipe.framework.TextureFrame;
import com.google.mediapipe.glutil.EglManager;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Runs a MediaPipe graph on input frames. The implementation is currently limited to graphs that
* can immediately produce one output frame per input frame.
*/
/* package */ final class MediaPipeProcessor implements SingleFrameGlTextureProcessor {
private static final LibraryLoader LOADER =
new LibraryLoader("mediapipe_jni") {
@Override
protected void loadLibrary(String name) {
System.loadLibrary(name);
}
};
static {
// Not all build configurations require OpenCV to be loaded separately, so attempt to load the
// library but ignore the error if it's not present.
try {
System.loadLibrary("opencv_java3");
} catch (UnsatisfiedLinkError e) {
// Do nothing.
}
}
private static final String COPY_VERTEX_SHADER_NAME = "vertex_shader_copy_es2.glsl";
private static final String COPY_FRAGMENT_SHADER_NAME = "shaders/fragment_shader_copy_es2.glsl";
private final ConditionVariable frameProcessorConditionVariable;
private final FrameProcessor frameProcessor;
private final GlProgram glProgram;
private int inputWidth;
private int inputHeight;
private @MonotonicNonNull TextureFrame outputFrame;
private @MonotonicNonNull RuntimeException frameProcessorPendingError;
/**
* Creates a new texture processor that wraps a MediaPipe graph.
*
* @param context The {@link Context}.
* @param graphName Name of a MediaPipe graph asset to load.
* @param inputStreamName Name of the input video stream in the graph.
* @param outputStreamName Name of the input video stream in the graph.
* @throws IOException If a problem occurs while reading shader files or initializing MediaPipe
* resources.
*/
public MediaPipeProcessor(
Context context, String graphName, String inputStreamName, String outputStreamName)
throws IOException {
checkState(LOADER.isAvailable());
frameProcessorConditionVariable = new ConditionVariable();
AndroidAssetUtil.initializeNativeAssetManager(context);
EglManager eglManager = new EglManager(EGL14.eglGetCurrentContext());
frameProcessor =
new FrameProcessor(
context, eglManager.getNativeContext(), graphName, inputStreamName, outputStreamName);
// Unblock drawFrame when there is an output frame or an error.
frameProcessor.setConsumer(
frame -> {
outputFrame = frame;
frameProcessorConditionVariable.open();
});
frameProcessor.setAsynchronousErrorListener(
error -> {
frameProcessorPendingError = error;
frameProcessorConditionVariable.open();
});
glProgram = new GlProgram(context, COPY_VERTEX_SHADER_NAME, COPY_FRAGMENT_SHADER_NAME);
}
@Override
public Size configure(int inputWidth, int inputHeight) {
this.inputWidth = inputWidth;
this.inputHeight = inputHeight;
return new Size(inputWidth, inputHeight);
}
@Override
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
frameProcessorConditionVariable.close();
// Pass the input frame to MediaPipe.
AppTextureFrame appTextureFrame = new AppTextureFrame(inputTexId, inputWidth, inputHeight);
appTextureFrame.setTimestamp(presentationTimeUs);
checkStateNotNull(frameProcessor).onNewFrame(appTextureFrame);
// Wait for output to be passed to the consumer.
try {
frameProcessorConditionVariable.block();
} catch (InterruptedException e) {
// Propagate the interrupted flag so the next blocking operation will throw.
// TODO(b/230469581): The next processor that runs will not have valid input due to returning
// early here. This could be fixed by checking for interruption in the outer loop that runs
// through the texture processors.
Thread.currentThread().interrupt();
return;
}
if (frameProcessorPendingError != null) {
throw new FrameProcessingException(frameProcessorPendingError);
}
// Copy from MediaPipe's output texture to the current output.
try {
checkStateNotNull(glProgram).use();
glProgram.setSamplerTexIdUniform(
"uTexSampler", checkStateNotNull(outputFrame).getTextureName(), /* texUnitIndex= */ 0);
glProgram.setBufferAttribute(
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
glProgram.bindAttributesAndUniforms();
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
} finally {
checkStateNotNull(outputFrame).release();
}
}
@Override
public void release() {
checkStateNotNull(frameProcessor).close();
}
}
......@@ -51,8 +51,8 @@ build and inject a `DefaultMediaSourceFactory` configured with an
~~~
MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory(context)
.setAdsLoaderProvider(adsLoaderProvider)
.setAdViewProvider(playerView);
.setLocalAdInsertionComponents(
adsLoaderProvider, /* adViewProvider= */ playerView);
ExoPlayer player = new ExoPlayer.Builder(context)
.setMediaSourceFactory(mediaSourceFactory)
.build();
......@@ -220,7 +220,7 @@ server-side ad insertion `MediaSource` for URIs using the `ssai://` scheme:
Player player =
new ExoPlayer.Builder(context)
.setMediaSourceFactory(
new DefaultMediaSourceFactory(dataSourceFactory)
new DefaultMediaSourceFactory(context)
.setServerSideAdInsertionMediaSourceFactory(ssaiFactory))
.build();
```
......@@ -241,7 +241,7 @@ In order to use this class, you need to set up the
```
// MediaSource.Factory to load the actual media stream.
DefaultMediaSourceFactory defaultMediaSourceFactory =
new DefaultMediaSourceFactory(dataSourceFactory);
new DefaultMediaSourceFactory(context);
// AdsLoader that can be reused for multiple playbacks.
ImaServerSideAdInsertionMediaSource.AdsLoader adsLoader =
new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(context, adViewProvider)
......@@ -267,7 +267,10 @@ with `ImaServerSideAdInsertionUriBuilder`:
```
Uri ssaiUri =
new ImaServerSideAdInsertionUriBuilder().setAssetKey(assetKey).build();
new ImaServerSideAdInsertionUriBuilder()
.setAssetKey(assetKey)
.setFormat(C.TYPE_HLS)
.build();
player.setMediaItem(MediaItem.fromUri(ssaiUri));
```
......
......@@ -43,32 +43,7 @@ described below.
### Configuring the network stack ###
ExoPlayer supports Android's default network stack, as well as Cronet and
OkHttp. In each case it's possible to customize the network stack for your use
case. The following example shows how to customize the player to use Android's
default network stack with cross-protocol redirects enabled:
~~~
// Build a HttpDataSource.Factory with cross-protocol redirects enabled.
HttpDataSource.Factory httpDataSourceFactory =
new DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true);
// Wrap the HttpDataSource.Factory in a DefaultDataSource.Factory, which adds in
// support for requesting data from other sources (e.g., files, resources, etc).
DefaultDataSource.Factory dataSourceFactory =
new DefaultDataSource.Factory(context, httpDataSourceFactory);
// Inject the DefaultDataSourceFactory when creating the player.
ExoPlayer player =
new ExoPlayer.Builder(context)
.setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory))
.build();
~~~
{: .language-java}
The same approach can be used to configure and inject `HttpDataSource.Factory`
implementations provided by the [Cronet extension] and the [OkHttp extension],
depending on your preferred choice of network stack.
We have a page about [customizing the network stack used by ExoPlayer].
### Caching data loaded from the network ###
......@@ -84,7 +59,8 @@ DataSource.Factory cacheDataSourceFactory =
ExoPlayer player = new ExoPlayer.Builder(context)
.setMediaSourceFactory(
new DefaultMediaSourceFactory(cacheDataSourceFactory))
new DefaultMediaSourceFactory(context)
.setDataSourceFactory(cacheDataSourceFactory))
.build();
~~~
{: .language-java}
......@@ -108,7 +84,9 @@ DataSource.Factory dataSourceFactory = () -> {
};
ExoPlayer player = new ExoPlayer.Builder(context)
.setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory))
.setMediaSourceFactory(
new DefaultMediaSourceFactory(context)
.setDataSourceFactory(dataSourceFactory))
.build();
~~~
{: .language-java}
......@@ -206,6 +184,56 @@ DefaultExtractorsFactory extractorsFactory =
The `ExtractorsFactory` can then be injected via `DefaultMediaSourceFactory` as
described for customizing extractor flags above.
### Enabling asynchronous buffer queueing ###
Asynchronous buffer queueing is an enhancement in ExoPlayer's rendering
pipeline, which operates `MediaCodec` instances in [asynchronous mode][] and
uses additional threads to schedule decoding and rendering of data. Enabling it
can reduce dropped frames and audio underruns.
Asynchronous buffer queueing is enabled by default on devices running Android 12
and above, and can be enabled manually from Android 6. Consider enabling the
feature for specific devices on which you observe dropped frames or audio
underruns, particularly when playing DRM protected or high frame rate content.
In the simplest case, you need to inject a `DefaultRenderersFactory` to the
player as follows:
~~~
DefaultRenderersFactory renderersFactory =
new DefaultRenderersFactory(context)
.forceEnableMediaCodecAsynchronousQueueing();
ExoPlayer exoPlayer = new ExoPlayer.Builder(context, renderersFactory).build();
~~~
{: .language-java}
If you're instantiating renderers directly, pass a
`AsynchronousMediaCodecAdapter.Factory` to the `MediaCodecVideoRenderer` and
`MediaCodecAudioRenderer` constructors.
### Intercepting method calls with `ForwardingPlayer` ###
You can customize some of the behavior of a `Player` instance by wrapping it in
a subclass of `ForwardingPlayer` and overriding methods in order to do any of
the following:
* Access parameters before passing them to the delegate `Player`.
* Access the return value from the delegate `Player` before returning it.
* Re-implement the method completely.
When overriding `ForwardingPlayer` methods it's important to ensure the
implementation remains self-consistent and compliant with the `Player`
interface, especially when dealing with methods that are intended to have
identical or related behavior. For example, if you want to override every 'play'
operation, you need to override both `ForwardingPlayer.play` and
`ForwardingPlayer.setPlayWhenReady`, because a caller will expect the behavior
of these methdods to be identical when `playWhenReady = true`. Similarly, if you
want to change the seek-forward increment you need to override both
`ForwardingPlayer.seekForward` to perform a seek with your customized increment,
and `ForwardingPlayer.getSeekForwardIncrement` in order to report the correct
customized increment back to the caller.
## MediaSource customization ##
The examples above inject customized components for use during playback of all
......@@ -270,8 +298,8 @@ When building custom components, we recommend the following:
ensures that they are executed in order with any other operations being
performed on the player.
[Cronet extension]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/cronet
[OkHttp extension]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/okhttp
[customizing the network stack used by ExoPlayer]: {{ site.baseurl }}/network-stacks.html
[LoadErrorHandlingPolicy]: {{ site.exo_sdk }}/upstream/LoadErrorHandlingPolicy.html
[media source based playlist API]: {{ site.baseurl }}/media-sources.html#media-source-based-playlist-api
[asynchronous mode]: https://developer.android.com/reference/android/media/MediaCodec#asynchronous-processing-using-buffers
......@@ -9,13 +9,10 @@ issues. `EventLogger` implements `AnalyticsListener`, so registering an instance
with an `ExoPlayer` is easy:
```
player.addAnalyticsListener(new EventLogger(trackSelector));
player.addAnalyticsListener(new EventLogger());
```
{: .language-java}
Passing the `trackSelector` enables additional logging, but is optional and so
`null` can be passed instead.
The easiest way to observe the log is using Android Studio's [logcat tab][]. You
can select your app as debuggable process by the package name (
`com.google.android.exoplayer2.demo` if using the demo app) and tell the logcat
......@@ -80,20 +77,16 @@ logging for an adaptive stream:
```
EventLogger: tracks [eventTime=0.30, mediaPos=0.00, window=0, period=0,
EventLogger: MediaCodecVideoRenderer [
EventLogger: Group:0, adaptive_supported=YES [
EventLogger: [X] Track:0, id=133, mimeType=video/avc, bitrate=261112, codecs=avc1.4d4015, res=426x240, fps=30.0, supported=YES
EventLogger: [X] Track:1, id=134, mimeType=video/avc, bitrate=671331, codecs=avc1.4d401e, res=640x360, fps=30.0, supported=YES
EventLogger: [X] Track:2, id=135, mimeType=video/avc, bitrate=1204535, codecs=avc1.4d401f, res=854x480, fps=30.0, supported=YES
EventLogger: [X] Track:3, id=160, mimeType=video/avc, bitrate=112329, codecs=avc1.4d400c, res=256x144, fps=30.0, supported=YES
EventLogger: [ ] Track:4, id=136, mimeType=video/avc, bitrate=2400538, codecs=avc1.4d401f, res=1280x720, fps=30.0, supported=NO_EXCEEDS_CAPABILITIES
EventLogger: ]
EventLogger: group [
EventLogger: [X] Track:0, id=133, mimeType=video/avc, bitrate=261112, codecs=avc1.4d4015, res=426x240, fps=30.0, supported=YES
EventLogger: [X] Track:1, id=134, mimeType=video/avc, bitrate=671331, codecs=avc1.4d401e, res=640x360, fps=30.0, supported=YES
EventLogger: [X] Track:2, id=135, mimeType=video/avc, bitrate=1204535, codecs=avc1.4d401f, res=854x480, fps=30.0, supported=YES
EventLogger: [X] Track:3, id=160, mimeType=video/avc, bitrate=112329, codecs=avc1.4d400c, res=256x144, fps=30.0, supported=YES
EventLogger: [ ] Track:4, id=136, mimeType=video/avc, bitrate=2400538, codecs=avc1.4d401f, res=1280x720, fps=30.0, supported=NO_EXCEEDS_CAPABILITIES
EventLogger: ]
EventLogger: MediaCodecAudioRenderer [
EventLogger: Group:0, adaptive_supported=YES_NOT_SEAMLESS [
EventLogger: [ ] Track:0, id=139, mimeType=audio/mp4a-latm, bitrate=48582, codecs=mp4a.40.5, channels=2, sample_rate=22050, supported=YES
EventLogger: [X] Track:1, id=140, mimeType=audio/mp4a-latm, bitrate=127868, codecs=mp4a.40.2, channels=2, sample_rate=44100, supported=YES
EventLogger: ]
EventLogger: group [
EventLogger: [ ] Track:0, id=139, mimeType=audio/mp4a-latm, bitrate=48582, codecs=mp4a.40.5, channels=2, sample_rate=22050, supported=YES
EventLogger: [X] Track:1, id=140, mimeType=audio/mp4a-latm, bitrate=127868, codecs=mp4a.40.2, channels=2, sample_rate=44100, supported=YES
EventLogger: ]
EventLogger: ]
```
......
......@@ -319,7 +319,8 @@ DataSource.Factory cacheDataSourceFactory =
ExoPlayer player = new ExoPlayer.Builder(context)
.setMediaSourceFactory(
new DefaultMediaSourceFactory(cacheDataSourceFactory))
new DefaultMediaSourceFactory(context)
.setDataSourceFactory(cacheDataSourceFactory))
.build();
~~~
{: .language-java}
......
......@@ -94,11 +94,18 @@ DrmSessionManager customDrmSessionManager =
new CustomDrmSessionManager(/* ... */);
// Pass a drm session manager provider to the media source factory.
MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory(dataSourceFactory)
new DefaultMediaSourceFactory(context)
.setDrmSessionManagerProvider(mediaItem -> customDrmSessionManager);
~~~
{: .language-java}
### Improving playback performance ###
If you're experiencing video stuttering on a device running Android 6 to 11 when
playing DRM protected content, you can try [enabling asynchronous buffer
queueing].
[main demo app]: {{ site.release_v2 }}/demos/main
[`MediaDrm`]: {{ site.android_sdk }}/android/media/MediaDrm.html
[used when building the player]: {{ site.baseurl }}/media-sources.html#customizing-media-source-creation
[enabling asynchronous buffer queueing]: {{ site.baseurl }}/customization.html#enabling-asynchronous-buffer-queueing
......@@ -88,8 +88,6 @@ public void onPlayerError(PlaybackException error) {
if (cause instanceof HttpDataSourceException) {
// An HTTP error occurred.
HttpDataSourceException httpError = (HttpDataSourceException) cause;
// This is the request for which the error occurred.
DataSpec requestDataSpec = httpError.dataSpec;
// It's possible to find out more about the error both by casting and by
// querying the cause.
if (httpError instanceof HttpDataSource.InvalidResponseCodeException) {
......@@ -193,12 +191,11 @@ logging purposes. It can be added to an `ExoPlayer` to enable useful
additional logging with a single line.
```
player.addAnalyticsListener(new EventLogger(trackSelector));
player.addAnalyticsListener(new EventLogger());
```
{: .language-java }
Passing the `trackSelector` enables additional logging, but is optional and so
`null` can be passed instead. See the [debug logging page][] for more details.
See the [debug logging page][] for more details.
## Firing events at specified playback positions ##
......
......@@ -16,13 +16,6 @@ and can only be played at one position. The documentation on this page is only
relevant to adaptive live streams.
{:.info}
ExoPlayer adjusts the live offset by slightly changing the playback speed.
The player will try to match user and media preferences, but will also try to
react to changing network conditions. For example, if rebuffers occur during
playback, the player will move further away from the live edge. If there is
enough available buffer over a longer period of time, the player will move
closer to the live edge again.
## Detecting and monitoring live playbacks ##
Every time a live window is updated, registered `Player.Listener` instances
......@@ -87,12 +80,16 @@ components to support additional modes when playing live streams.
## Configuring live playback parameters ##
By default, ExoPlayer uses live playback parameters defined by the media. If you
want to configure the live playback parameters yourself, you can set them on a
per `MediaItem` basis by calling `MediaItem.Builder.setLiveConfiguration`. If
you'd like to set these values globally for all items, you can set them on the
`DefaultMediaSourceFactory` provided to the player. In both cases, the provided
values will override parameters defined by the media.
ExoPlayer uses some parameters to control the offset of the playback position
from the live edge, and the range of playback speeds that can be used to
adjust this offset.
ExoPlayer gets values for these parameters from three places, in descending
order of priority (the first value found is used):
* Per `MediaItem` values passed to `MediaItem.Builder.setLiveConfiguration`.
* Global default values set on `DefaultMediaSourceFactory`.
* Values read directly from the media.
~~~
// Global settings.
......@@ -130,40 +127,31 @@ Available configuration values are:
* `maxPlaybackSpeed`: The maximum playback speed the player can use to catch up
when trying to reach the target live offset.
If automatic playback speed adjustment is not desired, it can be disabled by
setting `minPlaybackSpeed` and `maxPlaybackSpeed` to `1.0f`.
## Playback speed adjustment ##
## BehindLiveWindowException and ERROR_CODE_BEHIND_LIVE_WINDOW ##
The playback position may fall behind the live window, for example if the player
is paused or buffering for a long enough period of time. If this happens then
playback will fail and an exception with error code
`ERROR_CODE_BEHIND_LIVE_WINDOW` will be reported via
`Player.Listener.onPlayerError`. Application code may wish to handle such
errors by resuming playback at the default position. The [PlayerActivity][] of
the demo app exemplifies this approach.
When playing a low-latency live stream ExoPlayer adjusts the live offset by
slightly changing the playback speed. The player will try to match the target
live offset provided by the media or the app, but will also try to react to
changing network conditions. For example, if rebuffers occur during playback,
the player will slow down playback slightly to move further away from the live
edge. If the network then becomes stable enough to support playing closer to the
live edge again, the player will speed up playback to move back toward the
target live offset.
~~~
@Override
public void onPlayerError(PlaybackException error) {
if (eror.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
// Re-initialize player at the current live window default position.
player.seekToDefaultPosition();
player.prepare();
} else {
// Handle other errors.
}
}
~~~
{: .language-java}
If automatic playback speed adjustment is not desired, it can be disabled by
setting `minPlaybackSpeed` and `maxPlaybackSpeed` properties to `1.0f`.
Similarly, it can be enabled for non-low-latency live streams by setting these
explicitly to values other than `1.0f`. See
[the configuration section above](#configuring-live-playback-parameters) for
more details on how these properties can be set.
## Customizing the playback speed adjustment algorithm ##
### Customizing the playback speed adjustment algorithm ###
To stay close to the target live offset, a `LivePlaybackSpeedControl` is used to
make adjustments to the playback speed during live playbacks. It's possible to
implement a custom `LivePlaybackSpeedControl`, or to customize the default
implementation, which is `DefaultLivePlaybackSpeedControl`. In both cases an
instance can be set when building the player:
If speed adjustment is enabled, a `LivePlaybackSpeedControl` defines what
adjustments are made. It's possible to implement a custom
`LivePlaybackSpeedControl`, or to customize the default implementation, which is
`DefaultLivePlaybackSpeedControl`. In both cases an instance can be set when
building the player:
~~~
ExoPlayer player =
......@@ -195,6 +183,30 @@ Relevant customization parameters of `DefaultLivePlaybackSpeedControl` are:
a lower value means the estimation will adjust faster at a higher risk of
running into rebuffers.
## BehindLiveWindowException and ERROR_CODE_BEHIND_LIVE_WINDOW ##
The playback position may fall behind the live window, for example if the player
is paused or buffering for a long enough period of time. If this happens then
playback will fail and an exception with error code
`ERROR_CODE_BEHIND_LIVE_WINDOW` will be reported via
`Player.Listener.onPlayerError`. Application code may wish to handle such
errors by resuming playback at the default position. The [PlayerActivity][] of
the demo app exemplifies this approach.
~~~
@Override
public void onPlayerError(PlaybackException error) {
if (eror.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
// Re-initialize player at the current live window default position.
player.seekToDefaultPosition();
player.prepare();
} else {
// Handle other errors.
}
}
~~~
{: .language-java}
[Supported Formats page]: {{ site.baseurl }}/supported-formats.html
[default UI components]: {{ site.baseurl }}/ui-components.html
[pending feature request (#2213)]: https://github.com/google/ExoPlayer/issues/2213
......
......@@ -34,9 +34,10 @@ these requirements and injected during player construction:
~~~
MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory(cacheDataSourceFactory)
.setAdsLoaderProvider(adsLoaderProvider)
.setAdViewProvider(playerView);
new DefaultMediaSourceFactory(context)
.setDataSourceFactory(cacheDataSourceFactory)
.setLocalAdInsertionComponents(
adsLoaderProvider, /* adViewProvider= */ playerView);
ExoPlayer player = new ExoPlayer.Builder(context)
.setMediaSourceFactory(mediaSourceFactory)
.build();
......
......@@ -22,14 +22,14 @@ that corresponds to the network stack you wish to use. If your application also
needs to play non-http(s) content such as local files, use
~~~
new DefaultDataSourceFactory(
new DefaultDataSource.Factory(
...
/* baseDataSourceFactory= */ new PreferredHttpDataSource.Factory(...));
~~~
{: .language-java}
where `PreferredHttpDataSource.Factory` is the factory corresponding to your
preferred network stack. The `DefaultDataSourceFactory` layer adds in support
preferred network stack. The `DefaultDataSource.Factory` layer adds in support
for non-http(s) sources such as local files.
The example below shows how to build an `ExoPlayer` that will use the Cronet
......@@ -48,10 +48,12 @@ DefaultDataSource.Factory dataSourceFactory =
context,
/* baseDataSourceFactory= */ cronetDataSourceFactory);
// Inject the DefaultDataSourceFactory when creating the player.
// Inject the DefaultDataSource.Factory when creating the player.
ExoPlayer player =
new ExoPlayer.Builder(context)
.setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory))
.setMediaSourceFactory(
new DefaultMediaSourceFactory(context)
.setDataSourceFactory(dataSourceFactory))
.build();
~~~
{: .language-java}
......
......@@ -65,7 +65,7 @@ app is notified of events via the listener passed to the `Transformer` builder.
Transformer.Listener transformerListener =
new Transformer.Listener() {
@Override
public void onTransformationCompleted(MediaItem inputMediaItem) {
public void onTransformationCompleted(MediaItem inputMediaItem, TransformationResult transformationResult) {
playOutput();
}
......
......@@ -22,6 +22,7 @@ redirect_from:
* [Why does content fail to play, but no error is surfaced?]
* [How can I get a decoding extension to load and be used for playback?][]
* [Can I play YouTube videos directly with ExoPlayer?][]
* [Video playback is stuttering][]
---
......@@ -293,6 +294,16 @@ No, ExoPlayer cannot play videos from YouTube, i.e., urls of the form
Android Player API](https://developers.google.com/youtube/android/player/) which
is the official way to play YouTube videos on Android.
#### Video playback is stuttering ###
The device may not be able to decode the content fast enough if, for example,
the content bitrate or resolution exceeds the device capabilities. You may need
to use lower quality content to obtain good performance on such devices.
If you're experiencing video stuttering on a device running Android 6 to 11,
particularly when playing DRM protected or high frame rate content, you can try
[enabling asynchronous buffer queueing].
[Fixing "Cleartext HTTP traffic not permitted" errors]: #fixing-cleartext-http-traffic-not-permitted-errors
[Fixing "SSLHandshakeException", "CertPathValidatorException" and "ERR_CERT_AUTHORITY_INVALID" errors]: #fixing-sslhandshakeexception-certpathvalidatorexception-and-err_cert_authority_invalid-errors
[What formats does ExoPlayer support?]: #what-formats-does-exoplayer-support
......@@ -311,7 +322,7 @@ is the official way to play YouTube videos on Android.
[Why does content fail to play, but no error is surfaced?]: #why-does-content-fail-to-play-but-no-error-is-surfaced
[How can I get a decoding extension to load and be used for playback?]: #how-can-i-get-a-decoding-extension-to-load-and-be-used-for-playback
[Can I play YouTube videos directly with ExoPlayer?]: #can-i-play-youtube-videos-directly-with-exoplayer
[Video playback is stuttering]: #video-playback-is-stuttering
[Supported formats]: {{ site.baseurl }}/supported-formats.html
[set on a `DefaultExtractorsFactory`]: {{ site.base_url }}/customization.html#customizing-extractor-flags
......@@ -349,3 +360,4 @@ is the official way to play YouTube videos on Android.
[`DefaultRenderersFactory`]: {{ site.exo_sdk }}/DefaultRenderersFactory.html
[`LibraryLoader`]: {{ site.exo_sdk }}/util/LibraryLoader.html
[`EventLogger`]: {{ site.baseurl }}/debug-logging.html
[enabling asynchronous buffer queueing]: {{ site.baseurl }}/customization.html#enabling-asynchronous-buffer-queueing
......@@ -37,19 +37,15 @@ import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.TracksInfo;
import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.text.CueGroup;
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ListenerSet;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoSize;
import com.google.android.gms.cast.CastStatusCodes;
......@@ -67,7 +63,6 @@ import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.common.collect.ImmutableList;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
......@@ -105,7 +100,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;
......@@ -113,13 +108,7 @@ public final class CastPlayer extends BasePlayer {
private static final String TAG = "CastPlayer";
private static final int RENDERER_COUNT = 3;
private static final int RENDERER_INDEX_VIDEO = 0;
private static final int RENDERER_INDEX_AUDIO = 1;
private static final int RENDERER_INDEX_TEXT = 2;
private static final long PROGRESS_REPORT_PERIOD_MS = 1000;
private static final TrackSelectionArray EMPTY_TRACK_SELECTION_ARRAY =
new TrackSelectionArray(null, null, null);
private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
private final CastContext castContext;
......@@ -144,9 +133,7 @@ public final class CastPlayer extends BasePlayer {
private final StateHolder<PlaybackParameters> playbackParameters;
@Nullable private RemoteMediaClient remoteMediaClient;
private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups;
private TrackSelectionArray currentTrackSelection;
private TracksInfo currentTracksInfo;
private Tracks currentTracks;
private Commands availableCommands;
private @Player.State int playbackState;
private int currentWindowIndex;
......@@ -222,9 +209,7 @@ public final class CastPlayer extends BasePlayer {
playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT);
playbackState = STATE_IDLE;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
currentTrackGroups = TrackGroupArray.EMPTY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
currentTracksInfo = TracksInfo.EMPTY;
currentTracks = Tracks.EMPTY;
availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build();
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
......@@ -471,6 +456,11 @@ public final class CastPlayer extends BasePlayer {
stop(/* reset= */ false);
}
/**
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
* {@link #prepare() re-preparing} the player.
*/
@Deprecated
@Override
public void stop(boolean reset) {
......@@ -556,18 +546,8 @@ public final class CastPlayer extends BasePlayer {
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
return currentTrackGroups;
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return currentTrackSelection;
}
@Override
public TracksInfo getCurrentTracksInfo() {
return currentTracksInfo;
public Tracks getCurrentTracks() {
return currentTracks;
}
@Override
......@@ -728,10 +708,10 @@ public final class CastPlayer extends BasePlayer {
return VideoSize.UNKNOWN;
}
/** This method is not supported and returns an empty list. */
/** This method is not supported and returns an empty {@link CueGroup}. */
@Override
public ImmutableList<Cue> getCurrentCues() {
return ImmutableList.of();
public CueGroup getCurrentCues() {
return CueGroup.EMPTY;
}
/** This method is not supported and always returns {@link DeviceInfo#UNKNOWN}. */
......@@ -840,10 +820,7 @@ public final class CastPlayer extends BasePlayer {
}
if (updateTracksAndSelectionsAndNotifyIfChanged()) {
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED,
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection));
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksInfoChanged(currentTracksInfo));
Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(currentTracks));
}
updateAvailableCommandsAndNotifyIfChanged();
listeners.flushEvents();
......@@ -998,55 +975,33 @@ public final class CastPlayer extends BasePlayer {
return false;
}
MediaStatus mediaStatus = getMediaStatus();
MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null;
@Nullable MediaStatus mediaStatus = getMediaStatus();
@Nullable MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null;
@Nullable
List<MediaTrack> castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null;
if (castMediaTracks == null || castMediaTracks.isEmpty()) {
boolean hasChanged = !currentTrackGroups.isEmpty();
currentTrackGroups = TrackGroupArray.EMPTY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
currentTracksInfo = TracksInfo.EMPTY;
boolean hasChanged = !Tracks.EMPTY.equals(currentTracks);
currentTracks = Tracks.EMPTY;
return hasChanged;
}
long[] activeTrackIds = mediaStatus.getActiveTrackIds();
@Nullable long[] activeTrackIds = mediaStatus.getActiveTrackIds();
if (activeTrackIds == null) {
activeTrackIds = EMPTY_TRACK_ID_ARRAY;
}
TrackGroup[] trackGroups = new TrackGroup[castMediaTracks.size()];
@NullableType TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT];
TracksInfo.TrackGroupInfo[] trackGroupInfos =
new TracksInfo.TrackGroupInfo[castMediaTracks.size()];
Tracks.Group[] trackGroups = new Tracks.Group[castMediaTracks.size()];
for (int i = 0; i < castMediaTracks.size(); i++) {
MediaTrack mediaTrack = castMediaTracks.get(i);
trackGroups[i] =
TrackGroup trackGroup =
new TrackGroup(/* id= */ Integer.toString(i), CastUtils.mediaTrackToFormat(mediaTrack));
long id = mediaTrack.getId();
@C.TrackType int trackType = MimeTypes.getTrackType(mediaTrack.getContentType());
int rendererIndex = getRendererIndexForTrackType(trackType);
boolean supported = rendererIndex != C.INDEX_UNSET;
boolean selected =
isTrackActive(id, activeTrackIds) && supported && trackSelections[rendererIndex] == null;
if (selected) {
trackSelections[rendererIndex] = new CastTrackSelection(trackGroups[i]);
}
@C.FormatSupport
int[] trackSupport = new int[] {supported ? C.FORMAT_HANDLED : C.FORMAT_UNSUPPORTED_TYPE};
final boolean[] trackSelected = new boolean[] {selected};
trackGroupInfos[i] =
new TracksInfo.TrackGroupInfo(trackGroups[i], trackSupport, trackType, trackSelected);
}
TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups);
TrackSelectionArray newTrackSelections = new TrackSelectionArray(trackSelections);
TracksInfo newTracksInfo = new TracksInfo(ImmutableList.copyOf(trackGroupInfos));
if (!newTrackGroups.equals(currentTrackGroups)
|| !newTrackSelections.equals(currentTrackSelection)
|| !newTracksInfo.equals(currentTracksInfo)) {
currentTrackSelection = newTrackSelections;
currentTrackGroups = newTrackGroups;
currentTracksInfo = newTracksInfo;
@C.FormatSupport int[] trackSupport = new int[] {C.FORMAT_HANDLED};
boolean[] trackSelected = new boolean[] {isTrackActive(mediaTrack.getId(), activeTrackIds)};
trackGroups[i] =
new Tracks.Group(trackGroup, /* adaptiveSupported= */ false, trackSupport, trackSelected);
}
Tracks newTracks = new Tracks(ImmutableList.copyOf(trackGroups));
if (!newTracks.equals(currentTracks)) {
currentTracks = newTracks;
return true;
}
return false;
......@@ -1304,14 +1259,6 @@ public final class CastPlayer extends BasePlayer {
return false;
}
private static int getRendererIndexForTrackType(@C.TrackType int trackType) {
return trackType == C.TRACK_TYPE_VIDEO
? RENDERER_INDEX_VIDEO
: trackType == C.TRACK_TYPE_AUDIO
? RENDERER_INDEX_AUDIO
: trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT : C.INDEX_UNSET;
}
private static int getCastRepeatMode(@RepeatMode int repeatMode) {
switch (repeatMode) {
case REPEAT_MODE_ONE:
......
/*
* 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 com.google.android.exoplayer2.ext.cast;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.util.Assertions;
/**
* {@link TrackSelection} that only selects the first track of the provided {@link TrackGroup}.
*
* <p>This relies on {@link CastPlayer} track groups only having one track.
*/
/* package */ class CastTrackSelection implements TrackSelection {
private final TrackGroup trackGroup;
/** @param trackGroup The {@link TrackGroup} from which the first track will only be selected. */
public CastTrackSelection(TrackGroup trackGroup) {
this.trackGroup = trackGroup;
}
@Override
public int getType() {
return TYPE_UNSET;
}
@Override
public TrackGroup getTrackGroup() {
return trackGroup;
}
@Override
public int length() {
return 1;
}
@Override
public Format getFormat(int index) {
Assertions.checkArgument(index == 0);
return trackGroup.getFormat(0);
}
@Override
public int getIndexInTrackGroup(int index) {
return index == 0 ? 0 : C.INDEX_UNSET;
}
@Override
@SuppressWarnings("ReferenceEquality")
public int indexOf(Format format) {
return format == trackGroup.getFormat(0) ? 0 : C.INDEX_UNSET;
}
@Override
public int indexOf(int indexInTrackGroup) {
return indexInTrackGroup == 0 ? 0 : C.INDEX_UNSET;
}
// Object overrides.
@Override
public int hashCode() {
return System.identityHashCode(trackGroup);
}
// Track groups are compared by identity not value, as distinct groups may have the same value.
@Override
@SuppressWarnings({"ReferenceEquality", "EqualsGetClass"})
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
CastTrackSelection other = (CastTrackSelection) obj;
return trackGroup == other.trackGroup;
}
}
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.cast;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Test for {@link CastTrackSelection}. */
@RunWith(AndroidJUnit4.class)
public class CastTrackSelectionTest {
private static final TrackGroup TRACK_GROUP =
new TrackGroup(new Format.Builder().build(), new Format.Builder().build());
private static final CastTrackSelection SELECTION = new CastTrackSelection(TRACK_GROUP);
@Test
public void length_isOne() {
assertThat(SELECTION.length()).isEqualTo(1);
}
@Test
public void getTrackGroup_returnsSameGroup() {
assertThat(SELECTION.getTrackGroup()).isSameInstanceAs(TRACK_GROUP);
}
@Test
public void getFormatSelectedTrack_isFirstTrack() {
assertThat(SELECTION.getFormat(0)).isSameInstanceAs(TRACK_GROUP.getFormat(0));
}
@Test
public void getIndexInTrackGroup_ofSelectedTrack_returnsFirstTrack() {
assertThat(SELECTION.getIndexInTrackGroup(0)).isEqualTo(0);
}
@Test
public void getIndexInTrackGroup_onePastTheEnd_returnsIndexUnset() {
assertThat(SELECTION.getIndexInTrackGroup(1)).isEqualTo(C.INDEX_UNSET);
}
@Test
public void indexOf_selectedTrack_returnsFirstTrack() {
assertThat(SELECTION.indexOf(0)).isEqualTo(0);
}
@Test
public void indexOf_onePastTheEnd_returnsIndexUnset() {
assertThat(SELECTION.indexOf(1)).isEqualTo(C.INDEX_UNSET);
}
@Test(expected = Exception.class)
public void getFormat_outOfBound_throws() {
CastTrackSelection selection = new CastTrackSelection(TRACK_GROUP);
selection.getFormat(1);
}
}
......@@ -38,8 +38,9 @@ If your application only needs to play http(s) content, using the Cronet
extension is as simple as updating `DataSource.Factory` instantiations in your
application code to use `CronetDataSource.Factory`. If your application also
needs to play non-http(s) content such as local files, use:
```
new DefaultDataSourceFactory(
new DefaultDataSource.Factory(
...
/* baseDataSourceFactory= */ new CronetDataSource.Factory(...) );
```
......
......@@ -343,7 +343,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
*/
public final int cronetConnectionStatus;
/** @deprecated Use {@link #OpenException(IOException, DataSpec, int, int)}. */
/**
* @deprecated Use {@link #OpenException(IOException, DataSpec, int, int)}.
*/
@Deprecated
public OpenException(IOException cause, DataSpec dataSpec, int cronetConnectionStatus) {
super(cause, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, TYPE_OPEN);
......@@ -359,7 +361,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
this.cronetConnectionStatus = cronetConnectionStatus;
}
/** @deprecated Use {@link #OpenException(String, DataSpec, int, int)}. */
/**
* @deprecated Use {@link #OpenException(String, DataSpec, int, int)}.
*/
@Deprecated
public OpenException(String errorMessage, DataSpec dataSpec, int cronetConnectionStatus) {
super(errorMessage, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, TYPE_OPEN);
......@@ -461,11 +465,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
}
/**
* Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
* {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
*
* @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
* predicate that was previously set.
* @deprecated Use {@link CronetDataSource.Factory#setContentTypePredicate(Predicate)} instead.
*/
@Deprecated
public void setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
......
......@@ -25,7 +25,9 @@ import com.google.android.exoplayer2.upstream.TransferListener;
import java.util.concurrent.Executor;
import org.chromium.net.CronetEngine;
/** @deprecated Use {@link CronetDataSource.Factory} instead. */
/**
* @deprecated Use {@link CronetDataSource.Factory} instead.
*/
@Deprecated
public final class CronetDataSourceFactory extends BaseFactory {
......
......@@ -17,7 +17,8 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
// failures if ffmpeg hasn't been built according to the README instructions.
if (project.file('src/main/jni/ffmpeg').exists()) {
android.externalNativeBuild.cmake.path = 'src/main/jni/CMakeLists.txt'
android.externalNativeBuild.cmake.version = '3.7.1+'
// Should match cmake_minimum_required.
android.externalNativeBuild.cmake.version = '3.21.0+'
}
dependencies {
......
......@@ -48,7 +48,7 @@ public final class FfmpegLibrary {
/**
* Override the names of the FFmpeg native libraries. If an application wishes to call this
* method, it must do so before calling any other method defined by this class, and before
* instantiating a {@link FfmpegAudioRenderer} instance.
* instantiating a {@link FfmpegAudioRenderer} or {@link FfmpegVideoRenderer} instance.
*
* @param libraries The names of the FFmpeg native libraries.
*/
......@@ -146,6 +146,10 @@ public final class FfmpegLibrary {
return "pcm_mulaw";
case MimeTypes.AUDIO_ALAW:
return "pcm_alaw";
case MimeTypes.VIDEO_H264:
return "h264";
case MimeTypes.VIDEO_H265:
return "hevc";
default:
return null;
}
......
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.ffmpeg;
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_MIME_TYPE_CHANGED;
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO;
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION;
import android.os.Handler;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.decoder.CryptoConfig;
import com.google.android.exoplayer2.decoder.Decoder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
import com.google.android.exoplayer2.decoder.VideoDecoderOutputBuffer;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.DecoderVideoRenderer;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
// TODO: Remove the NOTE below.
/**
* <b>NOTE: This class if under development and is not yet functional.</b>
*
* <p>Decodes and renders video using FFmpeg.
*/
public final class FfmpegVideoRenderer extends DecoderVideoRenderer {
private static final String TAG = "FfmpegVideoRenderer";
/**
* Creates a new instance.
*
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
*/
public FfmpegVideoRenderer(
long allowedJoiningTimeMs,
@Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
super(allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify);
// TODO: Implement.
}
@Override
public String getName() {
return TAG;
}
@Override
public final @RendererCapabilities.Capabilities int supportsFormat(Format format) {
// TODO: Remove this line and uncomment the implementation below.
return C.FORMAT_UNSUPPORTED_TYPE;
/*
String mimeType = Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isVideo(mimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType)) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE);
} else if (format.exoMediaCryptoType != null) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM);
} else {
return RendererCapabilities.create(
FORMAT_HANDLED,
ADAPTIVE_SEAMLESS,
TUNNELING_NOT_SUPPORTED);
}
*/
}
@SuppressWarnings("nullness:return")
@Override
protected Decoder<DecoderInputBuffer, VideoDecoderOutputBuffer, FfmpegDecoderException>
createDecoder(Format format, @Nullable CryptoConfig cryptoConfig)
throws FfmpegDecoderException {
TraceUtil.beginSection("createFfmpegVideoDecoder");
// TODO: Implement, remove the SuppressWarnings annotation, and update the return type to use
// the concrete type of the decoder (probably FfmepgVideoDecoder).
TraceUtil.endSection();
return null;
}
@Override
protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface)
throws FfmpegDecoderException {
// TODO: Implement.
}
@Override
protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) {
// TODO: Uncomment the implementation below.
/*
if (decoder != null) {
decoder.setOutputMode(outputMode);
}
*/
}
@Override
protected DecoderReuseEvaluation canReuseDecoder(
String decoderName, Format oldFormat, Format newFormat) {
boolean sameMimeType = Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType);
// TODO: Ability to reuse the decoder may be MIME type dependent.
return new DecoderReuseEvaluation(
decoderName,
oldFormat,
newFormat,
sameMimeType ? REUSE_RESULT_YES_WITHOUT_RECONFIGURATION : REUSE_RESULT_NO,
sameMimeType ? 0 : DISCARD_REASON_MIME_TYPE_CHANGED);
}
}
......@@ -14,7 +14,7 @@
# limitations under the License.
#
cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
cmake_minimum_required(VERSION 3.21.0 FATAL_ERROR)
# Enable C++11 features.
set(CMAKE_CXX_STANDARD 11)
......
......@@ -22,7 +22,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}.
* Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer} and {@link
* FfmpegVideoRenderer}.
*/
@RunWith(AndroidJUnit4.class)
public final class DefaultRenderersFactoryTest {
......@@ -32,4 +33,10 @@ public final class DefaultRenderersFactoryTest {
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO);
}
@Test
public void createRenderers_instantiatesFfmpegVideoRenderer() {
DefaultRenderersFactoryAsserts.assertExtensionRendererCreated(
FfmpegVideoRenderer.class, C.TRACK_TYPE_VIDEO);
}
}
......@@ -42,9 +42,7 @@ import com.google.android.exoplayer2.testutil.ExoHostedTest;
import com.google.android.exoplayer2.testutil.HostActivity;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
......@@ -235,14 +233,13 @@ public final class ImaPlaybackTest {
protected MediaSource buildSource(
HostActivity host, DrmSessionManager drmSessionManager, FrameLayout overlayFrameLayout) {
Context context = host.getApplicationContext();
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context);
MediaSource contentMediaSource =
new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(contentUri));
return new AdsMediaSource(
contentMediaSource,
adTagDataSpec,
/* adsId= */ adTagDataSpec.uri,
new DefaultMediaSourceFactory(dataSourceFactory),
new DefaultMediaSourceFactory(context),
Assertions.checkNotNull(imaAdsLoader),
() -> overlayFrameLayout);
}
......
......@@ -84,12 +84,14 @@ import java.util.Map;
private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.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) {
......
......@@ -503,11 +503,11 @@ public final class ImaAdsLoader implements AdsLoader {
List<String> supportedMimeTypes = new ArrayList<>();
for (@C.ContentType int contentType : contentTypes) {
// IMA does not support Smooth Streaming ad media.
if (contentType == C.TYPE_DASH) {
if (contentType == C.CONTENT_TYPE_DASH) {
supportedMimeTypes.add(MimeTypes.APPLICATION_MPD);
} else if (contentType == C.TYPE_HLS) {
} else if (contentType == C.CONTENT_TYPE_HLS) {
supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8);
} else if (contentType == C.TYPE_OTHER) {
} else if (contentType == C.CONTENT_TYPE_OTHER) {
supportedMimeTypes.addAll(
Arrays.asList(
MimeTypes.VIDEO_MP4,
......
......@@ -17,6 +17,8 @@ package com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.expandAdGroupPlaceholder;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.getAdGroupAndIndexInMultiPeriodWindow;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToMsRounded;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.secToUsRounded;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdPlaybackStateForPeriods;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.updateAdDurationAndPropagate;
import static com.google.android.exoplayer2.ext.ima.ImaUtil.updateAdDurationInAdGroup;
......@@ -24,7 +26,6 @@ import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.msToUs;
import static com.google.android.exoplayer2.util.Util.secToUs;
import static com.google.android.exoplayer2.util.Util.sum;
import static com.google.android.exoplayer2.util.Util.usToMs;
import static java.lang.Math.min;
......@@ -133,14 +134,14 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Override
public MediaSource.Factory setLoadErrorHandlingPolicy(
@Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
contentMediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
return this;
}
@Override
public MediaSource.Factory setDrmSessionManagerProvider(
@Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
DrmSessionManagerProvider drmSessionManagerProvider) {
contentMediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
return this;
}
......@@ -325,23 +326,31 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putSerializable(keyForField(FIELD_AD_PLAYBACK_STATES), adPlaybackStates);
Bundle adPlaybackStatesBundle = new Bundle();
for (Map.Entry<String, AdPlaybackState> entry : adPlaybackStates.entrySet()) {
adPlaybackStatesBundle.putBundle(entry.getKey(), entry.getValue().toBundle());
}
bundle.putBundle(keyForField(FIELD_AD_PLAYBACK_STATES), adPlaybackStatesBundle);
return bundle;
}
/** Object that can restore {@link AdsLoader.State} from a {@link Bundle}. */
public static final Bundleable.Creator<State> CREATOR = State::fromBundle;
@SuppressWarnings("unchecked")
private static State fromBundle(Bundle bundle) {
@Nullable
Map<String, AdPlaybackState> adPlaybackStateMap =
(Map<String, AdPlaybackState>)
bundle.getSerializable(keyForField(FIELD_AD_PLAYBACK_STATES));
return new State(
adPlaybackStateMap != null
? ImmutableMap.copyOf(adPlaybackStateMap)
: ImmutableMap.of());
ImmutableMap.Builder<String, AdPlaybackState> adPlaybackStateMap =
new ImmutableMap.Builder<>();
Bundle adPlaybackStateBundle =
checkNotNull(bundle.getBundle(keyForField(FIELD_AD_PLAYBACK_STATES)));
for (String key : adPlaybackStateBundle.keySet()) {
AdPlaybackState adPlaybackState =
AdPlaybackState.CREATOR.fromBundle(
checkNotNull(adPlaybackStateBundle.getBundle(key)));
adPlaybackStateMap.put(
key, AdPlaybackState.fromAdPlaybackState(/* adsId= */ key, adPlaybackState));
}
return new State(adPlaybackStateMap.buildOrThrow());
}
private static String keyForField(@FieldNumber int field) {
......@@ -596,7 +605,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
}
@MainThread
@EnsuresNonNull("contentTimeline")
@EnsuresNonNull("this.contentTimeline")
private void setContentTimeline(Timeline contentTimeline) {
if (contentTimeline.equals(this.contentTimeline)) {
return;
......@@ -651,19 +660,29 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
private static AdPlaybackState setVodAdGroupPlaceholders(
List<CuePoint> cuePoints, AdPlaybackState adPlaybackState) {
// TODO(b/192231683) Use getEndTimeMs()/getStartTimeMs() after jar target was removed
for (int i = 0; i < cuePoints.size(); i++) {
CuePoint cuePoint = cuePoints.get(i);
long fromPositionUs = msToUs(secToMsRounded(cuePoint.getStartTime()));
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ secToUs(cuePoint.getStartTime()),
/* fromPositionUs= */ fromPositionUs,
/* contentResumeOffsetUs= */ 0,
// TODO(b/192231683) Use getEndTimeMs()/getStartTimeMs() after jar target was removed
/* adDurationsUs...= */ secToUs(cuePoint.getEndTime() - cuePoint.getStartTime()));
/* adDurationsUs...= */ getAdDuration(
/* startTimeSeconds= */ cuePoint.getStartTime(),
/* endTimeSeconds= */ cuePoint.getEndTime()));
}
return adPlaybackState;
}
private static long getAdDuration(double startTimeSeconds, double endTimeSeconds) {
// startTimeSeconds and endTimeSeconds that are coming from the SDK, only have a precision of
// milliseconds so everything that is below a millisecond can be safely considered as coming
// from rounding issues.
return msToUs(secToMsRounded(endTimeSeconds - startTimeSeconds));
}
private static AdPlaybackState setVodAdInPlaceholder(Ad ad, AdPlaybackState adPlaybackState) {
AdPodInfo adPodInfo = ad.getAdPodInfo();
// Handle post rolls that have a podIndex of -1.
......@@ -675,9 +694,9 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
adPlaybackState =
expandAdGroupPlaceholder(
adGroupIndex,
/* adGroupDurationUs= */ secToUs(adPodInfo.getMaxDuration()),
/* adGroupDurationUs= */ msToUs(secToMsRounded(adPodInfo.getMaxDuration())),
adIndexInAdGroup,
/* adDurationUs= */ secToUs(ad.getDuration()),
/* adDurationUs= */ msToUs(secToMsRounded(ad.getDuration())),
/* adsInAdGroupCount= */ adPodInfo.getTotalAds(),
adPlaybackState);
} else if (adIndexInAdGroup < adGroup.count - 1) {
......@@ -685,7 +704,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
updateAdDurationInAdGroup(
adGroupIndex,
adIndexInAdGroup,
/* adDurationUs= */ secToUs(ad.getDuration()),
/* adDurationUs= */ msToUs(secToMsRounded(ad.getDuration())),
adPlaybackState);
}
return adPlaybackState;
......@@ -694,7 +713,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
private AdPlaybackState addLiveAdBreak(
Ad ad, long currentPeriodPositionUs, AdPlaybackState adPlaybackState) {
AdPodInfo adPodInfo = ad.getAdPodInfo();
long adDurationUs = secToUs(ad.getDuration());
long adDurationUs = secToUsRounded(ad.getDuration());
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
// TODO(b/208398934) Support seeking backwards.
if (adIndexInAdGroup == 0 || adPlaybackState.adGroupCount == 1) {
......@@ -708,7 +727,7 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou
new long[adCount],
adIndexInAdGroup,
adDurationUs,
secToUs(adPodInfo.getMaxDuration()));
msToUs(secToMsRounded(adPodInfo.getMaxDuration())));
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
......
......@@ -72,7 +72,7 @@ public final class ImaServerSideAdInsertionUriBuilder {
public ImaServerSideAdInsertionUriBuilder() {
adTagParameters = ImmutableMap.of();
loadVideoTimeoutMs = DEFAULT_LOAD_VIDEO_TIMEOUT_MS;
format = C.TYPE_OTHER;
format = C.CONTENT_TYPE_OTHER;
}
/**
......@@ -137,11 +137,11 @@ public final class ImaServerSideAdInsertionUriBuilder {
/**
* Sets the format of the stream request.
*
* @param format VOD or live stream type.
* @param format {@link C#TYPE_DASH} or {@link C#TYPE_HLS}.
* @return This instance, for convenience.
*/
public ImaServerSideAdInsertionUriBuilder setFormat(@ContentType int format) {
checkArgument(format == C.TYPE_DASH || format == C.TYPE_HLS);
checkArgument(format == C.CONTENT_TYPE_DASH || format == C.CONTENT_TYPE_HLS);
this.format = format;
return this;
}
......@@ -243,7 +243,7 @@ public final class ImaServerSideAdInsertionUriBuilder {
|| (!TextUtils.isEmpty(assetKey)
&& TextUtils.isEmpty(contentSourceId)
&& TextUtils.isEmpty(videoId)));
checkState(format != C.TYPE_OTHER);
checkState(format != C.CONTENT_TYPE_OTHER);
@Nullable String adsId = this.adsId;
if (adsId == null) {
adsId = assetKey != null ? assetKey : checkNotNull(videoId);
......@@ -330,9 +330,9 @@ public final class ImaServerSideAdInsertionUriBuilder {
.createVodStreamRequest(checkNotNull(contentSourceId), checkNotNull(videoId), apiKey);
}
int format = Integer.parseInt(uri.getQueryParameter(FORMAT));
if (format == C.TYPE_DASH) {
if (format == C.CONTENT_TYPE_DASH) {
streamRequest.setFormat(StreamFormat.DASH);
} else if (format == C.TYPE_HLS) {
} else if (format == C.CONTENT_TYPE_HLS) {
streamRequest.setFormat(StreamFormat.HLS);
} else {
throw new IllegalArgumentException("Unsupported stream format:" + format);
......
......@@ -54,7 +54,10 @@ import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.math.DoubleMath;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
......@@ -398,17 +401,13 @@ import java.util.Set;
long elapsedAdGroupAdDurationUs = 0;
for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) {
contentTimeline.getPeriod(j, period, /* setIds= */ true);
// TODO(b/192231683) Remove subtracted US from ad group time when we can upgrade the SDK.
// Subtract one microsecond to work around rounding errors with adGroup.timeUs.
if (totalElapsedContentDurationUs < adGroup.timeUs - 1) {
if (totalElapsedContentDurationUs < adGroup.timeUs) {
// Period starts before the ad group, so it is a content period.
adPlaybackStates.put(checkNotNull(period.uid), contentOnlyAdPlaybackState);
totalElapsedContentDurationUs += period.durationUs;
} else {
long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs;
// TODO(b/192231683) Remove additional US when we can upgrade the SDK.
// Add one microsecond to work around rounding errors with adGroup.timeUs.
if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs + 1) {
if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs) {
// The period ends before the end of the ad group, so it is an ad period (Note: A VOD ad
// reported by the IMA SDK spans multiple periods before the LOADED event arrives).
adPlaybackStates.put(
......@@ -490,16 +489,12 @@ import java.util.Set;
long elapsedAdGroupAdDurationUs = 0;
for (int j = periodIndex; j < contentTimeline.getPeriodCount(); j++) {
contentTimeline.getPeriod(j, period, /* setIds= */ true);
// TODO(b/192231683) Remove subtracted US from ad group time when we can upgrade the SDK.
// Subtract one microsecond to work around rounding errors with adGroup.timeUs.
if (totalElapsedContentDurationUs < adGroup.timeUs - 1) {
if (totalElapsedContentDurationUs < adGroup.timeUs) {
// Period starts before the ad group, so it is a content period.
totalElapsedContentDurationUs += period.durationUs;
} else {
long periodStartUs = totalElapsedContentDurationUs + elapsedAdGroupAdDurationUs;
// TODO(b/192231683) Remove additional US when we can upgrade the SDK.
// Add one microsecond to work around rounding errors with adGroup.timeUs.
if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs + 1) {
if (periodStartUs + period.durationUs <= adGroup.timeUs + adGroupDurationUs) {
// The period ends before the end of the ad group, so it is an ad period.
if (j == adPeriodIndex) {
return new Pair<>(/* adGroupIndex= */ i, adIndexInAdGroup);
......@@ -518,5 +513,31 @@ import java.util.Set;
throw new IllegalStateException();
}
/**
* Converts a time in seconds to the corresponding time in microseconds.
*
* <p>Fractional values are rounded to the nearest microsecond using {@link RoundingMode#HALF_UP}.
*
* @param timeSec The time in seconds.
* @return The corresponding time in microseconds.
*/
public static long secToUsRounded(double timeSec) {
return DoubleMath.roundToLong(
BigDecimal.valueOf(timeSec).scaleByPowerOfTen(6).doubleValue(), RoundingMode.HALF_UP);
}
/**
* Converts a time in seconds to the corresponding time in milliseconds.
*
* <p>Fractional values are rounded to the nearest millisecond using {@link RoundingMode#HALF_UP}.
*
* @param timeSec The time in seconds.
* @return The corresponding time in milliseconds.
*/
public static long secToMsRounded(double timeSec) {
return DoubleMath.roundToLong(
BigDecimal.valueOf(timeSec).scaleByPowerOfTen(3).doubleValue(), RoundingMode.HALF_UP);
}
private ImaUtil() {}
}
......@@ -24,7 +24,7 @@ import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.TracksInfo;
import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.testutil.StubExoPlayer;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
......@@ -79,7 +79,7 @@ import com.google.android.exoplayer2.util.Util;
PositionInfo oldPosition =
new PositionInfo(
windowUid,
/* windowIndex= */ 0,
/* mediaItemIndex= */ 0,
mediaItem,
periodUid,
/* periodIndex= */ 0,
......@@ -97,7 +97,7 @@ import com.google.android.exoplayer2.util.Util;
PositionInfo newPosition =
new PositionInfo(
windowUid,
/* windowIndex= */ 0,
/* mediaItemIndex= */ 0,
mediaItem,
periodUid,
/* periodIndex= */ 0,
......@@ -128,7 +128,7 @@ import com.google.android.exoplayer2.util.Util;
PositionInfo oldPosition =
new PositionInfo(
windowUid,
/* windowIndex= */ 0,
/* mediaItemIndex= */ 0,
mediaItem,
periodUid,
/* periodIndex= */ 0,
......@@ -146,7 +146,7 @@ import com.google.android.exoplayer2.util.Util;
PositionInfo newPosition =
new PositionInfo(
windowUid,
/* windowIndex= */ 0,
/* mediaItemIndex= */ 0,
mediaItem,
periodUid,
/* periodIndex= */ 0,
......@@ -266,8 +266,8 @@ import com.google.android.exoplayer2.util.Util;
}
@Override
public TracksInfo getCurrentTracksInfo() {
return TracksInfo.EMPTY;
public Tracks getCurrentTracks() {
return Tracks.EMPTY;
}
@Override
......
......@@ -296,8 +296,8 @@ public final class ImaAdsLoaderTest {
/* periodIndex= */ 0,
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
/* position= */ 0,
/* contentPosition= */ 0);
/* positionMs= */ 0,
/* contentPositionMs= */ 0);
fakePlayer.setState(Player.STATE_READY, /* playWhenReady= */ true);
adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
adEventListener.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd));
......@@ -962,7 +962,7 @@ public final class ImaAdsLoaderTest {
@Test
public void setsDefaultMimeTypes() throws Exception {
imaAdsLoader.setSupportedContentTypes(C.TYPE_DASH, C.TYPE_OTHER);
imaAdsLoader.setSupportedContentTypes(C.CONTENT_TYPE_DASH, C.CONTENT_TYPE_OTHER);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
......@@ -996,7 +996,7 @@ public final class ImaAdsLoaderTest {
adViewProvider);
when(mockAdsManager.getAdCuePoints()).thenReturn(PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.setSupportedContentTypes(C.TYPE_OTHER);
imaAdsLoader.setSupportedContentTypes(C.CONTENT_TYPE_OTHER);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
......@@ -1096,8 +1096,8 @@ public final class ImaAdsLoaderTest {
/* periodIndex= */ 0,
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
/* position= */ 0,
/* contentPosition= */ 0);
/* positionMs= */ 0,
/* contentPositionMs= */ 0);
fakePlayer.setState(Player.STATE_READY, /* playWhenReady= */ true);
adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
adEventListener.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd));
......@@ -1155,8 +1155,8 @@ public final class ImaAdsLoaderTest {
/* periodIndex= */ 0,
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
/* position= */ 0,
/* contentPosition= */ 0);
/* positionMs= */ 0,
/* contentPositionMs= */ 0);
fakePlayer.setState(Player.STATE_READY, /* playWhenReady= */ true);
adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
adEventListener.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd));
......@@ -1257,7 +1257,7 @@ public final class ImaAdsLoaderTest {
adViewProvider);
when(mockAdsManager.getAdCuePoints()).thenReturn(PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.setSupportedContentTypes(C.TYPE_OTHER);
imaAdsLoader.setSupportedContentTypes(C.CONTENT_TYPE_OTHER);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
......@@ -1283,7 +1283,7 @@ public final class ImaAdsLoaderTest {
adViewProvider);
when(mockAdsManager.getAdCuePoints()).thenReturn(PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.setSupportedContentTypes(C.TYPE_OTHER);
imaAdsLoader.setSupportedContentTypes(C.CONTENT_TYPE_OTHER);
imaAdsLoader.start(
adsMediaSource, TEST_DATA_SPEC, TEST_ADS_ID, adViewProvider, adsLoaderListener);
......
......@@ -60,7 +60,7 @@ public final class ImaServerSideAdInsertionUriBuilderTest {
builder.setContentUrl(CONTENT_URL);
builder.setAuthToken(AUTH_TOKEN);
builder.setStreamActivityMonitorId(STREAM_ACTIVITY_MONITOR_ID);
builder.setFormat(C.TYPE_HLS);
builder.setFormat(C.CONTENT_TYPE_HLS);
builder.setAdTagParameters(adTagParameters);
builder.setLoadVideoTimeoutMs(ADS_LOADER_TIMEOUT_MS);
Uri uri = builder.build();
......@@ -96,7 +96,7 @@ public final class ImaServerSideAdInsertionUriBuilderTest {
builder.setContentUrl(CONTENT_URL);
builder.setAuthToken(AUTH_TOKEN);
builder.setStreamActivityMonitorId(STREAM_ACTIVITY_MONITOR_ID);
builder.setFormat(C.TYPE_DASH);
builder.setFormat(C.CONTENT_TYPE_DASH);
builder.setAdTagParameters(adTagParameters);
builder.setLoadVideoTimeoutMs(ADS_LOADER_TIMEOUT_MS);
Uri uri = builder.build();
......@@ -127,7 +127,7 @@ public final class ImaServerSideAdInsertionUriBuilderTest {
ImaServerSideAdInsertionUriBuilder builder = new ImaServerSideAdInsertionUriBuilder();
builder.setContentSourceId(CONTENT_SOURCE_ID);
builder.setVideoId(VIDEO_ID);
builder.setFormat(C.TYPE_DASH);
builder.setFormat(C.CONTENT_TYPE_DASH);
Uri streamRequest = builder.build();
......@@ -139,7 +139,7 @@ public final class ImaServerSideAdInsertionUriBuilderTest {
public void build_liveWithNoAdsId_usesAssetKeyAsDefault() {
ImaServerSideAdInsertionUriBuilder builder = new ImaServerSideAdInsertionUriBuilder();
builder.setAssetKey(ASSET_KEY);
builder.setFormat(C.TYPE_DASH);
builder.setFormat(C.CONTENT_TYPE_DASH);
Uri streamRequest = builder.build();
......@@ -177,7 +177,7 @@ public final class ImaServerSideAdInsertionUriBuilderTest {
Uri uri =
new ImaServerSideAdInsertionUriBuilder()
.setAssetKey(ASSET_KEY)
.setFormat(C.TYPE_DASH)
.setFormat(C.CONTENT_TYPE_DASH)
.build();
int loadVideoTimeoutMs = ImaServerSideAdInsertionUriBuilder.getLoadVideoTimeoutMs(uri);
......
......@@ -29,12 +29,16 @@ import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidContentTypeException;
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer2.upstream.HttpUtil;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Predicate;
import com.google.common.net.HttpHeaders;
import com.google.common.util.concurrent.SettableFuture;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
......@@ -42,8 +46,10 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
......@@ -179,21 +185,27 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private long bytesToRead;
private long bytesRead;
/** @deprecated Use {@link OkHttpDataSource.Factory} instead. */
/**
* @deprecated Use {@link OkHttpDataSource.Factory} instead.
*/
@SuppressWarnings("deprecation")
@Deprecated
public OkHttpDataSource(Call.Factory callFactory) {
this(callFactory, /* userAgent= */ null);
}
/** @deprecated Use {@link OkHttpDataSource.Factory} instead. */
/**
* @deprecated Use {@link OkHttpDataSource.Factory} instead.
*/
@SuppressWarnings("deprecation")
@Deprecated
public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) {
this(callFactory, userAgent, /* cacheControl= */ null, /* defaultRequestProperties= */ null);
}
/** @deprecated Use {@link OkHttpDataSource.Factory} instead. */
/**
* @deprecated Use {@link OkHttpDataSource.Factory} instead.
*/
@Deprecated
public OkHttpDataSource(
Call.Factory callFactory,
......@@ -275,8 +287,9 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
Request request = makeRequest(dataSpec);
Response response;
ResponseBody responseBody;
Call call = callFactory.newCall(request);
try {
this.response = callFactory.newCall(request).execute();
this.response = executeCall(call);
response = this.response;
responseBody = Assertions.checkNotNull(response.body());
responseByteStream = responseBody.byteStream();
......@@ -423,6 +436,35 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
}
/**
* This method is an interrupt safe replacement of OkHttp Call.execute() which can get in bad
* states if interrupted while writing to the shared connection socket.
*/
private Response executeCall(Call call) throws IOException {
SettableFuture<Response> future = SettableFuture.create();
call.enqueue(
new Callback() {
@Override
public void onFailure(Call call, IOException e) {
future.setException(e);
}
@Override
public void onResponse(Call call, Response response) {
future.set(response);
}
});
try {
return future.get();
} catch (InterruptedException e) {
call.cancel();
throw new InterruptedIOException();
} catch (ExecutionException ee) {
throw new IOException(ee);
}
}
/**
* Attempts to skip the specified number of bytes in full.
*
* @param bytesToSkip The number of bytes to skip.
......
......@@ -22,7 +22,9 @@ import com.google.android.exoplayer2.upstream.TransferListener;
import okhttp3.CacheControl;
import okhttp3.Call;
/** @deprecated Use {@link OkHttpDataSource.Factory} instead. */
/**
* @deprecated Use {@link OkHttpDataSource.Factory} instead.
*/
@Deprecated
public final class OkHttpDataSourceFactory extends BaseFactory {
......
......@@ -113,6 +113,7 @@ public class LibopusAudioRenderer extends DecoderAudioRenderer<OpusDecoder> {
format.initializationData,
cryptoConfig,
outputFloat);
decoder.experimentalSetDiscardPaddingEnabled(experimentalGetDiscardPaddingEnabled());
TraceUtil.endSection();
return decoder;
......@@ -124,4 +125,14 @@ public class LibopusAudioRenderer extends DecoderAudioRenderer<OpusDecoder> {
int pcmEncoding = decoder.outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT;
return Util.getPcmFormat(pcmEncoding, decoder.channelCount, OpusDecoder.SAMPLE_RATE);
}
/**
* Returns true if support for padding removal from the end of decoder output buffer should be
* enabled.
*
* <p>This method is experimental, and will be renamed or removed in a future release.
*/
protected boolean experimentalGetDiscardPaddingEnabled() {
return false;
}
}
......@@ -54,6 +54,7 @@ public final class OpusDecoder
private final int preSkipSamples;
private final int seekPreRollSamples;
private final long nativeDecoderContext;
private boolean experimentalDiscardPaddingEnabled;
private int skipSamples;
......@@ -97,6 +98,7 @@ public final class OpusDecoder
}
preSkipSamples = getPreSkipSamples(initializationData);
seekPreRollSamples = getSeekPreRollSamples(initializationData);
skipSamples = preSkipSamples;
byte[] headerBytes = initializationData.get(0);
if (headerBytes.length < 19) {
......@@ -142,6 +144,16 @@ public final class OpusDecoder
}
}
/**
* Sets whether discard padding is enabled. When enabled, discard padding samples (provided as
* supplemental data on the input buffer) will be removed from the end of the decoder output.
*
* <p>This method is experimental, and will be renamed or removed in a future release.
*/
public void experimentalSetDiscardPaddingEnabled(boolean enabled) {
this.experimentalDiscardPaddingEnabled = enabled;
}
@Override
public String getName() {
return "libopus" + OpusLibrary.getVersion();
......@@ -221,6 +233,14 @@ public final class OpusDecoder
skipSamples = 0;
outputData.position(skipBytes);
}
} else if (experimentalDiscardPaddingEnabled && inputBuffer.hasSupplementalData()) {
int discardPaddingSamples = getDiscardPaddingSamples(inputBuffer.supplementalData);
if (discardPaddingSamples > 0) {
int discardBytes = samplesToBytes(discardPaddingSamples, channelCount, outputFloat);
if (result >= discardBytes) {
outputData.limit(result - discardBytes);
}
}
}
return null;
}
......@@ -278,6 +298,25 @@ public final class OpusDecoder
return DEFAULT_SEEK_PRE_ROLL_SAMPLES;
}
/**
* Returns the number of discard padding samples specified by the supplemental data attached to an
* input buffer.
*
* @param supplementalData Supplemental data related to the an input buffer.
* @return The number of discard padding samples to remove from the decoder output.
*/
@VisibleForTesting
/* package */ static int getDiscardPaddingSamples(@Nullable ByteBuffer supplementalData) {
if (supplementalData == null || supplementalData.remaining() != 8) {
return 0;
}
long discardPaddingNs = supplementalData.order(ByteOrder.LITTLE_ENDIAN).getLong();
if (discardPaddingNs < 0) {
return 0;
}
return (int) ((discardPaddingNs * SAMPLE_RATE) / C.NANOS_PER_SECOND);
}
/** Returns number of bytes to represent {@code samples}. */
private static int samplesToBytes(int samples, int channelCount, boolean outputFloat) {
int bytesPerChannel = outputFloat ? 4 : 2;
......
......@@ -14,17 +14,26 @@
* limitations under the License.
*/
#ifdef __ANDROID__
#include <android/log.h>
#endif
#include <jni.h>
#include <cstdint>
#include <cstdlib>
#include "opus.h" // NOLINT
#include "opus_multistream.h" // NOLINT
#ifdef __ANDROID__
#define LOG_TAG "opus_jni"
#define LOGE(...) \
((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
#else // __ANDROID__
#define LOGE(...) \
do { \
} while (0)
#endif // __ANDROID__
#define DECODER_FUNC(RETURN_TYPE, NAME, ...) \
extern "C" { \
......
......@@ -16,9 +16,14 @@
package com.google.android.exoplayer2.ext.opus;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeTrue;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoderOutputBuffer;
import com.google.android.exoplayer2.util.LibraryLoader;
import com.google.common.collect.ImmutableList;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
......@@ -29,16 +34,30 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public final class OpusDecoderTest {
private static final LibraryLoader LOADER =
new LibraryLoader("opusV2JNI") {
@Override
protected void loadLibrary(String name) {
System.loadLibrary(name);
}
};
private static final byte[] HEADER =
new byte[] {79, 112, 117, 115, 72, 101, 97, 100, 0, 2, 1, 56, 0, 0, -69, -128, 0, 0, 0};
private static final byte[] ENCODED_DATA = new byte[] {-4};
private static final int DECODED_DATA_SIZE = 3840;
private static final int HEADER_PRE_SKIP_SAMPLES = 14337;
private static final int DEFAULT_SEEK_PRE_ROLL_SAMPLES = 3840;
private static final int DISCARD_PADDING_NANOS = 166667;
private static final ImmutableList<byte[]> HEADER_ONLY_INITIALIZATION_DATA =
ImmutableList.of(HEADER);
private static final long PRE_SKIP_NANOS = 6_500_000;
private static final long CUSTOM_PRE_SKIP_SAMPLES = 28674;
private static final byte[] CUSTOM_PRE_SKIP_BYTES =
buildNativeOrderByteArray(sampleCountToNanoseconds(CUSTOM_PRE_SKIP_SAMPLES));
......@@ -80,11 +99,124 @@ public final class OpusDecoderTest {
assertThat(seekPreRollSamples).isEqualTo(DEFAULT_SEEK_PRE_ROLL_SAMPLES);
}
@Test
public void getDiscardPaddingSamples_positiveSampleLength_returnSampleLength() {
int discardPaddingSamples =
OpusDecoder.getDiscardPaddingSamples(createSupplementalData(DISCARD_PADDING_NANOS));
assertThat(discardPaddingSamples).isEqualTo(nanosecondsToSampleCount(DISCARD_PADDING_NANOS));
}
@Test
public void getDiscardPaddingSamples_negativeSampleLength_returnZero() {
int discardPaddingSamples =
OpusDecoder.getDiscardPaddingSamples(createSupplementalData(-DISCARD_PADDING_NANOS));
assertThat(discardPaddingSamples).isEqualTo(0);
}
@Test
public void decode_removesPreSkipFromOutput() throws OpusDecoderException {
assumeTrue(LOADER.isAvailable());
OpusDecoder decoder =
new OpusDecoder(
/* numInputBuffers= */ 0,
/* numOutputBuffers= */ 0,
/* initialInputBufferSize= */ 0,
createInitializationData(/* preSkipNanos= */ PRE_SKIP_NANOS),
/* cryptoConfig= */ null,
/* outputFloat= */ false);
DecoderInputBuffer input =
createInputBuffer(decoder, ENCODED_DATA, /* supplementalData= */ null);
SimpleDecoderOutputBuffer output = decoder.createOutputBuffer();
assertThat(decoder.decode(input, output, false)).isNull();
assertThat(output.data.remaining())
.isEqualTo(DECODED_DATA_SIZE - nanosecondsToBytes(PRE_SKIP_NANOS));
}
@Test
public void decode_whenDiscardPaddingDisabled_returnsDiscardPadding()
throws OpusDecoderException {
assumeTrue(LOADER.isAvailable());
OpusDecoder decoder =
new OpusDecoder(
/* numInputBuffers= */ 0,
/* numOutputBuffers= */ 0,
/* initialInputBufferSize= */ 0,
createInitializationData(/* preSkipNanos= */ 0),
/* cryptoConfig= */ null,
/* outputFloat= */ false);
DecoderInputBuffer input =
createInputBuffer(
decoder,
ENCODED_DATA,
/* supplementalData= */ buildNativeOrderByteArray(DISCARD_PADDING_NANOS));
SimpleDecoderOutputBuffer output = decoder.createOutputBuffer();
assertThat(decoder.decode(input, output, false)).isNull();
assertThat(output.data.remaining()).isEqualTo(DECODED_DATA_SIZE);
}
@Test
public void decode_whenDiscardPaddingEnabled_removesDiscardPadding() throws OpusDecoderException {
assumeTrue(LOADER.isAvailable());
OpusDecoder decoder =
new OpusDecoder(
/* numInputBuffers= */ 0,
/* numOutputBuffers= */ 0,
/* initialInputBufferSize= */ 0,
createInitializationData(/* preSkipNanos= */ 0),
/* cryptoConfig= */ null,
/* outputFloat= */ false);
decoder.experimentalSetDiscardPaddingEnabled(true);
DecoderInputBuffer input =
createInputBuffer(
decoder,
ENCODED_DATA,
/* supplementalData= */ buildNativeOrderByteArray(DISCARD_PADDING_NANOS));
SimpleDecoderOutputBuffer output = decoder.createOutputBuffer();
assertThat(decoder.decode(input, output, false)).isNull();
assertThat(output.data.limit())
.isEqualTo(DECODED_DATA_SIZE - nanosecondsToBytes(DISCARD_PADDING_NANOS));
}
private static long sampleCountToNanoseconds(long sampleCount) {
return (sampleCount * C.NANOS_PER_SECOND) / OpusDecoder.SAMPLE_RATE;
}
private static long nanosecondsToSampleCount(long nanoseconds) {
return (nanoseconds * OpusDecoder.SAMPLE_RATE) / C.NANOS_PER_SECOND;
}
private static long nanosecondsToBytes(long nanoseconds) {
return nanosecondsToSampleCount(nanoseconds) * 4;
}
private static byte[] buildNativeOrderByteArray(long value) {
return ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(value).array();
}
private static ImmutableList<byte[]> createInitializationData(long preSkipNanos) {
byte[] preSkip = buildNativeOrderByteArray(preSkipNanos);
return ImmutableList.of(HEADER, preSkip, CUSTOM_SEEK_PRE_ROLL_BYTES);
}
// The cast to ByteBuffer is required for Java 8 compatibility. See
// https://issues.apache.org/jira/browse/MRESOLVER-85
@SuppressWarnings("UnnecessaryCast")
private static ByteBuffer createSupplementalData(long value) {
return (ByteBuffer)
ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).rewind();
}
private static DecoderInputBuffer createInputBuffer(
OpusDecoder decoder, byte[] data, @Nullable byte[] supplementalData) {
DecoderInputBuffer input = decoder.createInputBuffer();
input.ensureSpaceForWrite(data.length);
input.data.put(data);
input.data.position(0).limit(data.length);
if (supplementalData != null) {
input.resetSupplementalData(supplementalData.length);
input.supplementalData.put(supplementalData).rewind();
input.addFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA);
}
return input;
}
}
......@@ -39,7 +39,7 @@ injected from application code.
`DefaultDataSource` will automatically use the RTMP extension whenever it's
available. Hence if your application is using `DefaultDataSource` or
`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as
`DefaultDataSource.Factory`, adding support for RTMP streams is as simple as
adding a dependency to the RTMP extension as described above. No changes to your
application code are required. Alternatively, if you know that your application
doesn't need to handle any other protocols, you can update any
......
......@@ -19,7 +19,9 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
/** @deprecated Use {@link RtmpDataSource.Factory} instead. */
/**
* @deprecated Use {@link RtmpDataSource.Factory} instead.
*/
@Deprecated
public final class RtmpDataSourceFactory implements DataSource.Factory {
......@@ -29,7 +31,9 @@ public final class RtmpDataSourceFactory implements DataSource.Factory {
this(null);
}
/** @param listener An optional listener. */
/**
* @param listener An optional listener.
*/
public RtmpDataSourceFactory(@Nullable TransferListener listener) {
this.listener = listener;
}
......
......@@ -54,7 +54,9 @@ public final class WorkManagerScheduler implements Scheduler {
private final WorkManager workManager;
private final String workName;
/** @deprecated Call {@link #WorkManagerScheduler(Context, String)} instead. */
/**
* @deprecated Call {@link #WorkManagerScheduler(Context, String)} instead.
*/
@Deprecated
@SuppressWarnings("deprecation")
public WorkManagerScheduler(String workName) {
......
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
......@@ -48,9 +48,21 @@ class CombinedJavadocPlugin implements Plugin<Project> {
libraryModule.android.libraryVariants.all { variant ->
def name = variant.buildType.name
if (name == "release") {
// Works around b/234569640 that causes different versions of the androidx.media
// jar to be on the classpath.
def allJarFiles = []
allJarFiles.addAll(variant.javaCompileProvider.get().classpath.files)
def filteredJarFiles = allJarFiles.findAll { file ->
if (file ==~ /.*media-.\..\..-api.jar$/
&& !file.path.endsWith(
"media-" + project.ext.androidxMediaVersion + "-api.jar")) {
return false;
}
return true;
}
classpath +=
libraryModule.project.files(
variant.javaCompileProvider.get().classpath.files,
filteredJarFiles,
libraryModule.project.android.getBootClasspath())
}
}
......
......@@ -92,7 +92,7 @@ public abstract class BasePlayer implements Player {
/**
* {@inheritDoc}
*
* <p>BasePlayer and its descendents will return {@code true}.
* <p>BasePlayer and its descendants will return {@code true}.
*/
@Override
public final boolean canAdvertiseSession() {
......@@ -141,12 +141,18 @@ public abstract class BasePlayer implements Player {
seekToOffset(getSeekForwardIncrement());
}
/**
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@Deprecated
@Override
public final boolean hasPrevious() {
return hasPreviousMediaItem();
}
/**
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@Deprecated
@Override
public final boolean hasPreviousWindow() {
......@@ -158,12 +164,18 @@ public abstract class BasePlayer implements Player {
return getPreviousMediaItemIndex() != C.INDEX_UNSET;
}
/**
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@Deprecated
@Override
public final void previous() {
seekToPreviousMediaItem();
}
/**
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@Deprecated
@Override
public final void seekToPreviousWindow() {
......@@ -196,12 +208,18 @@ public abstract class BasePlayer implements Player {
}
}
/**
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@Deprecated
@Override
public final boolean hasNext() {
return hasNextMediaItem();
}
/**
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@Deprecated
@Override
public final boolean hasNextWindow() {
......@@ -213,12 +231,18 @@ public abstract class BasePlayer implements Player {
return getNextMediaItemIndex() != C.INDEX_UNSET;
}
/**
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@Deprecated
@Override
public final void next() {
seekToNextMediaItem();
}
/**
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@Deprecated
@Override
public final void seekToNextWindow() {
......@@ -251,12 +275,18 @@ public abstract class BasePlayer implements Player {
setPlaybackParameters(getPlaybackParameters().withSpeed(speed));
}
/**
* @deprecated Use {@link #getCurrentMediaItemIndex()} instead.
*/
@Deprecated
@Override
public final int getCurrentWindowIndex() {
return getCurrentMediaItemIndex();
}
/**
* @deprecated Use {@link #getNextMediaItemIndex()} instead.
*/
@Deprecated
@Override
public final int getNextWindowIndex() {
......@@ -272,6 +302,9 @@ public abstract class BasePlayer implements Player {
getCurrentMediaItemIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled());
}
/**
* @deprecated Use {@link #getPreviousMediaItemIndex()} instead.
*/
@Deprecated
@Override
public final int getPreviousWindowIndex() {
......@@ -324,6 +357,9 @@ public abstract class BasePlayer implements Player {
: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
}
/**
* @deprecated Use {@link #isCurrentMediaItemDynamic()} instead.
*/
@Deprecated
@Override
public final boolean isCurrentWindowDynamic() {
......@@ -336,6 +372,9 @@ public abstract class BasePlayer implements Player {
return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isDynamic;
}
/**
* @deprecated Use {@link #isCurrentMediaItemLive()} instead.
*/
@Deprecated
@Override
public final boolean isCurrentWindowLive() {
......@@ -362,6 +401,9 @@ public abstract class BasePlayer implements Player {
return window.getCurrentUnixTimeMs() - window.windowStartTimeMs - getContentPosition();
}
/**
* @deprecated Use {@link #isCurrentMediaItemSeekable()} instead.
*/
@Deprecated
@Override
public final boolean isCurrentWindowSeekable() {
......
......@@ -771,7 +771,9 @@ public final class Format implements Bundleable {
// Video.
/** @deprecated Use {@link Format.Builder}. */
/**
* @deprecated Use {@link Format.Builder}.
*/
@Deprecated
public static Format createVideoSampleFormat(
@Nullable String id,
......@@ -799,7 +801,9 @@ public final class Format implements Bundleable {
.build();
}
/** @deprecated Use {@link Format.Builder}. */
/**
* @deprecated Use {@link Format.Builder}.
*/
@Deprecated
public static Format createVideoSampleFormat(
@Nullable String id,
......@@ -833,7 +837,9 @@ public final class Format implements Bundleable {
// Audio.
/** @deprecated Use {@link Format.Builder}. */
/**
* @deprecated Use {@link Format.Builder}.
*/
@Deprecated
public static Format createAudioSampleFormat(
@Nullable String id,
......@@ -863,7 +869,9 @@ public final class Format implements Bundleable {
.build();
}
/** @deprecated Use {@link Format.Builder}. */
/**
* @deprecated Use {@link Format.Builder}.
*/
@Deprecated
public static Format createAudioSampleFormat(
@Nullable String id,
......@@ -897,7 +905,9 @@ public final class Format implements Bundleable {
// Generic.
/** @deprecated Use {@link Format.Builder}. */
/**
* @deprecated Use {@link Format.Builder}.
*/
@Deprecated
public static Format createContainerFormat(
@Nullable String id,
......@@ -923,7 +933,9 @@ public final class Format implements Bundleable {
.build();
}
/** @deprecated Use {@link Format.Builder}. */
/**
* @deprecated Use {@link Format.Builder}.
*/
@Deprecated
public static Format createSampleFormat(@Nullable String id, @Nullable String sampleMimeType) {
return new Builder().setId(id).setSampleMimeType(sampleMimeType).build();
......@@ -981,25 +993,33 @@ public final class Format implements Bundleable {
return new Builder(this);
}
/** @deprecated Use {@link #buildUpon()} and {@link Builder#setMaxInputSize(int)}. */
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setMaxInputSize(int)}.
*/
@Deprecated
public Format copyWithMaxInputSize(int maxInputSize) {
return buildUpon().setMaxInputSize(maxInputSize).build();
}
/** @deprecated Use {@link #buildUpon()} and {@link Builder#setSubsampleOffsetUs(long)}. */
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setSubsampleOffsetUs(long)}.
*/
@Deprecated
public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
return buildUpon().setSubsampleOffsetUs(subsampleOffsetUs).build();
}
/** @deprecated Use {@link #buildUpon()} and {@link Builder#setLabel(String)} . */
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setLabel(String)} .
*/
@Deprecated
public Format copyWithLabel(@Nullable String label) {
return buildUpon().setLabel(label).build();
}
/** @deprecated Use {@link #withManifestFormatInfo(Format)}. */
/**
* @deprecated Use {@link #withManifestFormatInfo(Format)}.
*/
@Deprecated
public Format copyWithManifestFormatInfo(Format manifestFormat) {
return withManifestFormatInfo(manifestFormat);
......@@ -1081,19 +1101,25 @@ public final class Format implements Bundleable {
return buildUpon().setEncoderDelay(encoderDelay).setEncoderPadding(encoderPadding).build();
}
/** @deprecated Use {@link #buildUpon()} and {@link Builder#setFrameRate(float)}. */
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setFrameRate(float)}.
*/
@Deprecated
public Format copyWithFrameRate(float frameRate) {
return buildUpon().setFrameRate(frameRate).build();
}
/** @deprecated Use {@link #buildUpon()} and {@link Builder#setDrmInitData(DrmInitData)}. */
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setDrmInitData(DrmInitData)}.
*/
@Deprecated
public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {
return buildUpon().setDrmInitData(drmInitData).build();
}
/** @deprecated Use {@link #buildUpon()} and {@link Builder#setMetadata(Metadata)}. */
/**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setMetadata(Metadata)}.
*/
@Deprecated
public Format copyWithMetadata(@Nullable Metadata metadata) {
return buildUpon().setMetadata(metadata).build();
......@@ -1501,7 +1527,9 @@ public final class Format implements Bundleable {
bundle.putFloat(keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), pixelWidthHeightRatio);
bundle.putByteArray(keyForField(FIELD_PROJECTION_DATA), projectionData);
bundle.putInt(keyForField(FIELD_STEREO_MODE), stereoMode);
bundle.putBundle(keyForField(FIELD_COLOR_INFO), BundleableUtil.toNullableBundle(colorInfo));
if (colorInfo != null) {
bundle.putBundle(keyForField(FIELD_COLOR_INFO), colorInfo.toBundle());
}
// Audio specific.
bundle.putInt(keyForField(FIELD_CHANNEL_COUNT), channelCount);
bundle.putInt(keyForField(FIELD_SAMPLE_RATE), sampleRate);
......@@ -1568,11 +1596,13 @@ public final class Format implements Bundleable {
bundle.getFloat(
keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), DEFAULT.pixelWidthHeightRatio))
.setProjectionData(bundle.getByteArray(keyForField(FIELD_PROJECTION_DATA)))
.setStereoMode(bundle.getInt(keyForField(FIELD_STEREO_MODE), DEFAULT.stereoMode))
.setColorInfo(
BundleableUtil.fromNullableBundle(
ColorInfo.CREATOR, bundle.getBundle(keyForField(FIELD_COLOR_INFO))))
// Audio specific.
.setStereoMode(bundle.getInt(keyForField(FIELD_STEREO_MODE), DEFAULT.stereoMode));
Bundle colorInfoBundle = bundle.getBundle(keyForField(FIELD_COLOR_INFO));
if (colorInfoBundle != null) {
builder.setColorInfo(ColorInfo.CREATOR.fromBundle(colorInfoBundle));
}
// Audio specific.
builder
.setChannelCount(bundle.getInt(keyForField(FIELD_CHANNEL_COUNT), DEFAULT.channelCount))
.setSampleRate(bundle.getInt(keyForField(FIELD_SAMPLE_RATE), DEFAULT.sampleRate))
.setPcmEncoding(bundle.getInt(keyForField(FIELD_PCM_ENCODING), DEFAULT.pcmEncoding))
......
......@@ -23,9 +23,8 @@ import android.view.TextureView;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.text.CueGroup;
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
import com.google.android.exoplayer2.video.VideoSize;
import java.util.List;
......@@ -302,7 +301,11 @@ public class ForwardingPlayer implements Player {
player.seekForward();
}
/** Calls {@link Player#hasPrevious()} on the delegate and returns the result. */
/**
* Calls {@link Player#hasPrevious()} on the delegate and returns the result.
*
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -310,7 +313,11 @@ public class ForwardingPlayer implements Player {
return player.hasPrevious();
}
/** Calls {@link Player#hasPreviousWindow()} on the delegate and returns the result. */
/**
* Calls {@link Player#hasPreviousWindow()} on the delegate and returns the result.
*
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -324,7 +331,11 @@ public class ForwardingPlayer implements Player {
return player.hasPreviousMediaItem();
}
/** Calls {@link Player#previous()} on the delegate. */
/**
* Calls {@link Player#previous()} on the delegate.
*
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -332,7 +343,11 @@ public class ForwardingPlayer implements Player {
player.previous();
}
/** Calls {@link Player#seekToPreviousWindow()} on the delegate. */
/**
* Calls {@link Player#seekToPreviousWindow()} on the delegate.
*
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -358,7 +373,11 @@ public class ForwardingPlayer implements Player {
return player.getMaxSeekToPreviousPosition();
}
/** Calls {@link Player#hasNext()} on the delegate and returns the result. */
/**
* Calls {@link Player#hasNext()} on the delegate and returns the result.
*
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -366,7 +385,11 @@ public class ForwardingPlayer implements Player {
return player.hasNext();
}
/** Calls {@link Player#hasNextWindow()} on the delegate and returns the result. */
/**
* Calls {@link Player#hasNextWindow()} on the delegate and returns the result.
*
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -380,7 +403,11 @@ public class ForwardingPlayer implements Player {
return player.hasNextMediaItem();
}
/** Calls {@link Player#next()} on the delegate. */
/**
* Calls {@link Player#next()} on the delegate.
*
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -388,7 +415,11 @@ public class ForwardingPlayer implements Player {
player.next();
}
/** Calls {@link Player#seekToNextWindow()} on the delegate. */
/**
* Calls {@link Player#seekToNextWindow()} on the delegate.
*
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -432,7 +463,13 @@ public class ForwardingPlayer implements Player {
player.stop();
}
/** Calls {@link Player#stop(boolean)} on the delegate. */
/**
* Calls {@link Player#stop(boolean)} on the delegate.
*
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
* {@link #prepare() re-preparing} the player.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -446,26 +483,10 @@ public class ForwardingPlayer implements Player {
player.release();
}
/** Calls {@link Player#getCurrentTrackGroups()} on the delegate and returns the result. */
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
/** Calls {@link Player#getCurrentTracks()} on the delegate and returns the result. */
@Override
public TrackGroupArray getCurrentTrackGroups() {
return player.getCurrentTrackGroups();
}
/** Calls {@link Player#getCurrentTrackSelections()} on the delegate and returns the result. */
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return player.getCurrentTrackSelections();
}
/** Calls {@link Player#getCurrentTracksInfo()} 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. */
......@@ -517,7 +538,11 @@ public class ForwardingPlayer implements Player {
return player.getCurrentPeriodIndex();
}
/** Calls {@link Player#getCurrentWindowIndex()} on the delegate and returns the result. */
/**
* Calls {@link Player#getCurrentWindowIndex()} on the delegate and returns the result.
*
* @deprecated Use {@link #getCurrentMediaItemIndex()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -531,7 +556,11 @@ public class ForwardingPlayer implements Player {
return player.getCurrentMediaItemIndex();
}
/** Calls {@link Player#getNextWindowIndex()} on the delegate and returns the result. */
/**
* Calls {@link Player#getNextWindowIndex()} on the delegate and returns the result.
*
* @deprecated Use {@link #getNextMediaItemIndex()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -545,7 +574,11 @@ public class ForwardingPlayer implements Player {
return player.getNextMediaItemIndex();
}
/** Calls {@link Player#getPreviousWindowIndex()} on the delegate and returns the result. */
/**
* Calls {@link Player#getPreviousWindowIndex()} on the delegate and returns the result.
*
* @deprecated Use {@link #getPreviousMediaItemIndex()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -608,7 +641,11 @@ public class ForwardingPlayer implements Player {
return player.getTotalBufferedDuration();
}
/** Calls {@link Player#isCurrentWindowDynamic()} on the delegate and returns the result. */
/**
* Calls {@link Player#isCurrentWindowDynamic()} on the delegate and returns the result.
*
* @deprecated Use {@link #isCurrentMediaItemDynamic()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -622,7 +659,11 @@ public class ForwardingPlayer implements Player {
return player.isCurrentMediaItemDynamic();
}
/** Calls {@link Player#isCurrentWindowLive()} on the delegate and returns the result. */
/**
* Calls {@link Player#isCurrentWindowLive()} on the delegate and returns the result.
*
* @deprecated Use {@link #isCurrentMediaItemLive()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -642,7 +683,11 @@ public class ForwardingPlayer implements Player {
return player.getCurrentLiveOffset();
}
/** Calls {@link Player#isCurrentWindowSeekable()} on the delegate and returns the result. */
/**
* Calls {@link Player#isCurrentWindowSeekable()} on the delegate and returns the result.
*
* @deprecated Use {@link #isCurrentMediaItemSeekable()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
......@@ -772,7 +817,7 @@ public class ForwardingPlayer implements Player {
/** Calls {@link Player#getCurrentCues()} on the delegate and returns the result. */
@Override
public List<Cue> getCurrentCues() {
public CueGroup getCurrentCues() {
return player.getCurrentCues();
}
......@@ -851,14 +896,8 @@ public class ForwardingPlayer implements Player {
}
@Override
@SuppressWarnings("deprecation")
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
listener.onTracksChanged(trackGroups, trackSelections);
}
@Override
public void onTracksInfoChanged(TracksInfo tracksInfo) {
listener.onTracksInfoChanged(tracksInfo);
public void onTracksChanged(Tracks tracks) {
listener.onTracksChanged(tracks);
}
@Override
......@@ -1019,6 +1058,11 @@ public class ForwardingPlayer implements Player {
}
@Override
public void onCues(CueGroup cueGroup) {
listener.onCues(cueGroup);
}
@Override
public void onMetadata(Metadata metadata) {
listener.onMetadata(metadata);
}
......
......@@ -52,7 +52,6 @@ public final class MediaMetadata implements Bundleable {
@Nullable private CharSequence displayTitle;
@Nullable private CharSequence subtitle;
@Nullable private CharSequence description;
@Nullable private Uri mediaUri;
@Nullable private Rating userRating;
@Nullable private Rating overallRating;
@Nullable private byte[] artworkData;
......@@ -88,7 +87,6 @@ public final class MediaMetadata implements Bundleable {
this.displayTitle = mediaMetadata.displayTitle;
this.subtitle = mediaMetadata.subtitle;
this.description = mediaMetadata.description;
this.mediaUri = mediaMetadata.mediaUri;
this.userRating = mediaMetadata.userRating;
this.overallRating = mediaMetadata.overallRating;
this.artworkData = mediaMetadata.artworkData;
......@@ -161,12 +159,6 @@ public final class MediaMetadata implements Bundleable {
return this;
}
/** Sets the media {@link Uri}. */
public Builder setMediaUri(@Nullable Uri mediaUri) {
this.mediaUri = mediaUri;
return this;
}
/** Sets the user {@link Rating}. */
public Builder setUserRating(@Nullable Rating userRating) {
this.userRating = userRating;
......@@ -247,7 +239,9 @@ public final class MediaMetadata implements Bundleable {
return this;
}
/** @deprecated Use {@link #setRecordingYear(Integer)} instead. */
/**
* @deprecated Use {@link #setRecordingYear(Integer)} instead.
*/
@Deprecated
public Builder setYear(@Nullable Integer year) {
return setRecordingYear(year);
......@@ -424,9 +418,6 @@ public final class MediaMetadata implements Bundleable {
if (mediaMetadata.description != null) {
setDescription(mediaMetadata.description);
}
if (mediaMetadata.mediaUri != null) {
setMediaUri(mediaMetadata.mediaUri);
}
if (mediaMetadata.userRating != null) {
setUserRating(mediaMetadata.userRating);
}
......@@ -629,8 +620,6 @@ public final class MediaMetadata implements Bundleable {
@Nullable public final CharSequence subtitle;
/** Optional description. */
@Nullable public final CharSequence description;
/** Optional media {@link Uri}. */
@Nullable public final Uri mediaUri;
/** Optional user {@link Rating}. */
@Nullable public final Rating userRating;
/** Optional overall {@link Rating}. */
......@@ -649,7 +638,9 @@ public final class MediaMetadata implements Bundleable {
@Nullable public final @FolderType Integer folderType;
/** Optional boolean for media playability. */
@Nullable public final Boolean isPlayable;
/** @deprecated Use {@link #recordingYear} instead. */
/**
* @deprecated Use {@link #recordingYear} instead.
*/
@Deprecated @Nullable public final Integer year;
/** Optional year of the recording date. */
@Nullable public final Integer recordingYear;
......@@ -713,7 +704,6 @@ public final class MediaMetadata implements Bundleable {
this.displayTitle = builder.displayTitle;
this.subtitle = builder.subtitle;
this.description = builder.description;
this.mediaUri = builder.mediaUri;
this.userRating = builder.userRating;
this.overallRating = builder.overallRating;
this.artworkData = builder.artworkData;
......@@ -762,7 +752,6 @@ public final class MediaMetadata implements Bundleable {
&& Util.areEqual(displayTitle, that.displayTitle)
&& Util.areEqual(subtitle, that.subtitle)
&& Util.areEqual(description, that.description)
&& Util.areEqual(mediaUri, that.mediaUri)
&& Util.areEqual(userRating, that.userRating)
&& Util.areEqual(overallRating, that.overallRating)
&& Arrays.equals(artworkData, that.artworkData)
......@@ -798,7 +787,6 @@ public final class MediaMetadata implements Bundleable {
displayTitle,
subtitle,
description,
mediaUri,
userRating,
overallRating,
Arrays.hashCode(artworkData),
......@@ -908,7 +896,6 @@ public final class MediaMetadata implements Bundleable {
bundle.putCharSequence(keyForField(FIELD_DISPLAY_TITLE), displayTitle);
bundle.putCharSequence(keyForField(FIELD_SUBTITLE), subtitle);
bundle.putCharSequence(keyForField(FIELD_DESCRIPTION), description);
bundle.putParcelable(keyForField(FIELD_MEDIA_URI), mediaUri);
bundle.putByteArray(keyForField(FIELD_ARTWORK_DATA), artworkData);
bundle.putParcelable(keyForField(FIELD_ARTWORK_URI), artworkUri);
bundle.putCharSequence(keyForField(FIELD_WRITER), writer);
......@@ -982,7 +969,6 @@ public final class MediaMetadata implements Bundleable {
.setDisplayTitle(bundle.getCharSequence(keyForField(FIELD_DISPLAY_TITLE)))
.setSubtitle(bundle.getCharSequence(keyForField(FIELD_SUBTITLE)))
.setDescription(bundle.getCharSequence(keyForField(FIELD_DESCRIPTION)))
.setMediaUri(bundle.getParcelable(keyForField(FIELD_MEDIA_URI)))
.setArtworkData(
bundle.getByteArray(keyForField(FIELD_ARTWORK_DATA)),
bundle.containsKey(keyForField(FIELD_ARTWORK_DATA_TYPE))
......
......@@ -397,27 +397,6 @@ public class PlaybackException extends Exception implements Bundleable {
// Bundleable implementation.
/**
* Identifiers for fields in a {@link Bundle} which represents a playback exception. Subclasses
* may use {@link #FIELD_CUSTOM_ID_BASE} to generate more keys using {@link #keyForField(int)}.
*
* <p>Note: Changes to the Bundleable implementation must be backwards compatible, so as to avoid
* breaking communication across different Bundleable implementation versions.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef(
open = true,
value = {
FIELD_INT_ERROR_CODE,
FIELD_LONG_TIMESTAMP_MS,
FIELD_STRING_MESSAGE,
FIELD_STRING_CAUSE_CLASS_NAME,
FIELD_STRING_CAUSE_MESSAGE,
})
protected @interface FieldNumber {}
private static final int FIELD_INT_ERROR_CODE = 0;
private static final int FIELD_LONG_TIMESTAMP_MS = 1;
private static final int FIELD_STRING_MESSAGE = 2;
......@@ -425,7 +404,7 @@ public class PlaybackException extends Exception implements Bundleable {
private static final int FIELD_STRING_CAUSE_MESSAGE = 4;
/**
* Defines a minimum field id value for subclasses to use when implementing {@link #toBundle()}
* Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()}
* and {@link Bundleable.Creator}.
*
* <p>Subclasses should obtain their {@link Bundle Bundle's} field keys by applying a non-negative
......@@ -452,10 +431,13 @@ public class PlaybackException extends Exception implements Bundleable {
}
/**
* Converts the given {@link FieldNumber} to a string which can be used as a field key when
* implementing {@link #toBundle()} and {@link Bundleable.Creator}.
* Converts the given field number to a string which can be used as a field key when implementing
* {@link #toBundle()} and {@link Bundleable.Creator}.
*
* <p>Subclasses should use {@code field} values greater than or equal to {@link
* #FIELD_CUSTOM_ID_BASE}.
*/
protected static String keyForField(@FieldNumber int field) {
protected static String keyForField(int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
......
......@@ -31,7 +31,7 @@ import java.lang.annotation.Target;
public abstract class Rating implements Bundleable {
/** A float value that denotes the rating is unset. */
public static final float RATING_UNSET = -1.0f;
/* package */ static final float RATING_UNSET = -1.0f;
// Default package-private constructor to prevent extending Rating class outside this package.
/* package */ Rating() {}
......
......@@ -169,7 +169,9 @@ public abstract class Timeline implements Bundleable {
*/
public Object uid;
/** @deprecated Use {@link #mediaItem} instead. */
/**
* @deprecated Use {@link #mediaItem} instead.
*/
@Deprecated @Nullable public Object tag;
/** The {@link MediaItem} associated to the window. Not necessarily unique. */
......@@ -212,7 +214,9 @@ public abstract class Timeline implements Bundleable {
/** Whether this window may change when the timeline is updated. */
public boolean isDynamic;
/** @deprecated Use {@link #isLive()} instead. */
/**
* @deprecated Use {@link #isLive()} instead.
*/
@Deprecated public boolean isLive;
/**
......@@ -1169,14 +1173,18 @@ public abstract class Timeline implements Bundleable {
== C.INDEX_UNSET;
}
/** @deprecated Use {@link #getPeriodPositionUs(Window, Period, int, long)} instead. */
/**
* @deprecated Use {@link #getPeriodPositionUs(Window, Period, int, long)} instead.
*/
@Deprecated
@InlineMe(replacement = "this.getPeriodPositionUs(window, period, windowIndex, windowPositionUs)")
public final Pair<Object, Long> getPeriodPosition(
Window window, Period period, int windowIndex, long windowPositionUs) {
return getPeriodPositionUs(window, period, windowIndex, windowPositionUs);
}
/** @deprecated Use {@link #getPeriodPositionUs(Window, Period, int, long, long)} instead. */
/**
* @deprecated Use {@link #getPeriodPositionUs(Window, Period, int, long, long)} instead.
*/
@Deprecated
@Nullable
@InlineMe(
......
......@@ -29,7 +29,6 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
/**
* Attributes for audio playback, which configure the underlying platform {@link
......@@ -44,10 +43,31 @@ import java.lang.reflect.Method;
*/
public final class AudioAttributes implements Bundleable {
/** A direct wrapper around {@link android.media.AudioAttributes}. */
@RequiresApi(21)
public static final class AudioAttributesV21 {
public final android.media.AudioAttributes audioAttributes;
private AudioAttributesV21(AudioAttributes audioAttributes) {
android.media.AudioAttributes.Builder builder =
new android.media.AudioAttributes.Builder()
.setContentType(audioAttributes.contentType)
.setFlags(audioAttributes.flags)
.setUsage(audioAttributes.usage);
if (Util.SDK_INT >= 29) {
Api29.setAllowedCapturePolicy(builder, audioAttributes.allowedCapturePolicy);
}
if (Util.SDK_INT >= 32) {
Api32.setSpatializationBehavior(builder, audioAttributes.spatializationBehavior);
}
this.audioAttributes = builder.build();
}
}
/**
* The default audio attributes, where the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage
* is {@link C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are
* set.
* The default audio attributes, where the content type is {@link C#AUDIO_CONTENT_TYPE_UNKNOWN},
* usage is {@link C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags
* are set.
*/
public static final AudioAttributes DEFAULT = new Builder().build();
......@@ -63,11 +83,11 @@ public final class AudioAttributes implements Bundleable {
/**
* Creates a new builder for {@link AudioAttributes}.
*
* <p>By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is {@link
* <p>By default the content type is {@link C#AUDIO_CONTENT_TYPE_UNKNOWN}, usage is {@link
* C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are set.
*/
public Builder() {
contentType = C.CONTENT_TYPE_UNKNOWN;
contentType = C.AUDIO_CONTENT_TYPE_UNKNOWN;
flags = 0;
usage = C.USAGE_MEDIA;
allowedCapturePolicy = C.ALLOW_CAPTURE_BY_ALL;
......@@ -98,9 +118,7 @@ public final class AudioAttributes implements Bundleable {
return this;
}
// TODO[b/190759307] Update javadoc to link to AudioAttributes.Builder#setSpatializationBehavior
// once compile SDK target is set to 32.
/** See {@code android.media.AudioAttributes.Builder.setSpatializationBehavior(int)}. */
/** See {@link android.media.AudioAttributes.Builder#setSpatializationBehavior(int)}. */
public Builder setSpatializationBehavior(@C.SpatializationBehavior int spatializationBehavior) {
this.spatializationBehavior = spatializationBehavior;
return this;
......@@ -124,7 +142,7 @@ public final class AudioAttributes implements Bundleable {
/** The {@link C.SpatializationBehavior}. */
public final @C.SpatializationBehavior int spatializationBehavior;
@Nullable private android.media.AudioAttributes audioAttributesV21;
@Nullable private AudioAttributesV21 audioAttributesV21;
private AudioAttributes(
@C.AudioContentType int contentType,
......@@ -140,25 +158,15 @@ public final class AudioAttributes implements Bundleable {
}
/**
* Returns a {@link android.media.AudioAttributes} from this instance.
* Returns a {@link AudioAttributesV21} from this instance.
*
* <p>Field {@link AudioAttributes#allowedCapturePolicy} is ignored for API levels prior to 29.
* <p>Some fields are ignored if the corresponding {@link android.media.AudioAttributes.Builder}
* setter is not available on the current API level.
*/
@RequiresApi(21)
public android.media.AudioAttributes getAudioAttributesV21() {
public AudioAttributesV21 getAudioAttributesV21() {
if (audioAttributesV21 == null) {
android.media.AudioAttributes.Builder builder =
new android.media.AudioAttributes.Builder()
.setContentType(contentType)
.setFlags(flags)
.setUsage(usage);
if (Util.SDK_INT >= 29) {
Api29.setAllowedCapturePolicy(builder, allowedCapturePolicy);
}
if (Util.SDK_INT >= 32) {
Api32.setSpatializationBehavior(builder, spatializationBehavior);
}
audioAttributesV21 = builder.build();
audioAttributesV21 = new AudioAttributesV21(this);
}
return audioAttributesV21;
}
......@@ -250,8 +258,6 @@ public final class AudioAttributes implements Bundleable {
@RequiresApi(29)
private static final class Api29 {
private Api29() {}
@DoNotInline
public static void setAllowedCapturePolicy(
android.media.AudioAttributes.Builder builder,
......@@ -262,20 +268,11 @@ public final class AudioAttributes implements Bundleable {
@RequiresApi(32)
private static final class Api32 {
private Api32() {}
@DoNotInline
public static void setSpatializationBehavior(
android.media.AudioAttributes.Builder builder,
@C.SpatializationBehavior int spatializationBehavior) {
try {
// TODO[b/190759307]: Remove reflection once compile SDK target is set to 32.
Method setSpatializationBehavior =
builder.getClass().getMethod("setSpatializationBehavior", Integer.TYPE);
setSpatializationBehavior.invoke(builder, spatializationBehavior);
} catch (Exception e) {
// Do nothing if reflection fails.
}
builder.setSpatializationBehavior(spatializationBehavior);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment