Commit b1000940 by Oliver Woodman Committed by GitHub

Merge pull request #8541 from google/dev-v2-r2.13.0

r2.13.0
parents 03263db3 77798e4f
Showing with 1733 additions and 576 deletions
...@@ -26,7 +26,6 @@ allprojects { ...@@ -26,7 +26,6 @@ allprojects {
repositories { repositories {
google() google()
jcenter() jcenter()
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
} }
project.ext { project.ext {
exoplayerPublishEnabled = false exoplayerPublishEnabled = false
......
...@@ -13,18 +13,18 @@ ...@@ -13,18 +13,18 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.12.3' releaseVersion = '2.13.0'
releaseVersionCode = 2012003 releaseVersionCode = 2013000
minSdkVersion = 16 minSdkVersion = 16
appTargetSdkVersion = 29 appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest. targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
compileSdkVersion = 29 compileSdkVersion = 30
dexmakerVersion = '2.21.0' dexmakerVersion = '2.21.0'
junitVersion = '4.13-rc-2' junitVersion = '4.13-rc-2'
guavaVersion = '27.1-android' guavaVersion = '27.1-android'
mockitoVersion = '2.28.2' mockitoVersion = '2.28.2'
mockWebServerVersion = '3.12.0' mockWebServerVersion = '3.12.0'
robolectricVersion = '4.4' robolectricVersion = '4.5'
checkerframeworkVersion = '3.3.0' checkerframeworkVersion = '3.3.0'
checkerframeworkCompatVersion = '2.5.0' checkerframeworkCompatVersion = '2.5.0'
jsr305Version = '3.0.2' jsr305Version = '3.0.2'
...@@ -33,14 +33,15 @@ project.ext { ...@@ -33,14 +33,15 @@ project.ext {
androidxAppCompatVersion = '1.1.0' androidxAppCompatVersion = '1.1.0'
androidxCollectionVersion = '1.1.0' androidxCollectionVersion = '1.1.0'
androidxFuturesVersion = '1.1.0' androidxFuturesVersion = '1.1.0'
androidxMediaVersion = '1.0.1' androidxMediaVersion = '1.2.1'
androidxMedia2Version = '1.1.0' androidxMedia2Version = '1.1.0'
androidxMultidexVersion = '2.0.0' androidxMultidexVersion = '2.0.0'
androidxRecyclerViewVersion = '1.1.0' androidxRecyclerViewVersion = '1.1.0'
androidxTestCoreVersion = '1.2.0' androidxTestCoreVersion = '1.3.0'
androidxTestJUnitVersion = '1.1.1' androidxTestJUnitVersion = '1.1.1'
androidxTestRunnerVersion = '1.2.0' androidxTestRunnerVersion = '1.3.0'
androidxTestRulesVersion = '1.2.0' androidxTestRulesVersion = '1.3.0'
androidxTestServicesStorageVersion = '1.3.0'
truthVersion = '1.0' truthVersion = '1.0'
modulePrefix = ':' modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
......
...@@ -28,6 +28,7 @@ include modulePrefix + 'library-dash' ...@@ -28,6 +28,7 @@ include modulePrefix + 'library-dash'
include modulePrefix + 'library-extractor' include modulePrefix + 'library-extractor'
include modulePrefix + 'library-hls' include modulePrefix + 'library-hls'
include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-smoothstreaming'
include modulePrefix + 'library-transformer'
include modulePrefix + 'library-ui' include modulePrefix + 'library-ui'
include modulePrefix + 'robolectricutils' include modulePrefix + 'robolectricutils'
include modulePrefix + 'testutils' include modulePrefix + 'testutils'
...@@ -56,6 +57,7 @@ project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/d ...@@ -56,6 +57,7 @@ project(modulePrefix + 'library-dash').projectDir = new File(rootDir, 'library/d
project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor') project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'library/extractor')
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls') project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming') project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
project(modulePrefix + 'library-transformer').projectDir = new File(rootDir, 'library/transformer')
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
project(modulePrefix + 'robolectricutils').projectDir = new File(rootDir, 'robolectricutils') project(modulePrefix + 'robolectricutils').projectDir = new File(rootDir, 'robolectricutils')
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
......
...@@ -312,7 +312,8 @@ import java.util.ArrayList; ...@@ -312,7 +312,8 @@ import java.util.ArrayList;
windowIndex = currentItemIndex; windowIndex = currentItemIndex;
} }
} }
previousPlayer.stop(true); previousPlayer.stop();
previousPlayer.clearMediaItems();
} }
this.currentPlayer = currentPlayer; this.currentPlayer = currentPlayer;
......
...@@ -152,7 +152,7 @@ public final class MainActivity extends Activity { ...@@ -152,7 +152,7 @@ public final class MainActivity extends Activity {
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER) .setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback); .build(drmCallback);
} else { } else {
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); drmSessionManager = DrmSessionManager.DRM_UNSUPPORTED;
} }
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this); DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
......
...@@ -354,6 +354,23 @@ ...@@ -354,6 +354,23 @@
"name": "VMAP midroll ad pod at 5 s with 10 skippable ads", "name": "VMAP midroll ad pod at 5 s with 10 skippable ads",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
"ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-10-skippable-ads" "ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-10-skippable-ads"
},
{
"name": "Playlist with three ad tags",
"playlist": [
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator="
},
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-25s.mp4",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator="
},
{
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
"ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator="
}
]
} }
] ]
}, },
......
...@@ -16,11 +16,13 @@ ...@@ -16,11 +16,13 @@
package com.google.android.exoplayer2.demo; package com.google.android.exoplayer2.demo;
import android.content.Context; import android.content.Context;
import android.os.Build;
import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.database.ExoDatabaseProvider; import com.google.android.exoplayer2.database.ExoDatabaseProvider;
import com.google.android.exoplayer2.ext.cronet.CronetDataSourceFactory; import com.google.android.exoplayer2.ext.cronet.CronetDataSource;
import com.google.android.exoplayer2.ext.cronet.CronetEngineWrapper; import com.google.android.exoplayer2.ext.cronet.CronetEngineWrapper;
import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil; import com.google.android.exoplayer2.offline.ActionFileUpgradeUtil;
import com.google.android.exoplayer2.offline.DefaultDownloadIndex; import com.google.android.exoplayer2.offline.DefaultDownloadIndex;
...@@ -28,6 +30,7 @@ import com.google.android.exoplayer2.offline.DownloadManager; ...@@ -28,6 +30,7 @@ import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.ui.DownloadNotificationHelper; import com.google.android.exoplayer2.ui.DownloadNotificationHelper;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
...@@ -36,6 +39,9 @@ import com.google.android.exoplayer2.upstream.cache.SimpleCache; ...@@ -36,6 +39,9 @@ import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
...@@ -44,6 +50,22 @@ public final class DemoUtil { ...@@ -44,6 +50,22 @@ public final class DemoUtil {
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"; public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
/**
* Whether the demo application uses Cronet for networking. Note that Cronet does not provide
* automatic support for cookies (https://github.com/google/ExoPlayer/issues/5975).
*
* <p>If set to false, the platform's default network stack is used with a {@link CookieManager}
* configured in {@link #getHttpDataSourceFactory}.
*/
private static final boolean USE_CRONET_FOR_NETWORKING = true;
private static final String USER_AGENT =
"ExoPlayerDemo/"
+ ExoPlayerLibraryInfo.VERSION
+ " (Linux; Android "
+ Build.VERSION.RELEASE
+ ") "
+ ExoPlayerLibraryInfo.VERSION_SLASHY;
private static final String TAG = "DemoUtil"; private static final String TAG = "DemoUtil";
private static final String DOWNLOAD_ACTION_FILE = "actions"; private static final String DOWNLOAD_ACTION_FILE = "actions";
private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions";
...@@ -78,10 +100,18 @@ public final class DemoUtil { ...@@ -78,10 +100,18 @@ public final class DemoUtil {
public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) { public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) {
if (httpDataSourceFactory == null) { if (httpDataSourceFactory == null) {
if (USE_CRONET_FOR_NETWORKING) {
context = context.getApplicationContext(); context = context.getApplicationContext();
CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper(context); CronetEngineWrapper cronetEngineWrapper =
new CronetEngineWrapper(context, USER_AGENT, /* preferGMSCoreCronet= */ false);
httpDataSourceFactory = httpDataSourceFactory =
new CronetDataSourceFactory(cronetEngineWrapper, Executors.newSingleThreadExecutor()); new CronetDataSource.Factory(cronetEngineWrapper, Executors.newSingleThreadExecutor());
} else {
CookieManager cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
CookieHandler.setDefault(cookieManager);
httpDataSourceFactory = new DefaultHttpDataSource.Factory().setUserAgent(USER_AGENT);
}
} }
return httpDataSourceFactory; return httpDataSourceFactory;
} }
......
...@@ -332,7 +332,7 @@ public class DownloadTracker { ...@@ -332,7 +332,7 @@ public class DownloadTracker {
/* titleId= */ R.string.exo_download_description, /* titleId= */ R.string.exo_download_description,
mappedTrackInfo, mappedTrackInfo,
trackSelectorParameters, trackSelectorParameters,
/* allowAdaptiveSelections =*/ false, /* allowAdaptiveSelections= */ false,
/* allowMultipleOverrides= */ true, /* allowMultipleOverrides= */ true,
/* onClickListener= */ this, /* onClickListener= */ this,
/* onDismissListener= */ this); /* onDismissListener= */ this);
......
...@@ -173,7 +173,9 @@ public class IntentUtil { ...@@ -173,7 +173,9 @@ public class IntentUtil {
.putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, playbackProperties.mimeType) .putExtra(MIME_TYPE_EXTRA + extrasKeySuffix, playbackProperties.mimeType)
.putExtra( .putExtra(
AD_TAG_URI_EXTRA + extrasKeySuffix, AD_TAG_URI_EXTRA + extrasKeySuffix,
playbackProperties.adTagUri != null ? playbackProperties.adTagUri.toString() : null); playbackProperties.adsConfiguration != null
? playbackProperties.adsConfiguration.adTagUri.toString()
: null);
if (playbackProperties.drmConfiguration != null) { if (playbackProperties.drmConfiguration != null) {
addDrmConfigurationToIntent(playbackProperties.drmConfiguration, intent, extrasKeySuffix); addDrmConfigurationToIntent(playbackProperties.drmConfiguration, intent, extrasKeySuffix);
} }
......
...@@ -19,7 +19,6 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull; ...@@ -19,7 +19,6 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.util.Pair; import android.util.Pair;
import android.view.KeyEvent; import android.view.KeyEvent;
...@@ -59,9 +58,6 @@ import com.google.android.exoplayer2.upstream.DataSource; ...@@ -59,9 +58,6 @@ import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.EventLogger; import com.google.android.exoplayer2.util.EventLogger;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
...@@ -78,13 +74,6 @@ public class PlayerActivity extends AppCompatActivity ...@@ -78,13 +74,6 @@ public class PlayerActivity extends AppCompatActivity
private static final String KEY_POSITION = "position"; private static final String KEY_POSITION = "position";
private static final String KEY_AUTO_PLAY = "auto_play"; private static final String KEY_AUTO_PLAY = "auto_play";
private static final CookieManager DEFAULT_COOKIE_MANAGER;
static {
DEFAULT_COOKIE_MANAGER = new CookieManager();
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
}
protected StyledPlayerView playerView; protected StyledPlayerView playerView;
protected LinearLayout debugRootView; protected LinearLayout debugRootView;
protected TextView debugTextView; protected TextView debugTextView;
...@@ -102,20 +91,16 @@ public class PlayerActivity extends AppCompatActivity ...@@ -102,20 +91,16 @@ public class PlayerActivity extends AppCompatActivity
private int startWindow; private int startWindow;
private long startPosition; private long startPosition;
// Fields used only for ad playback. // For ad playback only.
private AdsLoader adsLoader; private AdsLoader adsLoader;
private Uri loadedAdTagUri;
// Activity lifecycle // Activity lifecycle.
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
dataSourceFactory = DemoUtil.getDataSourceFactory(/* context= */ this); dataSourceFactory = DemoUtil.getDataSourceFactory(/* context= */ this);
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
}
setContentView(); setContentView();
debugRootView = findViewById(R.id.controls_root); debugRootView = findViewById(R.id.controls_root);
...@@ -348,7 +333,7 @@ public class PlayerActivity extends AppCompatActivity ...@@ -348,7 +333,7 @@ public class PlayerActivity extends AppCompatActivity
return Collections.emptyList(); return Collections.emptyList();
} }
} }
hasAds |= mediaItem.playbackProperties.adTagUri != null; hasAds |= mediaItem.playbackProperties.adsConfiguration != null;
} }
if (!hasAds) { if (!hasAds) {
releaseAdsLoader(); releaseAdsLoader();
...@@ -356,16 +341,7 @@ public class PlayerActivity extends AppCompatActivity ...@@ -356,16 +341,7 @@ public class PlayerActivity extends AppCompatActivity
return mediaItems; return mediaItems;
} }
private AdsLoader getAdsLoader(Uri adTagUri) { private AdsLoader getAdsLoader(MediaItem.AdsConfiguration adsConfiguration) {
if (mediaItems.size() > 1) {
showToast(R.string.unsupported_ads_in_playlist);
releaseAdsLoader();
return null;
}
if (!adTagUri.equals(loadedAdTagUri)) {
releaseAdsLoader();
loadedAdTagUri = adTagUri;
}
// The ads loader is reused for multiple playbacks, so that ad playback can resume. // The ads loader is reused for multiple playbacks, so that ad playback can resume.
if (adsLoader == null) { if (adsLoader == null) {
adsLoader = new ImaAdsLoader.Builder(/* context= */ this).build(); adsLoader = new ImaAdsLoader.Builder(/* context= */ this).build();
...@@ -394,7 +370,6 @@ public class PlayerActivity extends AppCompatActivity ...@@ -394,7 +370,6 @@ public class PlayerActivity extends AppCompatActivity
if (adsLoader != null) { if (adsLoader != null) {
adsLoader.release(); adsLoader.release();
adsLoader = null; adsLoader = null;
loadedAdTagUri = null;
playerView.getOverlayFrameLayout().removeAllViews(); playerView.getOverlayFrameLayout().removeAllViews();
} }
} }
......
...@@ -252,7 +252,7 @@ public class SampleChooserActivity extends AppCompatActivity ...@@ -252,7 +252,7 @@ public class SampleChooserActivity extends AppCompatActivity
} }
MediaItem.PlaybackProperties playbackProperties = MediaItem.PlaybackProperties playbackProperties =
checkNotNull(playlistHolder.mediaItems.get(0).playbackProperties); checkNotNull(playlistHolder.mediaItems.get(0).playbackProperties);
if (playbackProperties.adTagUri != null) { if (playbackProperties.adsConfiguration != null) {
return R.string.download_ads_unsupported; return R.string.download_ads_unsupported;
} }
String scheme = playbackProperties.uri.getScheme(); String scheme = playbackProperties.uri.getScheme();
......
...@@ -94,7 +94,7 @@ public final class TrackSelectionDialog extends DialogFragment { ...@@ -94,7 +94,7 @@ public final class TrackSelectionDialog extends DialogFragment {
/* titleId= */ R.string.track_selection_title, /* titleId= */ R.string.track_selection_title,
mappedTrackInfo, mappedTrackInfo,
/* initialParameters = */ parameters, /* initialParameters = */ parameters,
/* allowAdaptiveSelections =*/ true, /* allowAdaptiveSelections= */ true,
/* allowMultipleOverrides= */ false, /* allowMultipleOverrides= */ false,
/* onClickListener= */ (dialog, which) -> { /* onClickListener= */ (dialog, which) -> {
DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon(); DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon();
......
...@@ -45,8 +45,6 @@ ...@@ -45,8 +45,6 @@
<string name="sample_list_load_error">One or more sample lists failed to load</string> <string name="sample_list_load_error">One or more sample lists failed to load</string>
<string name="unsupported_ads_in_playlist">Playing without ads, as ads are not supported in playlists</string>
<string name="download_start_error">Failed to start download</string> <string name="download_start_error">Failed to start download</string>
<string name="download_start_error_offline_license">Failed to obtain offline license</string> <string name="download_start_error_offline_license">Failed to obtain offline license</string>
......
...@@ -197,7 +197,7 @@ public final class MainActivity extends Activity { ...@@ -197,7 +197,7 @@ public final class MainActivity extends Activity {
.setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER) .setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback); .build(drmCallback);
} else { } else {
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); drmSessionManager = DrmSessionManager.DRM_UNSUPPORTED;
} }
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this); DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this);
......
...@@ -5,3 +5,7 @@ ...@@ -5,3 +5,7 @@
native <methods>; native <methods>;
} }
# Some members of this class are being accessed from native methods. Keep them unobfuscated.
-keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer {
*;
}
...@@ -15,10 +15,12 @@ ...@@ -15,10 +15,12 @@
*/ */
package com.google.android.exoplayer2.ext.av1; package com.google.android.exoplayer2.ext.av1;
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
import static java.lang.Runtime.getRuntime; import static java.lang.Runtime.getRuntime;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
...@@ -28,7 +30,8 @@ import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer; ...@@ -28,7 +30,8 @@ import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** Gav1 decoder. */ /** Gav1 decoder. */
/* package */ final class Gav1Decoder @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
public final class Gav1Decoder
extends SimpleDecoder<VideoDecoderInputBuffer, VideoDecoderOutputBuffer, Gav1DecoderException> { extends SimpleDecoder<VideoDecoderInputBuffer, VideoDecoderOutputBuffer, Gav1DecoderException> {
// LINT.IfChange // LINT.IfChange
......
...@@ -15,12 +15,15 @@ ...@@ -15,12 +15,15 @@
*/ */
package com.google.android.exoplayer2.ext.av1; package com.google.android.exoplayer2.ext.av1;
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION;
import android.os.Handler; import android.os.Handler;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.TraceUtil;
...@@ -42,7 +45,10 @@ public class Libgav1VideoRenderer extends DecoderVideoRenderer { ...@@ -42,7 +45,10 @@ public class Libgav1VideoRenderer extends DecoderVideoRenderer {
private static final String TAG = "Libgav1VideoRenderer"; private static final String TAG = "Libgav1VideoRenderer";
private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4; private static final int DEFAULT_NUM_OF_INPUT_BUFFERS = 4;
private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4; private static final int DEFAULT_NUM_OF_OUTPUT_BUFFERS = 4;
/* Default size based on 720p resolution video compressed by a factor of two. */ /**
* Default input buffer size in bytes, based on 720p resolution video compressed by a factor of
* two.
*/
private static final int DEFAULT_INPUT_BUFFER_SIZE = private static final int DEFAULT_INPUT_BUFFER_SIZE =
Util.ceilDivide(1280, 64) * Util.ceilDivide(720, 64) * (64 * 64 * 3 / 2) / 2; Util.ceilDivide(1280, 64) * Util.ceilDivide(720, 64) * (64 * 64 * 3 / 2) / 2;
...@@ -124,12 +130,13 @@ public class Libgav1VideoRenderer extends DecoderVideoRenderer { ...@@ -124,12 +130,13 @@ public class Libgav1VideoRenderer extends DecoderVideoRenderer {
public final int supportsFormat(Format format) { public final int supportsFormat(Format format) {
if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType) if (!MimeTypes.VIDEO_AV1.equalsIgnoreCase(format.sampleMimeType)
|| !Gav1Library.isAvailable()) { || !Gav1Library.isAvailable()) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
} }
if (format.exoMediaCryptoType != null) { if (format.exoMediaCryptoType != null) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_DRM);
} }
return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED); return RendererCapabilities.create(
C.FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
} }
@Override @Override
...@@ -164,7 +171,13 @@ public class Libgav1VideoRenderer extends DecoderVideoRenderer { ...@@ -164,7 +171,13 @@ public class Libgav1VideoRenderer extends DecoderVideoRenderer {
} }
@Override @Override
protected boolean canKeepCodec(Format oldFormat, Format newFormat) { protected DecoderReuseEvaluation canReuseDecoder(
return true; String decoderName, Format oldFormat, Format newFormat) {
return new DecoderReuseEvaluation(
decoderName,
oldFormat,
newFormat,
REUSE_RESULT_YES_WITHOUT_RECONFIGURATION,
/* discardReasons= */ 0);
} }
} }
...@@ -16,7 +16,7 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" ...@@ -16,7 +16,7 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies { dependencies {
api 'com.google.android.gms:play-services-cast-framework:18.1.0' api 'com.google.android.gms:play-services-cast-framework:18.1.0'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-common')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
......
...@@ -46,8 +46,8 @@ import java.util.Arrays; ...@@ -46,8 +46,8 @@ import java.util.Arrays;
private ItemData() { private ItemData() {
this( this(
/* durationUs= */ C.TIME_UNSET, /* defaultPositionUs */ /* durationUs= */ C.TIME_UNSET,
C.TIME_UNSET, /* defaultPositionUs= */ C.TIME_UNSET,
/* isLive= */ false); /* isLive= */ false);
} }
...@@ -126,16 +126,18 @@ import java.util.Arrays; ...@@ -126,16 +126,18 @@ import java.util.Arrays;
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
long durationUs = durationsUs[windowIndex]; long durationUs = durationsUs[windowIndex];
boolean isDynamic = durationUs == C.TIME_UNSET; boolean isDynamic = durationUs == C.TIME_UNSET;
MediaItem mediaItem =
new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build();
return window.set( return window.set(
/* uid= */ ids[windowIndex], /* uid= */ ids[windowIndex],
/* mediaItem= */ new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build(), /* mediaItem= */ mediaItem,
/* manifest= */ null, /* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET, /* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* isSeekable= */ !isDynamic, /* isSeekable= */ !isDynamic,
isDynamic, isDynamic,
isLive[windowIndex], isLive[windowIndex] ? mediaItem.liveConfiguration : null,
defaultPositionsUs[windowIndex], defaultPositionsUs[windowIndex],
durationUs, durationUs,
/* firstPeriodIndex= */ windowIndex, /* firstPeriodIndex= */ windowIndex,
......
/*
* 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 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;
}
}
...@@ -45,7 +45,9 @@ public final class DefaultMediaItemConverter implements MediaItemConverter { ...@@ -45,7 +45,9 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
@Override @Override
public MediaItem toMediaItem(MediaQueueItem item) { public MediaItem toMediaItem(MediaQueueItem item) {
// `item` came from `toMediaQueueItem()` so the custom JSON data must be set. // `item` came from `toMediaQueueItem()` so the custom JSON data must be set.
return getMediaItem(Assertions.checkNotNull(item.getMedia().getCustomData())); MediaInfo mediaInfo = item.getMedia();
Assertions.checkNotNull(mediaInfo);
return getMediaItem(Assertions.checkNotNull(mediaInfo.getCustomData()));
} }
@Override @Override
......
/*
* 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);
}
}
...@@ -13,14 +13,28 @@ ...@@ -13,14 +13,28 @@
// limitations under the License. // limitations under the License.
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
android {
defaultConfig {
multiDexEnabled true
}
}
dependencies { dependencies {
api "com.google.android.gms:play-services-cronet:17.0.0" api "com.google.android.gms:play-services-cronet:17.0.0"
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-common')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
// Instrumentation tests assume that an app-packaged version of cronet is
// available.
androidTestImplementation 'org.chromium.net:cronet-embedded:72.3626.96'
androidTestImplementation(project(modulePrefix + 'testutils'))
testImplementation project(modulePrefix + 'library') testImplementation project(modulePrefix + 'library')
testImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils')
testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
......
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.ext.cronet">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-sdk/>
<application
android:allowBackup="false"
android:usesCleartextTraffic="true"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode"/>
<instrumentation
android:targetPackage="com.google.android.exoplayer2.ext.cronet"
android:name="androidx.test.runner.AndroidJUnitRunner"/>
</manifest>
/*
* 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.cronet;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.DataSourceContractTest;
import com.google.android.exoplayer2.testutil.HttpDataSourceTestEnv;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.common.collect.ImmutableList;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.After;
import org.junit.Rule;
import org.junit.runner.RunWith;
/** {@link DataSource} contract tests for {@link CronetDataSource}. */
@RunWith(AndroidJUnit4.class)
public class CronetDataSourceContractTest extends DataSourceContractTest {
@Rule public HttpDataSourceTestEnv httpDataSourceTestEnv = new HttpDataSourceTestEnv();
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
@After
public void tearDown() {
executorService.shutdown();
}
@Override
protected DataSource createDataSource() {
CronetEngineWrapper cronetEngineWrapper =
new CronetEngineWrapper(
ApplicationProvider.getApplicationContext(),
/* userAgent= */ "test-agent",
/* preferGMSCoreCronet= */ false);
assertThat(cronetEngineWrapper.getCronetEngineSource())
.isEqualTo(CronetEngineWrapper.SOURCE_NATIVE);
return new CronetDataSource.Factory(cronetEngineWrapper, executorService)
.setFallbackFactory(new InvalidDataSourceFactory())
.createDataSource();
}
@Override
protected ImmutableList<TestResource> getTestResources() {
return httpDataSourceTestEnv.getServedResources();
}
@Override
protected Uri getNotFoundUri() {
return Uri.parse(httpDataSourceTestEnv.getNonexistentUrl());
}
/**
* An {@link HttpDataSource.Factory} that throws {@link UnsupportedOperationException} on every
* interaction.
*/
private static class InvalidDataSourceFactory implements HttpDataSource.Factory {
@Override
public HttpDataSource createDataSource() {
throw new UnsupportedOperationException();
}
@Override
public HttpDataSource.RequestProperties getDefaultRequestProperties() {
throw new UnsupportedOperationException();
}
@Override
public HttpDataSource.Factory setDefaultRequestProperties(
Map<String, String> defaultRequestProperties) {
throw new UnsupportedOperationException();
}
}
}
...@@ -73,25 +73,29 @@ public final class CronetEngineWrapper { ...@@ -73,25 +73,29 @@ public final class CronetEngineWrapper {
public static final int SOURCE_UNAVAILABLE = 4; public static final int SOURCE_UNAVAILABLE = 4;
/** /**
* Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable * Creates a wrapper for a {@link CronetEngine} built using the most suitable {@link
* {@link CronetProvider}. Sets wrapper to prefer natively bundled Cronet over GMSCore Cronet * CronetProvider}. When natively bundled Cronet and GMSCore Cronet are both available, the
* if both are available. * natively bundled provider is preferred.
* *
* @param context A context. * @param context A context.
*/ */
public CronetEngineWrapper(Context context) { public CronetEngineWrapper(Context context) {
this(context, false); this(context, /* userAgent= */ null, /* preferGMSCoreCronet= */ false);
} }
/** /**
* Creates a wrapper for a {@link CronetEngine} which automatically selects the most suitable * Creates a wrapper for a {@link CronetEngine} built using the most suitable {@link
* {@link CronetProvider} based on user preference. * CronetProvider}. When natively bundled Cronet and GMSCore Cronet are both available, {@code
* preferGMSCoreCronet} determines which is preferred.
* *
* @param context A context. * @param context A context.
* @param userAgent A default user agent, or {@code null} to use a default user agent of the
* {@link CronetEngine}.
* @param preferGMSCoreCronet Whether Cronet from GMSCore should be preferred over natively * @param preferGMSCoreCronet Whether Cronet from GMSCore should be preferred over natively
* bundled Cronet if both are available. * bundled Cronet if both are available.
*/ */
public CronetEngineWrapper(Context context, boolean preferGMSCoreCronet) { public CronetEngineWrapper(
Context context, @Nullable String userAgent, boolean preferGMSCoreCronet) {
CronetEngine cronetEngine = null; CronetEngine cronetEngine = null;
@CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE; @CronetEngineSource int cronetEngineSource = SOURCE_UNAVAILABLE;
List<CronetProvider> cronetProviders = new ArrayList<>(CronetProvider.getAllProviders(context)); List<CronetProvider> cronetProviders = new ArrayList<>(CronetProvider.getAllProviders(context));
...@@ -108,7 +112,11 @@ public final class CronetEngineWrapper { ...@@ -108,7 +112,11 @@ public final class CronetEngineWrapper {
for (int i = 0; i < cronetProviders.size() && cronetEngine == null; i++) { for (int i = 0; i < cronetProviders.size() && cronetEngine == null; i++) {
String providerName = cronetProviders.get(i).getName(); String providerName = cronetProviders.get(i).getName();
try { try {
cronetEngine = cronetProviders.get(i).createBuilder().build(); CronetEngine.Builder cronetEngineBuilder = cronetProviders.get(i).createBuilder();
if (userAgent != null) {
cronetEngineBuilder.setUserAgent(userAgent);
}
cronetEngine = cronetEngineBuilder.build();
if (providerComparator.isNativeProvider(providerName)) { if (providerComparator.isNativeProvider(providerName)) {
cronetEngineSource = SOURCE_NATIVE; cronetEngineSource = SOURCE_NATIVE;
} else if (providerComparator.isGMSCoreProvider(providerName)) { } else if (providerComparator.isGMSCoreProvider(providerName)) {
...@@ -133,9 +141,9 @@ public final class CronetEngineWrapper { ...@@ -133,9 +141,9 @@ public final class CronetEngineWrapper {
} }
/** /**
* Creates a wrapper for an existing CronetEngine. * Creates a wrapper for an existing {@link CronetEngine}.
* *
* @param cronetEngine An existing CronetEngine. * @param cronetEngine The CronetEngine to wrap.
*/ */
public CronetEngineWrapper(CronetEngine cronetEngine) { public CronetEngineWrapper(CronetEngine cronetEngine) {
this.cronetEngine = cronetEngine; this.cronetEngine = cronetEngine;
......
...@@ -91,19 +91,19 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer<FfmpegAudioD ...@@ -91,19 +91,19 @@ public final class FfmpegAudioRenderer extends DecoderAudioRenderer<FfmpegAudioD
} }
@Override @Override
@FormatSupport @C.FormatSupport
protected int supportsFormatInternal(Format format) { protected int supportsFormatInternal(Format format) {
String mimeType = Assertions.checkNotNull(format.sampleMimeType); String mimeType = Assertions.checkNotNull(format.sampleMimeType);
if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(mimeType)) { if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(mimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return C.FORMAT_UNSUPPORTED_TYPE;
} else if (!FfmpegLibrary.supportsFormat(mimeType) } else if (!FfmpegLibrary.supportsFormat(mimeType)
|| (!sinkSupportsFormat(format, C.ENCODING_PCM_16BIT) || (!sinkSupportsFormat(format, C.ENCODING_PCM_16BIT)
&& !sinkSupportsFormat(format, C.ENCODING_PCM_FLOAT))) { && !sinkSupportsFormat(format, C.ENCODING_PCM_FLOAT))) {
return FORMAT_UNSUPPORTED_SUBTYPE; return C.FORMAT_UNSUPPORTED_SUBTYPE;
} else if (format.exoMediaCryptoType != null) { } else if (format.exoMediaCryptoType != null) {
return FORMAT_UNSUPPORTED_DRM; return C.FORMAT_UNSUPPORTED_DRM;
} else { } else {
return FORMAT_HANDLED; return C.FORMAT_HANDLED;
} }
} }
......
...@@ -34,8 +34,7 @@ public final class FfmpegLibrary { ...@@ -34,8 +34,7 @@ public final class FfmpegLibrary {
private static final String TAG = "FfmpegLibrary"; private static final String TAG = "FfmpegLibrary";
private static final LibraryLoader LOADER = private static final LibraryLoader LOADER = new LibraryLoader("ffmpegJNI");
new LibraryLoader("avutil", "swresample", "avcodec", "ffmpeg_jni");
private static @MonotonicNonNull String version; private static @MonotonicNonNull String version;
private static int inputBufferPaddingSize = C.LENGTH_UNSET; private static int inputBufferPaddingSize = C.LENGTH_UNSET;
...@@ -145,10 +144,6 @@ public final class FfmpegLibrary { ...@@ -145,10 +144,6 @@ public final class FfmpegLibrary {
return "pcm_mulaw"; return "pcm_mulaw";
case MimeTypes.AUDIO_ALAW: case MimeTypes.AUDIO_ALAW:
return "pcm_alaw"; return "pcm_alaw";
case MimeTypes.VIDEO_H264:
return "h264";
case MimeTypes.VIDEO_H265:
return "hevc";
default: default:
return null; return null;
} }
......
...@@ -3,17 +3,17 @@ cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR) ...@@ -3,17 +3,17 @@ cmake_minimum_required(VERSION 3.7.1 FATAL_ERROR)
# Enable C++11 features. # Enable C++11 features.
set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD 11)
project(libffmpeg_jni C CXX) project(libffmpegJNI C CXX)
set(ffmpeg_location "${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg") set(ffmpeg_location "${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg")
set(ffmpeg_binaries "${ffmpeg_location}/android-libs/${ANDROID_ABI}") set(ffmpeg_binaries "${ffmpeg_location}/android-libs/${ANDROID_ABI}")
foreach(ffmpeg_lib avutil swresample avcodec) foreach(ffmpeg_lib avutil swresample avcodec)
set(ffmpeg_lib_filename lib${ffmpeg_lib}.so) set(ffmpeg_lib_filename lib${ffmpeg_lib}.a)
set(ffmpeg_lib_file_path ${ffmpeg_binaries}/${ffmpeg_lib_filename}) set(ffmpeg_lib_file_path ${ffmpeg_binaries}/${ffmpeg_lib_filename})
add_library( add_library(
${ffmpeg_lib} ${ffmpeg_lib}
SHARED STATIC
IMPORTED) IMPORTED)
set_target_properties( set_target_properties(
${ffmpeg_lib} PROPERTIES ${ffmpeg_lib} PROPERTIES
...@@ -24,13 +24,13 @@ endforeach() ...@@ -24,13 +24,13 @@ endforeach()
include_directories(${ffmpeg_location}) include_directories(${ffmpeg_location})
find_library(android_log_lib log) find_library(android_log_lib log)
add_library(ffmpeg_jni add_library(ffmpegJNI
SHARED SHARED
ffmpeg_jni.cc) ffmpeg_jni.cc)
target_link_libraries(ffmpeg_jni target_link_libraries(ffmpegJNI
PRIVATE android PRIVATE android
PRIVATE avutil
PRIVATE swresample PRIVATE swresample
PRIVATE avcodec PRIVATE avcodec
PRIVATE avutil
PRIVATE ${android_log_lib}) PRIVATE ${android_log_lib})
...@@ -19,10 +19,12 @@ FFMPEG_EXT_PATH=$1 ...@@ -19,10 +19,12 @@ FFMPEG_EXT_PATH=$1
NDK_PATH=$2 NDK_PATH=$2
HOST_PLATFORM=$3 HOST_PLATFORM=$3
ENABLED_DECODERS=("${@:4}") ENABLED_DECODERS=("${@:4}")
JOBS=$(nproc 2> /dev/null || sysctl -n hw.ncpu 2> /dev/null || echo 4)
echo "Using $JOBS jobs for make"
COMMON_OPTIONS=" COMMON_OPTIONS="
--target-os=android --target-os=android
--disable-static --enable-static
--enable-shared --disable-shared
--disable-doc --disable-doc
--disable-programs --disable-programs
--disable-everything --disable-everything
...@@ -48,11 +50,13 @@ cd "${FFMPEG_EXT_PATH}/jni/ffmpeg" ...@@ -48,11 +50,13 @@ cd "${FFMPEG_EXT_PATH}/jni/ffmpeg"
--cpu=armv7-a \ --cpu=armv7-a \
--cross-prefix="${TOOLCHAIN_PREFIX}/armv7a-linux-androideabi16-" \ --cross-prefix="${TOOLCHAIN_PREFIX}/armv7a-linux-androideabi16-" \
--nm="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-nm" \ --nm="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-nm" \
--ar="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-ar" \
--ranlib="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-ranlib" \
--strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \ --strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \
--extra-cflags="-march=armv7-a -mfloat-abi=softfp" \ --extra-cflags="-march=armv7-a -mfloat-abi=softfp" \
--extra-ldflags="-Wl,--fix-cortex-a8" \ --extra-ldflags="-Wl,--fix-cortex-a8" \
${COMMON_OPTIONS} ${COMMON_OPTIONS}
make -j4 make -j$JOBS
make install-libs make install-libs
make clean make clean
./configure \ ./configure \
...@@ -61,9 +65,11 @@ make clean ...@@ -61,9 +65,11 @@ make clean
--cpu=armv8-a \ --cpu=armv8-a \
--cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \ --cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \
--nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \ --nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \
--ar="${TOOLCHAIN_PREFIX}/aarch64-linux-android-ar" \
--ranlib="${TOOLCHAIN_PREFIX}/aarch64-linux-android-ranlib" \
--strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \ --strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \
${COMMON_OPTIONS} ${COMMON_OPTIONS}
make -j4 make -j$JOBS
make install-libs make install-libs
make clean make clean
./configure \ ./configure \
...@@ -72,10 +78,12 @@ make clean ...@@ -72,10 +78,12 @@ make clean
--cpu=i686 \ --cpu=i686 \
--cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \ --cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \
--nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \ --nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \
--ar="${TOOLCHAIN_PREFIX}/i686-linux-android-ar" \
--ranlib="${TOOLCHAIN_PREFIX}/i686-linux-android-ranlib" \
--strip="${TOOLCHAIN_PREFIX}/i686-linux-android-strip" \ --strip="${TOOLCHAIN_PREFIX}/i686-linux-android-strip" \
--disable-asm \ --disable-asm \
${COMMON_OPTIONS} ${COMMON_OPTIONS}
make -j4 make -j$JOBS
make install-libs make install-libs
make clean make clean
./configure \ ./configure \
...@@ -84,9 +92,11 @@ make clean ...@@ -84,9 +92,11 @@ make clean
--cpu=x86_64 \ --cpu=x86_64 \
--cross-prefix="${TOOLCHAIN_PREFIX}/x86_64-linux-android21-" \ --cross-prefix="${TOOLCHAIN_PREFIX}/x86_64-linux-android21-" \
--nm="${TOOLCHAIN_PREFIX}/x86_64-linux-android-nm" \ --nm="${TOOLCHAIN_PREFIX}/x86_64-linux-android-nm" \
--ar="${TOOLCHAIN_PREFIX}/x86_64-linux-android-ar" \
--ranlib="${TOOLCHAIN_PREFIX}/x86_64-linux-android-ranlib" \
--strip="${TOOLCHAIN_PREFIX}/x86_64-linux-android-strip" \ --strip="${TOOLCHAIN_PREFIX}/x86_64-linux-android-strip" \
--disable-asm \ --disable-asm \
${COMMON_OPTIONS} ${COMMON_OPTIONS}
make -j4 make -j$JOBS
make install-libs make install-libs
make clean make clean
...@@ -68,7 +68,7 @@ renderer. ...@@ -68,7 +68,7 @@ renderer.
### Using `FlacExtractor` ### ### Using `FlacExtractor` ###
`FlacExtractor` is used via `ExtractorMediaSource`. If you're using `FlacExtractor` is used via `ProgressiveMediaSource`. If you're using
`DefaultExtractorsFactory`, `FlacExtractor` will automatically be used to read `DefaultExtractorsFactory`, `FlacExtractor` will automatically be used to read
`.flac` files. If you're not using `DefaultExtractorsFactory`, return a `.flac` files. If you're not using `DefaultExtractorsFactory`, return a
`FlacExtractor` from your `ExtractorsFactory.createExtractors` implementation. `FlacExtractor` from your `ExtractorsFactory.createExtractors` implementation.
......
...@@ -15,7 +15,10 @@ ...@@ -15,7 +15,10 @@
*/ */
package com.google.android.exoplayer2.ext.flac; package com.google.android.exoplayer2.ext.flac;
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
...@@ -27,11 +30,10 @@ import java.io.IOException; ...@@ -27,11 +30,10 @@ import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
/** /** Flac decoder. */
* Flac decoder. @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
*/ public final class FlacDecoder
/* package */ final class FlacDecoder extends extends SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FlacDecoderException> {
SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, FlacDecoderException> {
private final FlacStreamMetadata streamMetadata; private final FlacStreamMetadata streamMetadata;
private final FlacDecoderJni decoderJni; private final FlacDecoderJni decoderJni;
......
...@@ -79,11 +79,11 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer<FlacDecoder ...@@ -79,11 +79,11 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer<FlacDecoder
} }
@Override @Override
@FormatSupport @C.FormatSupport
protected int supportsFormatInternal(Format format) { protected int supportsFormatInternal(Format format) {
if (!FlacLibrary.isAvailable() if (!FlacLibrary.isAvailable()
|| !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return C.FORMAT_UNSUPPORTED_TYPE;
} }
// Compute the format that the FLAC decoder will output. // Compute the format that the FLAC decoder will output.
Format outputFormat; Format outputFormat;
...@@ -102,11 +102,11 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer<FlacDecoder ...@@ -102,11 +102,11 @@ public final class LibflacAudioRenderer extends DecoderAudioRenderer<FlacDecoder
outputFormat = getOutputFormat(streamMetadata); outputFormat = getOutputFormat(streamMetadata);
} }
if (!sinkSupportsFormat(outputFormat)) { if (!sinkSupportsFormat(outputFormat)) {
return FORMAT_UNSUPPORTED_SUBTYPE; return C.FORMAT_UNSUPPORTED_SUBTYPE;
} else if (format.exoMediaCryptoType != null) { } else if (format.exoMediaCryptoType != null) {
return FORMAT_UNSUPPORTED_DRM; return C.FORMAT_UNSUPPORTED_DRM;
} else { } else {
return FORMAT_HANDLED; return C.FORMAT_HANDLED;
} }
} }
......
...@@ -33,17 +33,19 @@ of the developer guide. The `AdsLoaderProvider` passed to the player's ...@@ -33,17 +33,19 @@ of the developer guide. The `AdsLoaderProvider` passed to the player's
extension only supports players which are accessed on the application's main extension only supports players which are accessed on the application's main
thread. thread.
Resuming the player after entering the background requires some special handling Resuming the player after entering the background requires some special
when playing ads. The player and its media source are released on entering the handling when playing ads. The player and its media source are released on
background, and are recreated when returning to the foreground. When playing ads entering the background, and are recreated when returning to the foreground.
it is necessary to persist ad playback state while in the background by keeping When playing ads it is necessary to persist ad playback state while in the
a reference to the `ImaAdsLoader`. When re-entering the foreground, pass the background by keeping a reference to the `ImaAdsLoader`. When re-entering the
same instance back when `AdsLoaderProvider.getAdsLoader(Uri adTagUri)` is called foreground, pass the same instance back when
to restore the state. It is also important to persist the player position when `AdsLoaderProvider.getAdsLoader(MediaItem.AdsConfiguration adsConfiguration)`
entering the background by storing the value of `player.getContentPosition()`. is called to restore the state. It is also important to persist the player
On returning to the foreground, seek to that position before preparing the new position when entering the background by storing the value of
player instance. Finally, it is important to call `ImaAdsLoader.release()` when `player.getContentPosition()`. On returning to the foreground, seek to that
playback has finished and will not be resumed. position before preparing the new player instance. Finally, it is important to
call `ImaAdsLoader.release()` when playback has finished and will not be
resumed.
You can try the IMA extension in the ExoPlayer demo app, which has test content You can try the IMA extension in the ExoPlayer demo app, which has test content
in the "IMA sample ad tags" section of the sample chooser. The demo app's in the "IMA sample ad tags" section of the sample chooser. The demo app's
......
...@@ -25,7 +25,7 @@ android { ...@@ -25,7 +25,7 @@ android {
} }
dependencies { dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.21.0' api 'com.google.ads.interactivemedia.v3:interactivemedia:3.22.0'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
......
# Proguard rules specific to the IMA extension. # Proguard rules specific to the IMA extension.
-dontwarn com.google.ads.interactivemedia.**
-keep class com.google.ads.interactivemedia.** { *; } -keep class com.google.ads.interactivemedia.** { *; }
-keep interface com.google.ads.interactivemedia.** { *; } -keep interface com.google.ads.interactivemedia.** { *; }
-keep class com.google.obf.** { *; } -keep class com.google.obf.** { *; }
......
...@@ -248,6 +248,7 @@ public final class ImaPlaybackTest { ...@@ -248,6 +248,7 @@ public final class ImaPlaybackTest {
return new AdsMediaSource( return new AdsMediaSource(
contentMediaSource, contentMediaSource,
adTagDataSpec, adTagDataSpec,
/* adsId= */ adTagDataSpec.uri,
new DefaultMediaSourceFactory(dataSourceFactory), new DefaultMediaSourceFactory(dataSourceFactory),
Assertions.checkNotNull(imaAdsLoader), Assertions.checkNotNull(imaAdsLoader),
new AdViewProvider() { new AdViewProvider() {
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.ima; package com.google.android.exoplayer2.ext.ima;
import android.content.Context; import android.content.Context;
import android.os.Looper;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -35,7 +36,6 @@ import com.google.ads.interactivemedia.v3.api.UiElement; ...@@ -35,7 +36,6 @@ import com.google.ads.interactivemedia.v3.api.UiElement;
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo; import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo;
import com.google.android.exoplayer2.upstream.DataSchemeDataSource; import com.google.android.exoplayer2.upstream.DataSchemeDataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
...@@ -89,6 +89,7 @@ import java.util.Set; ...@@ -89,6 +89,7 @@ import java.util.Set;
public final boolean focusSkipButtonWhenAvailable; public final boolean focusSkipButtonWhenAvailable;
public final boolean playAdBeforeStartPosition; public final boolean playAdBeforeStartPosition;
public final int mediaBitrate; public final int mediaBitrate;
@Nullable public final Boolean enableContinuousPlayback;
@Nullable public final List<String> adMediaMimeTypes; @Nullable public final List<String> adMediaMimeTypes;
@Nullable public final Set<UiElement> adUiElements; @Nullable public final Set<UiElement> adUiElements;
@Nullable public final Collection<CompanionAdSlot> companionAdSlots; @Nullable public final Collection<CompanionAdSlot> companionAdSlots;
...@@ -105,6 +106,7 @@ import java.util.Set; ...@@ -105,6 +106,7 @@ import java.util.Set;
boolean focusSkipButtonWhenAvailable, boolean focusSkipButtonWhenAvailable,
boolean playAdBeforeStartPosition, boolean playAdBeforeStartPosition,
int mediaBitrate, int mediaBitrate,
@Nullable Boolean enableContinuousPlayback,
@Nullable List<String> adMediaMimeTypes, @Nullable List<String> adMediaMimeTypes,
@Nullable Set<UiElement> adUiElements, @Nullable Set<UiElement> adUiElements,
@Nullable Collection<CompanionAdSlot> companionAdSlots, @Nullable Collection<CompanionAdSlot> companionAdSlots,
...@@ -119,6 +121,7 @@ import java.util.Set; ...@@ -119,6 +121,7 @@ import java.util.Set;
this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable;
this.playAdBeforeStartPosition = playAdBeforeStartPosition; this.playAdBeforeStartPosition = playAdBeforeStartPosition;
this.mediaBitrate = mediaBitrate; this.mediaBitrate = mediaBitrate;
this.enableContinuousPlayback = enableContinuousPlayback;
this.adMediaMimeTypes = adMediaMimeTypes; this.adMediaMimeTypes = adMediaMimeTypes;
this.adUiElements = adUiElements; this.adUiElements = adUiElements;
this.companionAdSlots = companionAdSlots; this.companionAdSlots = companionAdSlots;
...@@ -130,6 +133,9 @@ import java.util.Set; ...@@ -130,6 +133,9 @@ import java.util.Set;
} }
} }
public static final int TIMEOUT_UNSET = -1;
public static final int BITRATE_UNSET = -1;
/** /**
* Returns the IMA {@link FriendlyObstructionPurpose} corresponding to the given {@link * Returns the IMA {@link FriendlyObstructionPurpose} corresponding to the given {@link
* OverlayInfo#purpose}. * OverlayInfo#purpose}.
...@@ -150,15 +156,14 @@ import java.util.Set; ...@@ -150,15 +156,14 @@ import java.util.Set;
} }
/** /**
* Returns an initial {@link AdPlaybackState} with ad groups at the provided {@code cuePoints}. * Returns the microsecond ad group timestamps corresponding to the specified cue points.
* *
* @param cuePoints The cue points of the ads in seconds. * @param cuePoints The cue points of the ads in seconds, provided by the IMA SDK.
* @return The {@link AdPlaybackState}. * @return The corresponding microsecond ad group timestamps.
*/ */
public static AdPlaybackState getInitialAdPlaybackStateForCuePoints(List<Float> cuePoints) { public static long[] getAdGroupTimesUsForCuePoints(List<Float> cuePoints) {
if (cuePoints.isEmpty()) { if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad. return new long[] {0L};
return new AdPlaybackState(/* adGroupTimesUs...= */ 0);
} }
int count = cuePoints.size(); int count = cuePoints.size();
...@@ -174,7 +179,7 @@ import java.util.Set; ...@@ -174,7 +179,7 @@ import java.util.Set;
} }
// Cue points may be out of order, so sort them. // Cue points may be out of order, so sort them.
Arrays.sort(adGroupTimesUs, 0, adGroupIndex); Arrays.sort(adGroupTimesUs, 0, adGroupIndex);
return new AdPlaybackState(adGroupTimesUs); return adGroupTimesUs;
} }
/** Returns an {@link AdsRequest} based on the specified ad tag {@link DataSpec}. */ /** Returns an {@link AdsRequest} based on the specified ad tag {@link DataSpec}. */
...@@ -203,6 +208,13 @@ import java.util.Set; ...@@ -203,6 +208,13 @@ import java.util.Set;
|| adError.getErrorCode() == AdError.AdErrorCode.UNKNOWN_ERROR; || adError.getErrorCode() == AdError.AdErrorCode.UNKNOWN_ERROR;
} }
/** Returns the looper on which all IMA SDK interaction must occur. */
public static Looper getImaLooper() {
// IMA SDK callbacks occur on the main thread. This method can be used to check that the player
// is using the same looper, to ensure all interaction with this class is on the main thread.
return Looper.getMainLooper();
}
/** Returns a human-readable representation of a video progress update. */ /** Returns a human-readable representation of a video progress update. */
public static String getStringForVideoProgressUpdate(VideoProgressUpdate videoProgressUpdate) { public static String getStringForVideoProgressUpdate(VideoProgressUpdate videoProgressUpdate) {
if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) { if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) {
......
...@@ -21,25 +21,32 @@ import com.google.android.exoplayer2.Player; ...@@ -21,25 +21,32 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.testutil.StubExoPlayer; import com.google.android.exoplayer2.testutil.StubExoPlayer;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import java.util.ArrayList; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ListenerSet;
/** A fake player for testing content/ad playback. */ /** A fake player for testing content/ad playback. */
/* package */ final class FakePlayer extends StubExoPlayer { /* package */ final class FakePlayer extends StubExoPlayer {
private final ArrayList<Player.EventListener> listeners; private final ListenerSet<EventListener, Events> listeners;
private final Timeline.Period period; private final Timeline.Period period;
private final Timeline timeline;
private Timeline timeline;
@Player.State private int state; @Player.State private int state;
private boolean playWhenReady; private boolean playWhenReady;
private long position; private int periodIndex;
private long contentPosition; private long positionMs;
private long contentPositionMs;
private boolean isPlayingAd; private boolean isPlayingAd;
private int adGroupIndex; private int adGroupIndex;
private int adIndexInAdGroup; private int adIndexInAdGroup;
public FakePlayer() { public FakePlayer() {
listeners = new ArrayList<>(); listeners =
new ListenerSet<>(
Looper.getMainLooper(),
Clock.DEFAULT,
Player.Events::new,
(listener, eventFlags) -> listener.onEvents(/* player= */ this, eventFlags));
period = new Timeline.Period(); period = new Timeline.Period();
state = Player.STATE_IDLE; state = Player.STATE_IDLE;
playWhenReady = true; playWhenReady = true;
...@@ -48,26 +55,27 @@ import java.util.ArrayList; ...@@ -48,26 +55,27 @@ import java.util.ArrayList;
/** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */ /** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */
public void updateTimeline(Timeline timeline, @TimelineChangeReason int reason) { public void updateTimeline(Timeline timeline, @TimelineChangeReason int reason) {
for (Player.EventListener listener : listeners) { this.timeline = timeline;
listener.onTimelineChanged(timeline, reason); listeners.sendEvent(
} Player.EVENT_TIMELINE_CHANGED, listener -> listener.onTimelineChanged(timeline, reason));
} }
/** /**
* Sets the state of this player as if it were playing content at the given {@code position}. If * Sets the state of this player as if it were playing content at the given {@code position}. If
* an ad is currently playing, this will trigger a position discontinuity. * an ad is currently playing, this will trigger a position discontinuity.
*/ */
public void setPlayingContentPosition(long position) { public void setPlayingContentPosition(int periodIndex, long positionMs) {
boolean notify = isPlayingAd; boolean notify = isPlayingAd;
isPlayingAd = false; isPlayingAd = false;
adGroupIndex = C.INDEX_UNSET; adGroupIndex = C.INDEX_UNSET;
adIndexInAdGroup = C.INDEX_UNSET; adIndexInAdGroup = C.INDEX_UNSET;
this.position = position; this.periodIndex = periodIndex;
contentPosition = position; this.positionMs = positionMs;
contentPositionMs = positionMs;
if (notify) { if (notify) {
for (Player.EventListener listener : listeners) { listeners.sendEvent(
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION); Player.EVENT_POSITION_DISCONTINUITY,
} listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION));
} }
} }
...@@ -77,17 +85,22 @@ import java.util.ArrayList; ...@@ -77,17 +85,22 @@ import java.util.ArrayList;
* position discontinuity. * position discontinuity.
*/ */
public void setPlayingAdPosition( public void setPlayingAdPosition(
int adGroupIndex, int adIndexInAdGroup, long position, long contentPosition) { int periodIndex,
int adGroupIndex,
int adIndexInAdGroup,
long positionMs,
long contentPositionMs) {
boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup; boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup;
isPlayingAd = true; isPlayingAd = true;
this.periodIndex = periodIndex;
this.adGroupIndex = adGroupIndex; this.adGroupIndex = adGroupIndex;
this.adIndexInAdGroup = adIndexInAdGroup; this.adIndexInAdGroup = adIndexInAdGroup;
this.position = position; this.positionMs = positionMs;
this.contentPosition = contentPosition; this.contentPositionMs = contentPositionMs;
if (notify) { if (notify) {
for (Player.EventListener listener : listeners) { listeners.sendEvent(
listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION); EVENT_POSITION_DISCONTINUITY,
} listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION));
} }
} }
...@@ -99,7 +112,9 @@ import java.util.ArrayList; ...@@ -99,7 +112,9 @@ import java.util.ArrayList;
this.state = state; this.state = state;
this.playWhenReady = playWhenReady; this.playWhenReady = playWhenReady;
if (playbackStateChanged || playWhenReadyChanged) { if (playbackStateChanged || playWhenReadyChanged) {
for (Player.EventListener listener : listeners) { listeners.sendEvent(
Player.EVENT_PLAYBACK_STATE_CHANGED,
listener -> {
listener.onPlayerStateChanged(playWhenReady, state); listener.onPlayerStateChanged(playWhenReady, state);
if (playbackStateChanged) { if (playbackStateChanged) {
listener.onPlaybackStateChanged(state); listener.onPlaybackStateChanged(state);
...@@ -108,7 +123,7 @@ import java.util.ArrayList; ...@@ -108,7 +123,7 @@ import java.util.ArrayList;
listener.onPlayWhenReadyChanged( listener.onPlayWhenReadyChanged(
playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
} }
} });
} }
} }
...@@ -146,6 +161,17 @@ import java.util.ArrayList; ...@@ -146,6 +161,17 @@ import java.util.ArrayList;
} }
@Override @Override
@RepeatMode
public int getRepeatMode() {
return REPEAT_MODE_OFF;
}
@Override
public boolean getShuffleModeEnabled() {
return false;
}
@Override
public int getRendererCount() { public int getRendererCount() {
return 0; return 0;
} }
...@@ -162,7 +188,7 @@ import java.util.ArrayList; ...@@ -162,7 +188,7 @@ import java.util.ArrayList;
@Override @Override
public int getCurrentPeriodIndex() { public int getCurrentPeriodIndex() {
return 0; return periodIndex;
} }
@Override @Override
...@@ -186,7 +212,7 @@ import java.util.ArrayList; ...@@ -186,7 +212,7 @@ import java.util.ArrayList;
@Override @Override
public long getCurrentPosition() { public long getCurrentPosition() {
return position; return positionMs;
} }
@Override @Override
...@@ -206,6 +232,6 @@ import java.util.ArrayList; ...@@ -206,6 +232,6 @@ import java.util.ArrayList;
@Override @Override
public long getContentPosition() { public long getContentPosition() {
return contentPosition; return contentPositionMs;
} }
} }
...@@ -22,15 +22,6 @@ ...@@ -22,15 +22,6 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-sdk/> <uses-sdk/>
<application
android:allowBackup="false"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
<activity android:name="com.google.android.exoplayer2.ext.media2.MediaStubActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="false"
android:label="MediaStubActivity"/>
</application>
<instrumentation <instrumentation
android:targetPackage="com.google.android.exoplayer2.ext.media2.test" android:targetPackage="com.google.android.exoplayer2.ext.media2.test"
android:name="androidx.test.runner.AndroidJUnitRunner"/> android:name="androidx.test.runner.AndroidJUnitRunner"/>
......
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.media2;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import com.google.android.exoplayer2.ext.media2.test.R;
import com.google.android.exoplayer2.util.Util;
/** Stub activity to play media contents on. */
public final class MediaStubActivity extends Activity {
private static final String TAG = "MediaStubActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mediaplayer);
// disable enter animation.
overridePendingTransition(0, 0);
if (Util.SDK_INT >= 27) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setTurnScreenOn(true);
setShowWhenLocked(true);
KeyguardManager keyguardManager =
(KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
keyguardManager.requestDismissKeyguard(this, null);
} else {
getWindow()
.addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
}
}
@Override
public void finish() {
super.finish();
// disable exit animation.
overridePendingTransition(0, 0);
}
@Override
protected void onResume() {
Log.i(TAG, "onResume");
super.onResume();
}
@Override
protected void onPause() {
Log.i(TAG, "onPause");
super.onPause();
}
public SurfaceHolder getSurfaceHolder() {
SurfaceView surface = findViewById(R.id.surface);
return surface.getHolder();
}
}
...@@ -53,14 +53,6 @@ import org.junit.rules.ExternalResource; ...@@ -53,14 +53,6 @@ import org.junit.rules.ExternalResource;
@Override @Override
protected void before() { protected void before() {
// Workaround limitation in androidx.media2.session:1.0.3 which session can only be instantiated
// on thread with prepared Looper.
// TODO: Remove when androidx.media2.session:1.1.0 is released without the limitation
// [Internal: b/146536708]
if (Looper.myLooper() == null) {
Looper.prepare();
}
context = ApplicationProvider.getApplicationContext(); context = ApplicationProvider.getApplicationContext();
executor = Executors.newFixedThreadPool(1); executor = Executors.newFixedThreadPool(1);
......
...@@ -24,7 +24,6 @@ import android.content.Context; ...@@ -24,7 +24,6 @@ import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.media2.common.MediaItem; import androidx.media2.common.MediaItem;
...@@ -41,9 +40,6 @@ import androidx.media2.session.SessionResult; ...@@ -41,9 +40,6 @@ import androidx.media2.session.SessionResult;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest; import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.ext.media2.test.R; import com.google.android.exoplayer2.ext.media2.test.R;
import com.google.android.exoplayer2.upstream.RawResourceDataSource; import com.google.android.exoplayer2.upstream.RawResourceDataSource;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -60,9 +56,6 @@ import org.junit.runner.RunWith; ...@@ -60,9 +56,6 @@ import org.junit.runner.RunWith;
/** Tests {@link SessionCallbackBuilder}. */ /** Tests {@link SessionCallbackBuilder}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class SessionCallbackBuilderTest { public class SessionCallbackBuilderTest {
@Rule
public final ActivityTestRule<MediaStubActivity> activityRule =
new ActivityTestRule<>(MediaStubActivity.class);
@Rule public final PlayerTestRule playerTestRule = new PlayerTestRule(); @Rule public final PlayerTestRule playerTestRule = new PlayerTestRule();
...@@ -80,16 +73,6 @@ public class SessionCallbackBuilderTest { ...@@ -80,16 +73,6 @@ public class SessionCallbackBuilderTest {
context = ApplicationProvider.getApplicationContext(); context = ApplicationProvider.getApplicationContext();
executor = playerTestRule.getExecutor(); executor = playerTestRule.getExecutor();
sessionPlayerConnector = playerTestRule.getSessionPlayerConnector(); sessionPlayerConnector = playerTestRule.getSessionPlayerConnector();
// Sets the surface to the player for manual check.
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
SimpleExoPlayer exoPlayer = playerTestRule.getSimpleExoPlayer();
exoPlayer
.getVideoComponent()
.setVideoSurfaceHolder(activityRule.getActivity().getSurfaceHolder());
});
} }
@Test @Test
...@@ -469,8 +452,7 @@ public class SessionCallbackBuilderTest { ...@@ -469,8 +452,7 @@ public class SessionCallbackBuilderTest {
executor, executor,
new SessionPlayer.PlayerCallback() { new SessionPlayer.PlayerCallback() {
@Override @Override
public void onCurrentMediaItemChanged( public void onCurrentMediaItemChanged(SessionPlayer player, MediaItem item) {
@NonNull SessionPlayer player, @NonNull MediaItem item) {
MediaMetadata metadata = item.getMetadata(); MediaMetadata metadata = item.getMetadata();
assertThat(metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID)) assertThat(metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID))
.isEqualTo(testMediaUri.toString()); .isEqualTo(testMediaUri.toString());
...@@ -613,15 +595,14 @@ public class SessionCallbackBuilderTest { ...@@ -613,15 +595,14 @@ public class SessionCallbackBuilderTest {
new MediaController.ControllerCallback() { new MediaController.ControllerCallback() {
@Override @Override
public void onAllowedCommandsChanged( public void onAllowedCommandsChanged(
@NonNull MediaController controller, @NonNull SessionCommandGroup commands) { MediaController controller, SessionCommandGroup commands) {
if (onAllowedCommandsChangedListener != null) { if (onAllowedCommandsChangedListener != null) {
onAllowedCommandsChangedListener.onAllowedCommandsChanged(controller, commands); onAllowedCommandsChangedListener.onAllowedCommandsChanged(controller, commands);
} }
} }
@Override @Override
public void onConnected( public void onConnected(MediaController controller, SessionCommandGroup allowedCommands) {
@NonNull MediaController controller, @NonNull SessionCommandGroup allowedCommands) {
if (onConnectedListener != null) { if (onConnectedListener != null) {
onConnectedListener.onConnected(controller, allowedCommands); onConnectedListener.onConnected(controller, allowedCommands);
} }
......
...@@ -97,6 +97,9 @@ import java.util.concurrent.Callable; ...@@ -97,6 +97,9 @@ import java.util.concurrent.Callable;
/** Command code for {@link SessionPlayer#removePlaylistItem(int)} */ /** Command code for {@link SessionPlayer#removePlaylistItem(int)} */
public static final int COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM = 16; public static final int COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM = 16;
/** Command code for {@link SessionPlayer#movePlaylistItem(int, int)} */
public static final int COMMAND_CODE_PLAYER_MOVE_PLAYLIST_ITEM = 17;
/** List of session commands whose result would be set after the command is finished. */ /** List of session commands whose result would be set after the command is finished. */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
...@@ -119,6 +122,7 @@ import java.util.concurrent.Callable; ...@@ -119,6 +122,7 @@ import java.util.concurrent.Callable;
COMMAND_CODE_PLAYER_SET_PLAYLIST, COMMAND_CODE_PLAYER_SET_PLAYLIST,
COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM, COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM,
COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM, COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM,
COMMAND_CODE_PLAYER_MOVE_PLAYLIST_ITEM,
}) })
public @interface CommandCode {} public @interface CommandCode {}
...@@ -381,9 +385,25 @@ import java.util.concurrent.Callable; ...@@ -381,9 +385,25 @@ import java.util.concurrent.Callable;
case COMMAND_CODE_PLAYER_PAUSE: case COMMAND_CODE_PLAYER_PAUSE:
case COMMAND_CODE_PLAYER_PREPARE: case COMMAND_CODE_PLAYER_PREPARE:
return true; return true;
} case COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM:
case COMMAND_CODE_PLAYER_MOVE_PLAYLIST_ITEM:
case COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM:
case COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM:
case COMMAND_CODE_PLAYER_SEEK_TO:
case COMMAND_CODE_PLAYER_SET_AUDIO_ATTRIBUTES:
case COMMAND_CODE_PLAYER_SET_MEDIA_ITEM:
case COMMAND_CODE_PLAYER_SET_PLAYLIST:
case COMMAND_CODE_PLAYER_SET_REPEAT_MODE:
case COMMAND_CODE_PLAYER_SET_SHUFFLE_MODE:
case COMMAND_CODE_PLAYER_SET_SPEED:
case COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM:
case COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM:
case COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM:
case COMMAND_CODE_PLAYER_UPDATE_LIST_METADATA:
default:
return false; return false;
} }
}
private static final class AsyncPlayerCommandResult { private static final class AsyncPlayerCommandResult {
@AsyncCommandCode public final int commandCode; @AsyncCommandCode public final int commandCode;
......
...@@ -223,6 +223,19 @@ import java.util.List; ...@@ -223,6 +223,19 @@ import java.util.List;
return true; return true;
} }
public boolean movePlaylistItem(
@IntRange(from = 0) int fromIndex, @IntRange(from = 0) int toIndex) {
int itemCount = player.getMediaItemCount();
if (!(fromIndex < itemCount && toIndex < itemCount)) {
return false;
}
if (fromIndex == toIndex) {
return true;
}
player.moveMediaItem(fromIndex, toIndex);
return true;
}
public boolean skipToPreviousPlaylistItem() { public boolean skipToPreviousPlaylistItem() {
Timeline timeline = player.getCurrentTimeline(); Timeline timeline = player.getCurrentTimeline();
Assertions.checkState(!timeline.isEmpty()); Assertions.checkState(!timeline.isEmpty());
......
...@@ -227,7 +227,8 @@ import java.util.concurrent.TimeoutException; ...@@ -227,7 +227,8 @@ import java.util.concurrent.TimeoutException;
} }
build.addAllPredefinedCommands(SessionCommand.COMMAND_VERSION_1); build.addAllPredefinedCommands(SessionCommand.COMMAND_VERSION_1);
// TODO: Use removeCommand(int) when it's added [Internal: b/142848015]. build.addCommand(new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_MOVE_PLAYLIST_ITEM));
// TODO(internal b/142848015): Use removeCommand(int) when it's added.
if (mediaItemProvider == null) { if (mediaItemProvider == null) {
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM)); build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM));
build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SET_PLAYLIST)); build.removeCommand(new SessionCommand(SessionCommand.COMMAND_CODE_PLAYER_SET_PLAYLIST));
...@@ -348,7 +349,10 @@ import java.util.concurrent.TimeoutException; ...@@ -348,7 +349,10 @@ import java.util.concurrent.TimeoutException;
updateAllowedCommands(); updateAllowedCommands();
} }
// TODO(internal b/160846312): Remove warning suppression and mark item @Nullable once we depend
// on media2 1.2.0.
@Override @Override
@SuppressWarnings("nullness:override.param.invalid")
public void onCurrentMediaItemChanged(SessionPlayer player, MediaItem item) { public void onCurrentMediaItemChanged(SessionPlayer player, MediaItem item) {
currentMediaItemBuffered = isBufferedState(player.getBufferingState()); currentMediaItemBuffered = isBufferedState(player.getBufferingState());
updateAllowedCommands(); updateAllowedCommands();
......
...@@ -325,6 +325,17 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -325,6 +325,17 @@ public final class SessionPlayerConnector extends SessionPlayer {
} }
@Override @Override
public ListenableFuture<PlayerResult> movePlaylistItem(int fromIndex, int toIndex) {
Assertions.checkArgument(fromIndex >= 0);
Assertions.checkArgument(toIndex >= 0);
ListenableFuture<PlayerResult> result =
playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_MOVE_PLAYLIST_ITEM,
/* command= */ () -> player.movePlaylistItem(fromIndex, toIndex));
return result;
}
@Override
public ListenableFuture<PlayerResult> skipToPreviousPlaylistItem() { public ListenableFuture<PlayerResult> skipToPreviousPlaylistItem() {
ListenableFuture<PlayerResult> result = ListenableFuture<PlayerResult> result =
playerCommandQueue.addCommand( playerCommandQueue.addCommand(
......
...@@ -15,6 +15,13 @@ ...@@ -15,6 +15,13 @@
*/ */
package com.google.android.exoplayer2.ext.mediasession; package com.google.android.exoplayer2.ext.mediasession;
import static com.google.android.exoplayer2.Player.EVENT_IS_PLAYING_CHANGED;
import static com.google.android.exoplayer2.Player.EVENT_PLAYBACK_PARAMETERS_CHANGED;
import static com.google.android.exoplayer2.Player.EVENT_PLAYBACK_STATE_CHANGED;
import static com.google.android.exoplayer2.Player.EVENT_PLAY_WHEN_READY_CHANGED;
import static com.google.android.exoplayer2.Player.EVENT_REPEAT_MODE_CHANGED;
import static com.google.android.exoplayer2.Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
...@@ -38,7 +45,6 @@ import com.google.android.exoplayer2.ControlDispatcher; ...@@ -38,7 +45,6 @@ import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -95,6 +101,10 @@ public final class MediaSessionConnector { ...@@ -95,6 +101,10 @@ public final class MediaSessionConnector {
ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession"); ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession");
} }
/** Indicates this session supports the set playback speed command. */
// TODO(b/174297519) Replace with PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED when released.
public static final long ACTION_SET_PLAYBACK_SPEED = 1 << 22;
/** Playback actions supported by the connector. */ /** Playback actions supported by the connector. */
@LongDef( @LongDef(
flag = true, flag = true,
...@@ -107,7 +117,8 @@ public final class MediaSessionConnector { ...@@ -107,7 +117,8 @@ public final class MediaSessionConnector {
PlaybackStateCompat.ACTION_REWIND, PlaybackStateCompat.ACTION_REWIND,
PlaybackStateCompat.ACTION_STOP, PlaybackStateCompat.ACTION_STOP,
PlaybackStateCompat.ACTION_SET_REPEAT_MODE, PlaybackStateCompat.ACTION_SET_REPEAT_MODE,
PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE,
ACTION_SET_PLAYBACK_SPEED
}) })
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface PlaybackActions {} public @interface PlaybackActions {}
...@@ -122,10 +133,13 @@ public final class MediaSessionConnector { ...@@ -122,10 +133,13 @@ public final class MediaSessionConnector {
| PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_REWIND
| PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_STOP
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
| PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE; | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
| ACTION_SET_PLAYBACK_SPEED;
/** The default playback actions. */ /** The default playback actions. */
@PlaybackActions public static final long DEFAULT_PLAYBACK_ACTIONS = ALL_PLAYBACK_ACTIONS; @PlaybackActions
public static final long DEFAULT_PLAYBACK_ACTIONS =
ALL_PLAYBACK_ACTIONS - ACTION_SET_PLAYBACK_SPEED;
/** /**
* The name of the {@link PlaybackStateCompat} float extra with the value of {@code * The name of the {@link PlaybackStateCompat} float extra with the value of {@code
...@@ -139,7 +153,8 @@ public final class MediaSessionConnector { ...@@ -139,7 +153,8 @@ public final class MediaSessionConnector {
| PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_STOP
| PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE; | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
| ACTION_SET_PLAYBACK_SPEED;
private static final int BASE_MEDIA_SESSION_FLAGS = private static final int BASE_MEDIA_SESSION_FLAGS =
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS; | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS;
...@@ -1074,69 +1089,59 @@ public final class MediaSessionConnector { ...@@ -1074,69 +1089,59 @@ public final class MediaSessionConnector {
// Player.EventListener implementation. // Player.EventListener implementation.
@Override @Override
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { public void onEvents(Player player, Player.Events events) {
Player player = Assertions.checkNotNull(MediaSessionConnector.this.player); boolean invalidatePlaybackState = false;
boolean invalidateMetadata = false;
if (events.contains(Player.EVENT_POSITION_DISCONTINUITY)) {
if (currentWindowIndex != player.getCurrentWindowIndex()) {
if (queueNavigator != null) {
queueNavigator.onCurrentWindowIndexChanged(player);
}
invalidateMetadata = true;
}
invalidatePlaybackState = true;
}
if (events.contains(Player.EVENT_TIMELINE_CHANGED)) {
int windowCount = player.getCurrentTimeline().getWindowCount(); int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex(); int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) { if (queueNavigator != null) {
queueNavigator.onTimelineChanged(player); queueNavigator.onTimelineChanged(player);
invalidateMediaSessionPlaybackState(); invalidatePlaybackState = true;
} else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) { } else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) {
// active queue item and queue navigation actions may need to be updated // active queue item and queue navigation actions may need to be updated
invalidateMediaSessionPlaybackState(); invalidatePlaybackState = true;
} }
currentWindowCount = windowCount; currentWindowCount = windowCount;
currentWindowIndex = windowIndex; invalidateMetadata = true;
invalidateMediaSessionMetadata();
}
@Override
public void onPlaybackStateChanged(@Player.State int playbackState) {
invalidateMediaSessionPlaybackState();
} }
@Override // Update currentWindowIndex after comparisons above.
public void onPlayWhenReadyChanged( currentWindowIndex = player.getCurrentWindowIndex();
boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
invalidateMediaSessionPlaybackState();
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
invalidateMediaSessionPlaybackState();
}
@Override if (events.containsAny(
public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { EVENT_PLAYBACK_STATE_CHANGED,
invalidateMediaSessionPlaybackState(); EVENT_PLAY_WHEN_READY_CHANGED,
EVENT_IS_PLAYING_CHANGED,
EVENT_REPEAT_MODE_CHANGED,
EVENT_PLAYBACK_PARAMETERS_CHANGED)) {
invalidatePlaybackState = true;
} }
@Override // The queue needs to be updated by the queue navigator first. The queue navigator also
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { // delivers the active queue item that is used to update the playback state.
invalidateMediaSessionPlaybackState(); if (events.containsAny(EVENT_SHUFFLE_MODE_ENABLED_CHANGED)) {
invalidateMediaSessionQueue(); invalidateMediaSessionQueue();
invalidatePlaybackState = true;
} }
// Invalidate the playback state before invalidating metadata because the active queue item of
@Override // the session playback state needs to be updated before the MediaMetadataProvider uses it.
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { if (invalidatePlaybackState) {
Player player = Assertions.checkNotNull(MediaSessionConnector.this.player);
if (currentWindowIndex != player.getCurrentWindowIndex()) {
if (queueNavigator != null) {
queueNavigator.onCurrentWindowIndexChanged(player);
}
currentWindowIndex = player.getCurrentWindowIndex();
// Update playback state after queueNavigator.onCurrentWindowIndexChanged has been called
// and before updating metadata.
invalidateMediaSessionPlaybackState(); invalidateMediaSessionPlaybackState();
invalidateMediaSessionMetadata();
return;
} }
invalidateMediaSessionPlaybackState(); if (invalidateMetadata) {
invalidateMediaSessionMetadata();
} }
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
invalidateMediaSessionPlaybackState();
} }
// MediaSessionCompat.Callback implementation. // MediaSessionCompat.Callback implementation.
...@@ -1235,6 +1240,14 @@ public final class MediaSessionConnector { ...@@ -1235,6 +1240,14 @@ public final class MediaSessionConnector {
} }
@Override @Override
public void onSetPlaybackSpeed(float speed) {
if (canDispatchPlaybackAction(ACTION_SET_PLAYBACK_SPEED) && speed > 0) {
controlDispatcher.dispatchSetPlaybackParameters(
player, player.getPlaybackParameters().withSpeed(speed));
}
}
@Override
public void onSkipToNext() { public void onSkipToNext() {
if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) { if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {
queueNavigator.onSkipToNext(player, controlDispatcher); queueNavigator.onSkipToNext(player, controlDispatcher);
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-common')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
......
...@@ -15,19 +15,16 @@ ...@@ -15,19 +15,16 @@
*/ */
package com.google.android.exoplayer2.ext.okhttp; package com.google.android.exoplayer2.ext.okhttp;
import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import okhttp3.CacheControl; import okhttp3.CacheControl;
import okhttp3.Call; import okhttp3.Call;
/** /** @deprecated Use {@link OkHttpDataSource.Factory} instead. */
* A {@link Factory} that produces {@link OkHttpDataSource}. @Deprecated
*/
public final class OkHttpDataSourceFactory extends BaseFactory { public final class OkHttpDataSourceFactory extends BaseFactory {
private final Call.Factory callFactory; private final Call.Factory callFactory;
...@@ -42,7 +39,7 @@ public final class OkHttpDataSourceFactory extends BaseFactory { ...@@ -42,7 +39,7 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
* by the sources created by the factory. * by the sources created by the factory.
*/ */
public OkHttpDataSourceFactory(Call.Factory callFactory) { public OkHttpDataSourceFactory(Call.Factory callFactory) {
this(callFactory, DEFAULT_USER_AGENT, /* listener= */ null, /* cacheControl= */ null); this(callFactory, /* userAgent= */ null, /* listener= */ null, /* cacheControl= */ null);
} }
/** /**
...@@ -102,6 +99,8 @@ public final class OkHttpDataSourceFactory extends BaseFactory { ...@@ -102,6 +99,8 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
this.cacheControl = cacheControl; this.cacheControl = cacheControl;
} }
// Calls deprecated constructor.
@SuppressWarnings("deprecation")
@Override @Override
protected OkHttpDataSource createDataSourceInternal( protected OkHttpDataSource createDataSourceInternal(
HttpDataSource.RequestProperties defaultRequestProperties) { HttpDataSource.RequestProperties defaultRequestProperties) {
......
/*
* 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.okhttp;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.DataSourceContractTest;
import com.google.android.exoplayer2.testutil.HttpDataSourceTestEnv;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.common.collect.ImmutableList;
import okhttp3.OkHttpClient;
import org.junit.Rule;
import org.junit.runner.RunWith;
/** {@link DataSource} contract tests for {@link OkHttpDataSource}. */
@RunWith(AndroidJUnit4.class)
public class OkHttpDataSourceContractTest extends DataSourceContractTest {
@Rule public HttpDataSourceTestEnv httpDataSourceTestEnv = new HttpDataSourceTestEnv();
@Override
protected DataSource createDataSource() {
return new OkHttpDataSource.Factory(new OkHttpClient()).createDataSource();
}
@Override
protected ImmutableList<TestResource> getTestResources() {
return httpDataSourceTestEnv.getServedResources();
}
@Override
protected Uri getNotFoundUri() {
return Uri.parse(httpDataSourceTestEnv.getNonexistentUrl());
}
}
...@@ -59,15 +59,16 @@ public class OkHttpDataSourceTest { ...@@ -59,15 +59,16 @@ public class OkHttpDataSourceTest {
MockWebServer mockWebServer = new MockWebServer(); MockWebServer mockWebServer = new MockWebServer();
mockWebServer.enqueue(new MockResponse()); mockWebServer.enqueue(new MockResponse());
String propertyFromConstructor = "fromConstructor"; String propertyFromFactory = "fromFactory";
HttpDataSource.RequestProperties constructorProperties = new HttpDataSource.RequestProperties(); Map<String, String> defaultRequestProperties = new HashMap<>();
constructorProperties.set("0", propertyFromConstructor); defaultRequestProperties.put("0", propertyFromFactory);
constructorProperties.set("1", propertyFromConstructor); defaultRequestProperties.put("1", propertyFromFactory);
constructorProperties.set("2", propertyFromConstructor); defaultRequestProperties.put("2", propertyFromFactory);
constructorProperties.set("4", propertyFromConstructor); defaultRequestProperties.put("4", propertyFromFactory);
OkHttpDataSource dataSource = HttpDataSource dataSource =
new OkHttpDataSource( new OkHttpDataSource.Factory(new OkHttpClient())
new OkHttpClient(), "testAgent", /* cacheControl= */ null, constructorProperties); .setDefaultRequestProperties(defaultRequestProperties)
.createDataSource();
String propertyFromSetter = "fromSetter"; String propertyFromSetter = "fromSetter";
dataSource.setRequestProperty("1", propertyFromSetter); dataSource.setRequestProperty("1", propertyFromSetter);
...@@ -91,7 +92,7 @@ public class OkHttpDataSourceTest { ...@@ -91,7 +92,7 @@ public class OkHttpDataSourceTest {
dataSource.open(dataSpec); dataSource.open(dataSpec);
Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders(); Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders();
assertThat(headers.get("0")).isEqualTo(propertyFromConstructor); assertThat(headers.get("0")).isEqualTo(propertyFromFactory);
assertThat(headers.get("1")).isEqualTo(propertyFromSetter); assertThat(headers.get("1")).isEqualTo(propertyFromSetter);
assertThat(headers.get("2")).isEqualTo(propertyFromDataSpec); assertThat(headers.get("2")).isEqualTo(propertyFromDataSpec);
assertThat(headers.get("3")).isEqualTo(propertyFromDataSpec); assertThat(headers.get("3")).isEqualTo(propertyFromDataSpec);
...@@ -101,16 +102,13 @@ public class OkHttpDataSourceTest { ...@@ -101,16 +102,13 @@ public class OkHttpDataSourceTest {
} }
@Test @Test
public void open_invalidResponseCode() throws Exception { public void open_invalidResponseCode() {
MockWebServer mockWebServer = new MockWebServer(); MockWebServer mockWebServer = new MockWebServer();
mockWebServer.enqueue(new MockResponse().setResponseCode(404).setBody("failure msg")); mockWebServer.enqueue(new MockResponse().setResponseCode(404).setBody("failure msg"));
OkHttpDataSource okHttpDataSource = HttpDataSource okHttpDataSource =
new OkHttpDataSource( new OkHttpDataSource.Factory(new OkHttpClient()).createDataSource();
new OkHttpClient(),
"testAgent",
/* cacheControl= */ null,
/* defaultRequestProperties= */ null);
DataSpec dataSpec = DataSpec dataSpec =
new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build(); new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build();
...@@ -122,4 +120,22 @@ public class OkHttpDataSourceTest { ...@@ -122,4 +120,22 @@ public class OkHttpDataSourceTest {
assertThat(exception.responseCode).isEqualTo(404); assertThat(exception.responseCode).isEqualTo(404);
assertThat(exception.responseBody).isEqualTo("failure msg".getBytes(Charsets.UTF_8)); assertThat(exception.responseBody).isEqualTo("failure msg".getBytes(Charsets.UTF_8));
} }
@Test
public void factory_setRequestPropertyAfterCreation_setsCorrectHeaders() throws Exception {
MockWebServer mockWebServer = new MockWebServer();
mockWebServer.enqueue(new MockResponse());
DataSpec dataSpec =
new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build();
OkHttpDataSource.Factory factory = new OkHttpDataSource.Factory(new OkHttpClient());
OkHttpDataSource dataSource = factory.createDataSource();
Map<String, String> defaultRequestProperties = new HashMap<>();
defaultRequestProperties.put("0", "afterCreation");
factory.setDefaultRequestProperties(defaultRequestProperties);
dataSource.open(dataSpec);
Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders();
assertThat(headers.get("0")).isEqualTo("afterCreation");
}
} }
...@@ -39,7 +39,7 @@ NDK_PATH="<path to Android NDK>" ...@@ -39,7 +39,7 @@ NDK_PATH="<path to Android NDK>"
``` ```
cd "${OPUS_EXT_PATH}/jni" && \ cd "${OPUS_EXT_PATH}/jni" && \
git clone https://git.xiph.org/opus.git libopus git clone https://gitlab.xiph.org/xiph/opus.git libopus
``` ```
* Run the script to convert arm assembly to NDK compatible format: * Run the script to convert arm assembly to NDK compatible format:
......
...@@ -79,21 +79,21 @@ public class LibopusAudioRenderer extends DecoderAudioRenderer<OpusDecoder> { ...@@ -79,21 +79,21 @@ public class LibopusAudioRenderer extends DecoderAudioRenderer<OpusDecoder> {
} }
@Override @Override
@FormatSupport @C.FormatSupport
protected int supportsFormatInternal(Format format) { protected int supportsFormatInternal(Format format) {
boolean drmIsSupported = boolean drmIsSupported =
format.exoMediaCryptoType == null format.exoMediaCryptoType == null
|| OpusLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType); || OpusLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType);
if (!OpusLibrary.isAvailable() if (!OpusLibrary.isAvailable()
|| !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) { || !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return C.FORMAT_UNSUPPORTED_TYPE;
} else if (!sinkSupportsFormat( } else if (!sinkSupportsFormat(
Util.getPcmFormat(C.ENCODING_PCM_16BIT, format.channelCount, format.sampleRate))) { Util.getPcmFormat(C.ENCODING_PCM_16BIT, format.channelCount, format.sampleRate))) {
return FORMAT_UNSUPPORTED_SUBTYPE; return C.FORMAT_UNSUPPORTED_SUBTYPE;
} else if (!drmIsSupported) { } else if (!drmIsSupported) {
return FORMAT_UNSUPPORTED_DRM; return C.FORMAT_UNSUPPORTED_DRM;
} else { } else {
return FORMAT_HANDLED; return C.FORMAT_HANDLED;
} }
} }
......
...@@ -15,7 +15,10 @@ ...@@ -15,7 +15,10 @@
*/ */
package com.google.android.exoplayer2.ext.opus; package com.google.android.exoplayer2.ext.opus;
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.audio.OpusUtil; import com.google.android.exoplayer2.audio.OpusUtil;
import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.CryptoInfo;
...@@ -30,7 +33,8 @@ import java.nio.ByteBuffer; ...@@ -30,7 +33,8 @@ import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
/** Opus decoder. */ /** Opus decoder. */
/* package */ final class OpusDecoder @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
public final class OpusDecoder
extends SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, OpusDecoderException> { extends SimpleDecoder<DecoderInputBuffer, SimpleOutputBuffer, OpusDecoderException> {
private static final int NO_ERROR = 0; private static final int NO_ERROR = 0;
......
...@@ -34,7 +34,7 @@ while read file; do ...@@ -34,7 +34,7 @@ while read file; do
perl -pi -e "s/-gnu\.S/_gnu\.s/g" "${gnu_file}" perl -pi -e "s/-gnu\.S/_gnu\.s/g" "${gnu_file}"
rm -f "${file}" rm -f "${file}"
fi fi
done < <(find . -iname '*.s') done < <(find -L . -iname '*.s')
# Generate armopts.s from armopts.s.in # Generate armopts.s from armopts.s.in
sed \ sed \
......
...@@ -14,10 +14,11 @@ ...@@ -14,10 +14,11 @@
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-common')
implementation 'net.butterflytv.utils:rtmp-client:3.1.0' implementation 'net.butterflytv.utils:rtmp-client:3.1.0'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
testImplementation project(modulePrefix + 'library-core')
testImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
......
...@@ -35,25 +35,23 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ...@@ -35,25 +35,23 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
NDK_PATH="<path to Android NDK>" NDK_PATH="<path to Android NDK>"
``` ```
* Fetch libvpx: * Fetch an appropriate branch of libvpx. We cannot guarantee compatibility
```
cd "${VP9_EXT_PATH}/jni" && \
git clone https://chromium.googlesource.com/webm/libvpx libvpx
```
* Checkout an appropriate branch of libvpx. We cannot guarantee compatibility
with all versions of libvpx. We currently recommend version 1.8.0: with all versions of libvpx. We currently recommend version 1.8.0:
``` ```
cd "${VP9_EXT_PATH}/jni/libvpx" && \ cd "<preferred location for libvpx>" && \
git checkout tags/v1.8.0 -b v1.8.0 git clone https://chromium.googlesource.com/webm/libvpx && \
cd libvpx && \
git checkout tags/v1.8.0 -b v1.8.0 && \
LIBVPX_PATH="$(pwd)"
``` ```
* Run a script that generates necessary configuration files for libvpx: * Add a link to the libvpx source code in the vp9 extension `jni` directory and
run a script that generates necessary configuration files for libvpx:
``` ```
cd ${VP9_EXT_PATH}/jni && \ cd ${VP9_EXT_PATH}/jni && \
ln -s "$LIBVPX_PATH" libvpx && \
./generate_libvpx_android_configs.sh ./generate_libvpx_android_configs.sh
``` ```
...@@ -80,8 +78,8 @@ should be possible to follow the Linux instructions in [Windows PowerShell][]. ...@@ -80,8 +78,8 @@ should be possible to follow the Linux instructions in [Windows PowerShell][].
* Android config scripts should be re-generated by running * Android config scripts should be re-generated by running
`generate_libvpx_android_configs.sh` `generate_libvpx_android_configs.sh`
* Clean and re-build the project. * Clean and re-build the project.
* If you want to use your own version of libvpx, place it in * If you want to use your own version of libvpx, point to it with the
`${VP9_EXT_PATH}/jni/libvpx`. Please note that `${VP9_EXT_PATH}/jni/libvpx` symlink. Please note that
`generate_libvpx_android_configs.sh` and the makefiles may need to be modified `generate_libvpx_android_configs.sh` and the makefiles may need to be modified
to work with arbitrary versions of libvpx. to work with arbitrary versions of libvpx.
......
...@@ -6,6 +6,12 @@ ...@@ -6,6 +6,12 @@
} }
# Some members of this class are being accessed from native methods. Keep them unobfuscated. # Some members of this class are being accessed from native methods. Keep them unobfuscated.
-keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer {
*;
}
# The deprecated VpxOutputBuffer might be used by old binary versions. Remove
# once VpxOutputBuffer is removed.
-keep class com.google.android.exoplayer2.ext.vp9.VpxOutputBuffer { -keep class com.google.android.exoplayer2.ext.vp9.VpxOutputBuffer {
*; *;
} }
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.ext.vp9; package com.google.android.exoplayer2.ext.vp9;
import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION;
import static java.lang.Runtime.getRuntime; import static java.lang.Runtime.getRuntime;
import android.os.Handler; import android.os.Handler;
...@@ -23,6 +24,7 @@ import androidx.annotation.Nullable; ...@@ -23,6 +24,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation;
import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.TraceUtil;
...@@ -125,15 +127,16 @@ public class LibvpxVideoRenderer extends DecoderVideoRenderer { ...@@ -125,15 +127,16 @@ public class LibvpxVideoRenderer extends DecoderVideoRenderer {
@Capabilities @Capabilities
public final int supportsFormat(Format format) { public final int supportsFormat(Format format) {
if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) { if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
} }
boolean drmIsSupported = boolean drmIsSupported =
format.exoMediaCryptoType == null format.exoMediaCryptoType == null
|| VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType); || VpxLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType);
if (!drmIsSupported) { if (!drmIsSupported) {
return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_DRM);
} }
return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED); return RendererCapabilities.create(
C.FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED);
} }
@Override @Override
...@@ -169,7 +172,13 @@ public class LibvpxVideoRenderer extends DecoderVideoRenderer { ...@@ -169,7 +172,13 @@ public class LibvpxVideoRenderer extends DecoderVideoRenderer {
} }
@Override @Override
protected boolean canKeepCodec(Format oldFormat, Format newFormat) { protected DecoderReuseEvaluation canReuseDecoder(
return true; String decoderName, Format oldFormat, Format newFormat) {
return new DecoderReuseEvaluation(
decoderName,
oldFormat,
newFormat,
REUSE_RESULT_YES_WITHOUT_RECONFIGURATION,
/* discardReasons= */ 0);
} }
} }
...@@ -15,8 +15,11 @@ ...@@ -15,8 +15,11 @@
*/ */
package com.google.android.exoplayer2.ext.vp9; package com.google.android.exoplayer2.ext.vp9;
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo; import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
...@@ -30,7 +33,8 @@ import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer; ...@@ -30,7 +33,8 @@ import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** Vpx decoder. */ /** Vpx decoder. */
/* package */ final class VpxDecoder @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
public final class VpxDecoder
extends SimpleDecoder<VideoDecoderInputBuffer, VideoDecoderOutputBuffer, VpxDecoderException> { extends SimpleDecoder<VideoDecoderInputBuffer, VideoDecoderOutputBuffer, VpxDecoderException> {
// These constants should match the codes returned from vpxDecode and vpxSecureDecode functions in // These constants should match the codes returned from vpxDecode and vpxSecureDecode functions in
......
...@@ -18,7 +18,8 @@ package com.google.android.exoplayer2.ext.vp9; ...@@ -18,7 +18,8 @@ package com.google.android.exoplayer2.ext.vp9;
import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer; import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer;
// TODO(b/139174707): Delete this class once binaries in WVVp9OpusPlaybackTest are updated to depend // TODO(b/139174707): Delete this class once binaries in WVVp9OpusPlaybackTest are updated to depend
// on VideoDecoderOutputBuffer. Also mark VideoDecoderOutputBuffer as final. // on VideoDecoderOutputBuffer. Also mark VideoDecoderOutputBuffer as final and remove proguard
// config for VpxOutputBuffer.
/** /**
* Video output buffer, populated by {@link VpxDecoder}. * Video output buffer, populated by {@link VpxDecoder}.
* *
......
...@@ -18,6 +18,7 @@ dependencies { ...@@ -18,6 +18,7 @@ dependencies {
api project(modulePrefix + 'library-dash') api project(modulePrefix + 'library-dash')
api project(modulePrefix + 'library-hls') api project(modulePrefix + 'library-hls')
api project(modulePrefix + 'library-smoothstreaming') api project(modulePrefix + 'library-smoothstreaming')
api project(modulePrefix + 'library-transformer')
api project(modulePrefix + 'library-ui') api project(modulePrefix + 'library-ui')
} }
......
...@@ -35,7 +35,9 @@ dependencies { ...@@ -35,7 +35,9 @@ dependencies {
testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
testImplementation 'junit:junit:' + junitVersion testImplementation 'junit:junit:' + junitVersion
testImplementation 'com.google.truth:truth:' + truthVersion testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils')
} }
ext { ext {
......
...@@ -16,3 +16,7 @@ ...@@ -16,3 +16,7 @@
-dontwarn com.google.errorprone.annotations.** -dontwarn com.google.errorprone.annotations.**
-dontwarn com.google.j2objc.annotations.** -dontwarn com.google.j2objc.annotations.**
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
# Workaround for https://issuetracker.google.com/issues/112297269
# This is needed for ProGuard but not R8.
-keepclassmembernames class com.google.common.base.Function { *; }
...@@ -214,7 +214,7 @@ public abstract class BasePlayer implements Player { ...@@ -214,7 +214,7 @@ public abstract class BasePlayer implements Player {
@Override @Override
public final boolean isCurrentWindowLive() { public final boolean isCurrentWindowLive() {
Timeline timeline = getCurrentTimeline(); Timeline timeline = getCurrentTimeline();
return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isLive; return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isLive();
} }
@Override @Override
...@@ -249,58 +249,4 @@ public abstract class BasePlayer implements Player { ...@@ -249,58 +249,4 @@ public abstract class BasePlayer implements Player {
@RepeatMode int repeatMode = getRepeatMode(); @RepeatMode int repeatMode = getRepeatMode();
return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode; return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode;
} }
/** Holds a listener reference. */
protected static final class ListenerHolder {
/**
* The listener on which {link #invoke} will execute {@link ListenerInvocation listener
* invocations}.
*/
public final Player.EventListener listener;
private boolean released;
public ListenerHolder(Player.EventListener listener) {
this.listener = listener;
}
/** Prevents any further {@link ListenerInvocation} to be executed on {@link #listener}. */
public void release() {
released = true;
}
/**
* Executes the given {@link ListenerInvocation} on {@link #listener}. Does nothing if {@link
* #release} has been called on this instance.
*/
public void invoke(ListenerInvocation listenerInvocation) {
if (!released) {
listenerInvocation.invokeListener(listener);
}
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
return listener.equals(((ListenerHolder) other).listener);
}
@Override
public int hashCode() {
return listener.hashCode();
}
}
/** Parameterized invocation of a {@link Player.EventListener} method. */
protected interface ListenerInvocation {
/** Executes the invocation on the given {@link Player.EventListener}. */
void invokeListener(Player.EventListener listener);
}
} }
...@@ -24,6 +24,7 @@ import android.media.MediaFormat; ...@@ -24,6 +24,7 @@ import android.media.MediaFormat;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
...@@ -60,9 +61,10 @@ public final class C { ...@@ -60,9 +61,10 @@ public final class C {
*/ */
public static final int POSITION_UNSET = -1; public static final int POSITION_UNSET = -1;
/** /** Represents an unset or unknown rate. */
* Represents an unset or unknown length. public static final float RATE_UNSET = -Float.MAX_VALUE;
*/
/** Represents an unset or unknown length. */
public static final int LENGTH_UNSET = -1; public static final int LENGTH_UNSET = -1;
/** Represents an unset or unknown percentage. */ /** Represents an unset or unknown percentage. */
...@@ -555,24 +557,21 @@ public final class C { ...@@ -555,24 +557,21 @@ public final class C {
// ../../../../../../../../../extensions/vp9/src/main/jni/vpx_jni.cc // ../../../../../../../../../extensions/vp9/src/main/jni/vpx_jni.cc
// ) // )
/** @deprecated Use {@code Renderer.VideoScalingMode}. */ /**
@SuppressWarnings("deprecation") * Video scaling modes for {@link MediaCodec}-based renderers. One of {@link
* #VIDEO_SCALING_MODE_SCALE_TO_FIT} or {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}.
*/
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}) @IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING})
@Deprecated
public @interface VideoScalingMode {} public @interface VideoScalingMode {}
/** @deprecated Use {@code Renderer.VIDEO_SCALING_MODE_SCALE_TO_FIT}. */ /** See {@link MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT}. */
@Deprecated
public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT =
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT; MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT;
/** @deprecated Use {@code Renderer.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}. */ /** See {@link MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}. */
@Deprecated
public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING =
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING; MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING;
/** @deprecated Use {@code Renderer.VIDEO_SCALING_MODE_DEFAULT}. */ /** A default video scaling mode for {@link MediaCodec}-based renderers. */
@SuppressWarnings("deprecation")
@Deprecated
public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT; public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT;
/** /**
...@@ -681,12 +680,14 @@ public final class C { ...@@ -681,12 +680,14 @@ public final class C {
public static final int TRACK_TYPE_VIDEO = 2; public static final int TRACK_TYPE_VIDEO = 2;
/** A type constant for text tracks. */ /** A type constant for text tracks. */
public static final int TRACK_TYPE_TEXT = 3; public static final int TRACK_TYPE_TEXT = 3;
/** A type constant for image tracks. */
public static final int TRACK_TYPE_IMAGE = 4;
/** A type constant for metadata tracks. */ /** A type constant for metadata tracks. */
public static final int TRACK_TYPE_METADATA = 4; public static final int TRACK_TYPE_METADATA = 5;
/** A type constant for camera motion tracks. */ /** A type constant for camera motion tracks. */
public static final int TRACK_TYPE_CAMERA_MOTION = 5; public static final int TRACK_TYPE_CAMERA_MOTION = 6;
/** A type constant for a fake or empty track. */ /** A type constant for a fake or empty track. */
public static final int TRACK_TYPE_NONE = 6; public static final int TRACK_TYPE_NONE = 7;
/** /**
* Applications or extensions may define custom {@code TRACK_TYPE_*} constants greater than or * Applications or extensions may define custom {@code TRACK_TYPE_*} constants greater than or
* equal to this value. * equal to this value.
...@@ -1080,8 +1081,63 @@ public final class C { ...@@ -1080,8 +1081,63 @@ public final class C {
public static final int ROLE_FLAG_TRICK_PLAY = 1 << 14; public static final int ROLE_FLAG_TRICK_PLAY = 1 << 14;
/** /**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving * Level of renderer support for a format. One of {@link #FORMAT_HANDLED}, {@link
* {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values. * #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, {@link
* #FORMAT_UNSUPPORTED_SUBTYPE} or {@link #FORMAT_UNSUPPORTED_TYPE}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
FORMAT_HANDLED,
FORMAT_EXCEEDS_CAPABILITIES,
FORMAT_UNSUPPORTED_DRM,
FORMAT_UNSUPPORTED_SUBTYPE,
FORMAT_UNSUPPORTED_TYPE
})
public static @interface FormatSupport {}
// TODO(b/172315872) Renderer was a link. Link to equivalent concept or remove @code.
/** The {@code Renderer} is capable of rendering the format. */
public static final int FORMAT_HANDLED = 0b100;
/**
* The {@code Renderer} is capable of rendering formats with the same MIME type, but the
* properties of the format exceed the renderer's capabilities. There is a chance the renderer
* will be able to play the format in practice because some renderers report their capabilities
* conservatively, but the expected outcome is that playback will fail.
*
* <p>Example: The {@code Renderer} is capable of rendering H264 and the format's MIME type is
* {@code MimeTypes#VIDEO_H264}, but the format's resolution exceeds the maximum limit supported
* by the underlying H264 decoder.
*/
public static final int FORMAT_EXCEEDS_CAPABILITIES = 0b011;
/**
* The {@code Renderer} is capable of rendering formats with the same MIME type, but is not
* capable of rendering the format because the format's drm protection is not supported.
*
* <p>Example: The {@code Renderer} is capable of rendering H264 and the format's MIME type is
* {@link MimeTypes#VIDEO_H264}, but the format indicates PlayReady drm protection whereas the
* renderer only supports Widevine.
*/
public static final int FORMAT_UNSUPPORTED_DRM = 0b010;
/**
* The {@code Renderer} is a general purpose renderer for formats of the same top-level type, but
* is not capable of rendering the format or any other format with the same MIME type because the
* sub-type is not supported.
*
* <p>Example: The {@code Renderer} is a general purpose audio renderer and the format's MIME type
* matches audio/[subtype], but there does not exist a suitable decoder for [subtype].
*/
public static final int FORMAT_UNSUPPORTED_SUBTYPE = 0b001;
/**
* The {@code Renderer} is not capable of rendering the format, either because it does not support
* the format's top-level type, or because it's a specialized renderer for a different MIME type.
*
* <p>Example: The {@code Renderer} is a general purpose video renderer, but the format has an
* audio MIME type.
*/
public static final int FORMAT_UNSUPPORTED_TYPE = 0b000;
/**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving {@link
* #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values.
* *
* @param timeUs The time in microseconds. * @param timeUs The time in microseconds.
* @return The corresponding time in milliseconds. * @return The corresponding time in milliseconds.
...@@ -1114,4 +1170,26 @@ public final class C { ...@@ -1114,4 +1170,26 @@ public final class C {
return audioManager == null ? AudioManager.ERROR : audioManager.generateAudioSessionId(); return audioManager == null ? AudioManager.ERROR : audioManager.generateAudioSessionId();
} }
/**
* Returns string representation of a {@link FormatSupport} flag.
*
* @param formatSupport A {@link FormatSupport} flag.
* @return A string representation of the flag.
*/
public static String getFormatSupportString(@FormatSupport int formatSupport) {
switch (formatSupport) {
case FORMAT_HANDLED:
return "YES";
case FORMAT_EXCEEDS_CAPABILITIES:
return "NO_EXCEEDS_CAPABILITIES";
case FORMAT_UNSUPPORTED_DRM:
return "NO_UNSUPPORTED_DRM";
case FORMAT_UNSUPPORTED_SUBTYPE:
return "NO_UNSUPPORTED_TYPE";
case FORMAT_UNSUPPORTED_TYPE:
return "NO";
default:
throw new IllegalStateException();
}
}
} }
...@@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.12.3"; public static final String VERSION = "2.13.0";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.12.3"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.0";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
...@@ -44,11 +44,16 @@ public final class ExoPlayerLibraryInfo { ...@@ -44,11 +44,16 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2012003; public static final int VERSION_INT = 2013000;
/** The default user agent for requests made by the library. */ /**
* The default user agent for requests made by the library.
*
* @deprecated ExoPlayer now uses the user agent of the underlying network stack by default.
*/
@Deprecated
public static final String DEFAULT_USER_AGENT = public static final String DEFAULT_USER_AGENT =
VERSION_SLASHY + " (Linux;Android " + Build.VERSION.RELEASE + ") " + VERSION_SLASHY; VERSION_SLASHY + " (Linux; Android " + Build.VERSION.RELEASE + ") " + VERSION_SLASHY;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -70,6 +71,17 @@ public final class PlaybackParameters { ...@@ -70,6 +71,17 @@ public final class PlaybackParameters {
return timeMs * scaledUsPerMs; return timeMs * scaledUsPerMs;
} }
/**
* Returns a copy with the given speed.
*
* @param speed The new speed.
* @return The copied playback parameters.
*/
@CheckResult
public PlaybackParameters withSpeed(float speed) {
return new PlaybackParameters(speed, pitch);
}
@Override @Override
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.Pair; import android.util.Pair;
...@@ -74,10 +76,10 @@ import com.google.android.exoplayer2.util.Util; ...@@ -74,10 +76,10 @@ import com.google.android.exoplayer2.util.Util;
* <p>A timeline for a live stream consists of a period whose duration is unknown, since it's * <p>A timeline for a live stream consists of a period whose duration is unknown, since it's
* continually extending as more content is broadcast. If content only remains available for a * continually extending as more content is broadcast. If content only remains available for a
* limited period of time then the window may start at a non-zero position, defining the region of * limited period of time then the window may start at a non-zero position, defining the region of
* content that can still be played. The window will have {@link Window#isLive} set to true to * content that can still be played. The window will return true from {@link Window#isLive()} to
* indicate it's a live stream and {@link Window#isDynamic} set to true as long as we expect changes * indicate it's a live stream and {@link Window#isDynamic} will be set to true as long as we expect
* to the live window. Its default position is typically near to the live edge (indicated by the * changes to the live window. Its default position is typically near to the live edge (indicated by
* black dot in the figure above). * the black dot in the figure above).
* *
* <h3>Live stream with indefinite availability</h3> * <h3>Live stream with indefinite availability</h3>
* *
...@@ -191,12 +193,14 @@ public abstract class Timeline { ...@@ -191,12 +193,14 @@ public abstract class Timeline {
/** Whether this window may change when the timeline is updated. */ /** Whether this window may change when the timeline is updated. */
public boolean isDynamic; public boolean isDynamic;
/** @deprecated Use {@link #isLive()} instead. */
@Deprecated public boolean isLive;
/** /**
* Whether the media in this window is live. For informational purposes only. * The {@link MediaItem.LiveConfiguration} that is used or null if {@link #isLive()} returns
* * false.
* <p>Check {@link #isDynamic} to know whether this window may still change.
*/ */
public boolean isLive; @Nullable public MediaItem.LiveConfiguration liveConfiguration;
/** /**
* Whether this window contains placeholder information because the real information has yet to * Whether this window contains placeholder information because the real information has yet to
...@@ -248,7 +252,7 @@ public abstract class Timeline { ...@@ -248,7 +252,7 @@ public abstract class Timeline {
long elapsedRealtimeEpochOffsetMs, long elapsedRealtimeEpochOffsetMs,
boolean isSeekable, boolean isSeekable,
boolean isDynamic, boolean isDynamic,
boolean isLive, @Nullable MediaItem.LiveConfiguration liveConfiguration,
long defaultPositionUs, long defaultPositionUs,
long durationUs, long durationUs,
int firstPeriodIndex, int firstPeriodIndex,
...@@ -266,7 +270,8 @@ public abstract class Timeline { ...@@ -266,7 +270,8 @@ public abstract class Timeline {
this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs;
this.isSeekable = isSeekable; this.isSeekable = isSeekable;
this.isDynamic = isDynamic; this.isDynamic = isDynamic;
this.isLive = isLive; this.isLive = liveConfiguration != null;
this.liveConfiguration = liveConfiguration;
this.defaultPositionUs = defaultPositionUs; this.defaultPositionUs = defaultPositionUs;
this.durationUs = durationUs; this.durationUs = durationUs;
this.firstPeriodIndex = firstPeriodIndex; this.firstPeriodIndex = firstPeriodIndex;
...@@ -336,6 +341,14 @@ public abstract class Timeline { ...@@ -336,6 +341,14 @@ public abstract class Timeline {
return Util.getNowUnixTimeMs(elapsedRealtimeEpochOffsetMs); return Util.getNowUnixTimeMs(elapsedRealtimeEpochOffsetMs);
} }
/** Returns whether this is a live stream. */
// Verifies whether the deprecated isLive member field is in a correct state.
@SuppressWarnings("deprecation")
public boolean isLive() {
checkState(isLive == (liveConfiguration != null));
return liveConfiguration != null;
}
// Provide backward compatibility for tag. // Provide backward compatibility for tag.
@Override @Override
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
...@@ -349,12 +362,12 @@ public abstract class Timeline { ...@@ -349,12 +362,12 @@ public abstract class Timeline {
return Util.areEqual(uid, that.uid) return Util.areEqual(uid, that.uid)
&& Util.areEqual(mediaItem, that.mediaItem) && Util.areEqual(mediaItem, that.mediaItem)
&& Util.areEqual(manifest, that.manifest) && Util.areEqual(manifest, that.manifest)
&& Util.areEqual(liveConfiguration, that.liveConfiguration)
&& presentationStartTimeMs == that.presentationStartTimeMs && presentationStartTimeMs == that.presentationStartTimeMs
&& windowStartTimeMs == that.windowStartTimeMs && windowStartTimeMs == that.windowStartTimeMs
&& elapsedRealtimeEpochOffsetMs == that.elapsedRealtimeEpochOffsetMs && elapsedRealtimeEpochOffsetMs == that.elapsedRealtimeEpochOffsetMs
&& isSeekable == that.isSeekable && isSeekable == that.isSeekable
&& isDynamic == that.isDynamic && isDynamic == that.isDynamic
&& isLive == that.isLive
&& isPlaceholder == that.isPlaceholder && isPlaceholder == that.isPlaceholder
&& defaultPositionUs == that.defaultPositionUs && defaultPositionUs == that.defaultPositionUs
&& durationUs == that.durationUs && durationUs == that.durationUs
...@@ -370,6 +383,7 @@ public abstract class Timeline { ...@@ -370,6 +383,7 @@ public abstract class Timeline {
result = 31 * result + uid.hashCode(); result = 31 * result + uid.hashCode();
result = 31 * result + mediaItem.hashCode(); result = 31 * result + mediaItem.hashCode();
result = 31 * result + (manifest == null ? 0 : manifest.hashCode()); result = 31 * result + (manifest == null ? 0 : manifest.hashCode());
result = 31 * result + (liveConfiguration == null ? 0 : liveConfiguration.hashCode());
result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32)); result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32));
result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32)); result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32));
result = result =
...@@ -377,7 +391,6 @@ public abstract class Timeline { ...@@ -377,7 +391,6 @@ public abstract class Timeline {
+ (int) (elapsedRealtimeEpochOffsetMs ^ (elapsedRealtimeEpochOffsetMs >>> 32)); + (int) (elapsedRealtimeEpochOffsetMs ^ (elapsedRealtimeEpochOffsetMs >>> 32));
result = 31 * result + (isSeekable ? 1 : 0); result = 31 * result + (isSeekable ? 1 : 0);
result = 31 * result + (isDynamic ? 1 : 0); result = 31 * result + (isDynamic ? 1 : 0);
result = 31 * result + (isLive ? 1 : 0);
result = 31 * result + (isPlaceholder ? 1 : 0); result = 31 * result + (isPlaceholder ? 1 : 0);
result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32)); result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32));
result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); result = 31 * result + (int) (durationUs ^ (durationUs >>> 32));
...@@ -519,9 +532,13 @@ public abstract class Timeline { ...@@ -519,9 +532,13 @@ public abstract class Timeline {
return positionInWindowUs; return positionInWindowUs;
} }
/** /** Returns the opaque identifier for ads played with this period, or {@code null} if unset. */
* Returns the number of ad groups in the period. @Nullable
*/ public Object getAdsId() {
return adPlaybackState.adsId;
}
/** Returns the number of ad groups in the period. */
public int getAdGroupCount() { public int getAdGroupCount() {
return adPlaybackState.adGroupCount; return adPlaybackState.adGroupCount;
} }
...@@ -716,8 +733,8 @@ public abstract class Timeline { ...@@ -716,8 +733,8 @@ public abstract class Timeline {
* @param shuffleModeEnabled Whether shuffling is enabled. * @param shuffleModeEnabled Whether shuffling is enabled.
* @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window. * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window.
*/ */
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, public int getNextWindowIndex(
boolean shuffleModeEnabled) { int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
switch (repeatMode) { switch (repeatMode) {
case Player.REPEAT_MODE_OFF: case Player.REPEAT_MODE_OFF:
return windowIndex == getLastWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET return windowIndex == getLastWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET
...@@ -741,8 +758,8 @@ public abstract class Timeline { ...@@ -741,8 +758,8 @@ public abstract class Timeline {
* @param shuffleModeEnabled Whether shuffling is enabled. * @param shuffleModeEnabled Whether shuffling is enabled.
* @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window. * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window.
*/ */
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, public int getPreviousWindowIndex(
boolean shuffleModeEnabled) { int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
switch (repeatMode) { switch (repeatMode) {
case Player.REPEAT_MODE_OFF: case Player.REPEAT_MODE_OFF:
return windowIndex == getFirstWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET return windowIndex == getFirstWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET
...@@ -826,8 +843,12 @@ public abstract class Timeline { ...@@ -826,8 +843,12 @@ public abstract class Timeline {
* @param shuffleModeEnabled Whether shuffling is enabled. * @param shuffleModeEnabled Whether shuffling is enabled.
* @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period. * @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period.
*/ */
public final int getNextPeriodIndex(int periodIndex, Period period, Window window, public final int getNextPeriodIndex(
@Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { int periodIndex,
Period period,
Window window,
@Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
int windowIndex = getPeriod(periodIndex, period).windowIndex; int windowIndex = getPeriod(periodIndex, period).windowIndex;
if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) { if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) {
int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);
...@@ -840,8 +861,8 @@ public abstract class Timeline { ...@@ -840,8 +861,8 @@ public abstract class Timeline {
} }
/** /**
* Returns whether the given period is the last period of the timeline depending on the * Returns whether the given period is the last period of the timeline depending on the {@code
* {@code repeatMode} and whether shuffling is enabled. * repeatMode} and whether shuffling is enabled.
* *
* @param periodIndex A period index. * @param periodIndex A period index.
* @param period A {@link Period} to be used internally. Must not be null. * @param period A {@link Period} to be used internally. Must not be null.
...@@ -850,8 +871,12 @@ public abstract class Timeline { ...@@ -850,8 +871,12 @@ public abstract class Timeline {
* @param shuffleModeEnabled Whether shuffling is enabled. * @param shuffleModeEnabled Whether shuffling is enabled.
* @return Whether the period of the given index is the last period of the timeline. * @return Whether the period of the given index is the last period of the timeline.
*/ */
public final boolean isLastPeriod(int periodIndex, Period period, Window window, public final boolean isLastPeriod(
@Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { int periodIndex,
Period period,
Window window,
@Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
return getNextPeriodIndex(periodIndex, period, window, repeatMode, shuffleModeEnabled) return getNextPeriodIndex(periodIndex, period, window, repeatMode, shuffleModeEnabled)
== C.INDEX_UNSET; == C.INDEX_UNSET;
} }
......
...@@ -19,11 +19,11 @@ package com.google.android.exoplayer2.audio; ...@@ -19,11 +19,11 @@ package com.google.android.exoplayer2.audio;
public interface AudioListener { public interface AudioListener {
/** /**
* Called when the audio session is set. * Called when the audio session ID changes.
* *
* @param audioSessionId The audio session id. * @param audioSessionId The audio session ID.
*/ */
default void onAudioSessionId(int audioSessionId) {} default void onAudioSessionIdChanged(int audioSessionId) {}
/** /**
* Called when the audio attributes change. * Called when the audio attributes change.
......
...@@ -30,6 +30,31 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull; ...@@ -30,6 +30,31 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
public class DecoderInputBuffer extends Buffer { public class DecoderInputBuffer extends Buffer {
/** /**
* Thrown when an attempt is made to write into a {@link DecoderInputBuffer} whose {@link
* #bufferReplacementMode} is {@link #BUFFER_REPLACEMENT_MODE_DISABLED} and who {@link #data}
* capacity is smaller than required.
*/
public static final class InsufficientCapacityException extends IllegalStateException {
/** The current capacity of the buffer. */
public final int currentCapacity;
/** The required capacity of the buffer. */
public final int requiredCapacity;
/**
* Creates an instance.
*
* @param currentCapacity The current capacity of the buffer.
* @param requiredCapacity The required capacity of the buffer.
*/
public InsufficientCapacityException(int currentCapacity, int requiredCapacity) {
super("Buffer too small (" + currentCapacity + " < " + requiredCapacity + ")");
this.currentCapacity = currentCapacity;
this.requiredCapacity = requiredCapacity;
}
}
/**
* The buffer replacement mode. This controls how {@link #ensureSpaceForWrite} generates * The buffer replacement mode. This controls how {@link #ensureSpaceForWrite} generates
* replacement buffers when the capacity of the existing buffer is insufficient. One of {@link * replacement buffers when the capacity of the existing buffer is insufficient. One of {@link
* #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} or {@link * #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} or {@link
...@@ -144,8 +169,8 @@ public class DecoderInputBuffer extends Buffer { ...@@ -144,8 +169,8 @@ public class DecoderInputBuffer extends Buffer {
* whose capacity is sufficient. Data up to the current position is copied to the new buffer. * whose capacity is sufficient. Data up to the current position is copied to the new buffer.
* *
* @param length The length of the write that must be accommodated, in bytes. * @param length The length of the write that must be accommodated, in bytes.
* @throws IllegalStateException If there is insufficient capacity to accommodate the write and * @throws InsufficientCapacityException If there is insufficient capacity to accommodate the
* the buffer replacement mode of the holder is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}. * write and {@link #bufferReplacementMode} is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}.
*/ */
@EnsuresNonNull("data") @EnsuresNonNull("data")
public void ensureSpaceForWrite(int length) { public void ensureSpaceForWrite(int length) {
...@@ -223,8 +248,7 @@ public class DecoderInputBuffer extends Buffer { ...@@ -223,8 +248,7 @@ public class DecoderInputBuffer extends Buffer {
return ByteBuffer.allocateDirect(requiredCapacity); return ByteBuffer.allocateDirect(requiredCapacity);
} else { } else {
int currentCapacity = data == null ? 0 : data.capacity(); int currentCapacity = data == null ? 0 : data.capacity();
throw new IllegalStateException("Buffer too small (" + currentCapacity + " < " throw new InsufficientCapacityException(currentCapacity, requiredCapacity);
+ requiredCapacity + ")");
} }
} }
} }
...@@ -137,32 +137,13 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable { ...@@ -137,32 +137,13 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
Arrays.sort(this.schemeDatas, this); Arrays.sort(this.schemeDatas, this);
} }
/* package */ /* package */ DrmInitData(Parcel in) {
DrmInitData(Parcel in) {
schemeType = in.readString(); schemeType = in.readString();
schemeDatas = Util.castNonNull(in.createTypedArray(SchemeData.CREATOR)); schemeDatas = Util.castNonNull(in.createTypedArray(SchemeData.CREATOR));
schemeDataCount = schemeDatas.length; schemeDataCount = schemeDatas.length;
} }
/** /**
* Retrieves data for a given DRM scheme, specified by its UUID.
*
* @deprecated Use {@link #get(int)} and {@link SchemeData#matches(UUID)} instead.
* @param uuid The DRM scheme's UUID.
* @return The initialization data for the scheme, or null if the scheme is not supported.
*/
@Deprecated
@Nullable
public SchemeData get(UUID uuid) {
for (SchemeData schemeData : schemeDatas) {
if (schemeData.matches(uuid)) {
return schemeData;
}
}
return null;
}
/**
* Retrieves the {@link SchemeData} at a given index. * Retrieves the {@link SchemeData} at a given index.
* *
* @param index The index of the scheme to return. Must not exceed {@link #schemeDataCount}. * @param index The index of the scheme to return. Must not exceed {@link #schemeDataCount}.
......
...@@ -15,5 +15,5 @@ ...@@ -15,5 +15,5 @@
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
/** An opaque {@link android.media.MediaCrypto} equivalent. */ /** Enables decoding of encrypted data using keys in a DRM session. */
public interface ExoMediaCrypto {} public interface ExoMediaCrypto {}
...@@ -45,8 +45,7 @@ public final class ChapterTocFrame extends Id3Frame { ...@@ -45,8 +45,7 @@ public final class ChapterTocFrame extends Id3Frame {
this.subFrames = subFrames; this.subFrames = subFrames;
} }
/* package */ /* package */ ChapterTocFrame(Parcel in) {
ChapterTocFrame(Parcel in) {
super(ID); super(ID);
this.elementId = castNonNull(in.readString()); this.elementId = castNonNull(in.readString());
this.isRoot = in.readByte() != 0; this.isRoot = in.readByte() != 0;
......
...@@ -45,8 +45,7 @@ public final class MlltFrame extends Id3Frame { ...@@ -45,8 +45,7 @@ public final class MlltFrame extends Id3Frame {
this.millisecondsDeviations = millisecondsDeviations; this.millisecondsDeviations = millisecondsDeviations;
} }
/* package */ /* package */ MlltFrame(Parcel in) {
MlltFrame(Parcel in) {
super(ID); super(ID);
this.mpegFramesBetweenReference = in.readInt(); this.mpegFramesBetweenReference = in.readInt();
this.bytesBetweenReference = in.readInt(); this.bytesBetweenReference = in.readInt();
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.extractor.mp4; package com.google.android.exoplayer2.metadata.mp4;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
...@@ -28,6 +28,9 @@ import java.util.Arrays; ...@@ -28,6 +28,9 @@ import java.util.Arrays;
*/ */
public final class MdtaMetadataEntry implements Metadata.Entry { public final class MdtaMetadataEntry implements Metadata.Entry {
/** Key for the capture frame rate (in frames per second). */
public static final String KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps";
/** The metadata key name. */ /** The metadata key name. */
public final String key; public final String key;
/** The payload. The interpretation of the value depends on {@link #typeIndicator}. */ /** The payload. The interpretation of the value depends on {@link #typeIndicator}. */
......
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.mp4;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.common.primitives.Longs;
/** Metadata of a motion photo file. */
public final class MotionPhotoMetadata implements Metadata.Entry {
/** The start offset of the photo data, in bytes. */
public final long photoStartPosition;
/** The size of the photo data, in bytes. */
public final long photoSize;
/**
* The presentation timestamp of the photo, in microseconds, or {@link C#TIME_UNSET} if unknown.
*/
public final long photoPresentationTimestampUs;
/** The start offset of the video data, in bytes. */
public final long videoStartPosition;
/** The size of the video data, in bytes. */
public final long videoSize;
/** Creates an instance. */
public MotionPhotoMetadata(
long photoStartPosition,
long photoSize,
long photoPresentationTimestampUs,
long videoStartPosition,
long videoSize) {
this.photoStartPosition = photoStartPosition;
this.photoSize = photoSize;
this.photoPresentationTimestampUs = photoPresentationTimestampUs;
this.videoStartPosition = videoStartPosition;
this.videoSize = videoSize;
}
private MotionPhotoMetadata(Parcel in) {
photoStartPosition = in.readLong();
photoSize = in.readLong();
photoPresentationTimestampUs = in.readLong();
videoStartPosition = in.readLong();
videoSize = in.readLong();
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
MotionPhotoMetadata other = (MotionPhotoMetadata) obj;
return photoStartPosition == other.photoStartPosition
&& photoSize == other.photoSize
&& photoPresentationTimestampUs == other.photoPresentationTimestampUs
&& videoStartPosition == other.videoStartPosition
&& videoSize == other.videoSize;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + Longs.hashCode(photoStartPosition);
result = 31 * result + Longs.hashCode(photoSize);
result = 31 * result + Longs.hashCode(photoPresentationTimestampUs);
result = 31 * result + Longs.hashCode(videoStartPosition);
result = 31 * result + Longs.hashCode(videoSize);
return result;
}
@Override
public String toString() {
return "Motion photo metadata: photoStartPosition="
+ photoStartPosition
+ ", photoSize="
+ photoSize
+ ", photoPresentationTimestampUs="
+ photoPresentationTimestampUs
+ ", videoStartPosition="
+ videoStartPosition
+ ", videoSize="
+ videoSize;
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(photoStartPosition);
dest.writeLong(photoSize);
dest.writeLong(photoPresentationTimestampUs);
dest.writeLong(videoStartPosition);
dest.writeLong(videoSize);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<MotionPhotoMetadata> CREATOR =
new Parcelable.Creator<MotionPhotoMetadata>() {
@Override
public MotionPhotoMetadata createFromParcel(Parcel in) {
return new MotionPhotoMetadata(in);
}
@Override
public MotionPhotoMetadata[] newArray(int size) {
return new MotionPhotoMetadata[size];
}
};
}
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.mp4;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Objects;
import com.google.common.collect.ComparisonChain;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/** Holds information about the segments of slow motion playback within a track. */
public final class SlowMotionData implements Metadata.Entry {
/** Holds information about a single segment of slow motion playback within a track. */
public static final class Segment implements Parcelable {
public static final Comparator<Segment> BY_START_THEN_END_THEN_DIVISOR =
(s1, s2) ->
ComparisonChain.start()
.compare(s1.startTimeMs, s2.startTimeMs)
.compare(s1.endTimeMs, s2.endTimeMs)
.compare(s1.speedDivisor, s2.speedDivisor)
.result();
/** The start time, in milliseconds, of the track segment that is intended to be slow motion. */
public final long startTimeMs;
/** The end time, in milliseconds, of the track segment that is intended to be slow motion. */
public final long endTimeMs;
/**
* The speed reduction factor.
*
* <p>For example, 4 would mean the segment should be played at a quarter (1/4) of the normal
* speed.
*/
public final int speedDivisor;
/**
* Creates an instance.
*
* @param startTimeMs See {@link #startTimeMs}. Must be less than endTimeMs.
* @param endTimeMs See {@link #endTimeMs}.
* @param speedDivisor See {@link #speedDivisor}.
*/
public Segment(long startTimeMs, long endTimeMs, int speedDivisor) {
checkArgument(startTimeMs < endTimeMs);
this.startTimeMs = startTimeMs;
this.endTimeMs = endTimeMs;
this.speedDivisor = speedDivisor;
}
@Override
public String toString() {
return Util.formatInvariant(
"Segment: startTimeMs=%d, endTimeMs=%d, speedDivisor=%d",
startTimeMs, endTimeMs, speedDivisor);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Segment segment = (Segment) o;
return startTimeMs == segment.startTimeMs
&& endTimeMs == segment.endTimeMs
&& speedDivisor == segment.speedDivisor;
}
@Override
public int hashCode() {
return Objects.hashCode(startTimeMs, endTimeMs, speedDivisor);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(startTimeMs);
dest.writeLong(endTimeMs);
dest.writeInt(speedDivisor);
}
public static final Creator<Segment> CREATOR =
new Creator<Segment>() {
@Override
public Segment createFromParcel(Parcel in) {
long startTimeMs = in.readLong();
long endTimeMs = in.readLong();
int speedDivisor = in.readInt();
return new Segment(startTimeMs, endTimeMs, speedDivisor);
}
@Override
public Segment[] newArray(int size) {
return new Segment[size];
}
};
}
public final List<Segment> segments;
/**
* Creates an instance with a list of {@link Segment}s.
*
* <p>The segments must not overlap, that is that the start time of a segment can not be between
* the start and end time of another segment.
*/
public SlowMotionData(List<Segment> segments) {
this.segments = segments;
checkArgument(!doSegmentsOverlap(segments));
}
@Override
public String toString() {
return "SlowMotion: segments=" + segments;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SlowMotionData that = (SlowMotionData) o;
return segments.equals(that.segments);
}
@Override
public int hashCode() {
return segments.hashCode();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeList(segments);
}
public static final Creator<SlowMotionData> CREATOR =
new Creator<SlowMotionData>() {
@Override
public SlowMotionData createFromParcel(Parcel in) {
List<Segment> slowMotionSegments = new ArrayList<>();
in.readList(slowMotionSegments, Segment.class.getClassLoader());
return new SlowMotionData(slowMotionSegments);
}
@Override
public SlowMotionData[] newArray(int size) {
return new SlowMotionData[size];
}
};
private static boolean doSegmentsOverlap(List<Segment> segments) {
if (segments.isEmpty()) {
return false;
}
long previousEndTimeMs = segments.get(0).endTimeMs;
for (int i = 1; i < segments.size(); i++) {
if (segments.get(i).startTimeMs < previousEndTimeMs) {
return true;
}
previousEndTimeMs = segments.get(i).endTimeMs;
}
return false;
}
}
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.mp4;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.common.primitives.Floats;
/**
* Stores metadata from the Samsung smta box.
*
* <p>See [Internal: b/150138465#comment76].
*/
public final class SmtaMetadataEntry implements Metadata.Entry {
/**
* The capture frame rate, in fps, or {@link C#RATE_UNSET} if it is unknown.
*
* <p>If known, the capture frame rate should always be an integer value.
*/
public final float captureFrameRate;
/** The number of layers in the SVC extended frames. */
public final int svcTemporalLayerCount;
/** Creates an instance. */
public SmtaMetadataEntry(float captureFrameRate, int svcTemporalLayerCount) {
this.captureFrameRate = captureFrameRate;
this.svcTemporalLayerCount = svcTemporalLayerCount;
}
private SmtaMetadataEntry(Parcel in) {
captureFrameRate = in.readFloat();
svcTemporalLayerCount = in.readInt();
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
SmtaMetadataEntry other = (SmtaMetadataEntry) obj;
return captureFrameRate == other.captureFrameRate
&& svcTemporalLayerCount == other.svcTemporalLayerCount;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + Floats.hashCode(captureFrameRate);
result = 31 * result + svcTemporalLayerCount;
return result;
}
@Override
public String toString() {
return "smta: captureFrameRate="
+ captureFrameRate
+ ", svcTemporalLayerCount="
+ svcTemporalLayerCount;
}
// Parcelable implementation.
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeFloat(captureFrameRate);
dest.writeInt(svcTemporalLayerCount);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<SmtaMetadataEntry> CREATOR =
new Parcelable.Creator<SmtaMetadataEntry>() {
@Override
public SmtaMetadataEntry createFromParcel(Parcel in) {
return new SmtaMetadataEntry(in);
}
@Override
public SmtaMetadataEntry[] newArray(int size) {
return new SmtaMetadataEntry[size];
}
};
}
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@NonNullApi
package com.google.android.exoplayer2.metadata.mp4;
import com.google.android.exoplayer2.util.NonNullApi;
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline;
/**
* Identifies a specific playback of a {@link Timeline.Period}.
*
* <p>A {@link Timeline.Period} can be played multiple times, for example if it is repeated. Each
* instances of this class identifies a specific playback of a {@link Timeline.Period}.
*
* <p>In ExoPlayer's implementation, {@link MediaPeriodId} identifies a {@code MediaPeriod}.
*/
// TODO(b/172315872) Should be final, but subclassed in MediaSource for backward-compatibility.
public class MediaPeriodId {
/** The unique id of the timeline period. */
public final Object periodUid;
/**
* If the media period is in an ad group, the index of the ad group in the period. {@link
* C#INDEX_UNSET} otherwise.
*/
public final int adGroupIndex;
/**
* If the media period is in an ad group, the index of the ad in its ad group in the period.
* {@link C#INDEX_UNSET} otherwise.
*/
public final int adIndexInAdGroup;
/**
* The sequence number of the window in the buffered sequence of windows this media period is part
* of. {@link C#INDEX_UNSET} if the media period id is not part of a buffered sequence of windows.
*/
public final long windowSequenceNumber;
/**
* The index of the next ad group to which the media period's content is clipped, or {@link
* C#INDEX_UNSET} if there is no following ad group or if this media period is an ad.
*/
public final int nextAdGroupIndex;
/**
* Creates a media period identifier for a period which is not part of a buffered sequence of
* windows.
*
* @param periodUid The unique id of the timeline period.
*/
public MediaPeriodId(Object periodUid) {
this(periodUid, /* windowSequenceNumber= */ C.INDEX_UNSET);
}
/**
* Creates a media period identifier for the specified period in the timeline.
*
* @param periodUid The unique id of the timeline period.
* @param windowSequenceNumber The sequence number of the window in the buffered sequence of
* windows this media period is part of.
*/
public MediaPeriodId(Object periodUid, long windowSequenceNumber) {
this(
periodUid,
/* adGroupIndex= */ C.INDEX_UNSET,
/* adIndexInAdGroup= */ C.INDEX_UNSET,
windowSequenceNumber,
/* nextAdGroupIndex= */ C.INDEX_UNSET);
}
/**
* Creates a media period identifier for the specified clipped period in the timeline.
*
* @param periodUid The unique id of the timeline period.
* @param windowSequenceNumber The sequence number of the window in the buffered sequence of
* windows this media period is part of.
* @param nextAdGroupIndex The index of the next ad group to which the media period's content is
* clipped.
*/
public MediaPeriodId(Object periodUid, long windowSequenceNumber, int nextAdGroupIndex) {
this(
periodUid,
/* adGroupIndex= */ C.INDEX_UNSET,
/* adIndexInAdGroup= */ C.INDEX_UNSET,
windowSequenceNumber,
nextAdGroupIndex);
}
/**
* Creates a media period identifier that identifies an ad within an ad group at the specified
* timeline period.
*
* @param periodUid The unique id of the timeline period that contains the ad group.
* @param adGroupIndex The index of the ad group.
* @param adIndexInAdGroup The index of the ad in the ad group.
* @param windowSequenceNumber The sequence number of the window in the buffered sequence of
* windows this media period is part of.
*/
public MediaPeriodId(
Object periodUid, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) {
this(
periodUid,
adGroupIndex,
adIndexInAdGroup,
windowSequenceNumber,
/* nextAdGroupIndex= */ C.INDEX_UNSET);
}
/** Copy constructor for inheritance. */
// TODO(b/172315872) Delete when client have migrated from MediaSource.MediaPeriodId
protected MediaPeriodId(MediaPeriodId mediaPeriodId) {
this.periodUid = mediaPeriodId.periodUid;
this.adGroupIndex = mediaPeriodId.adGroupIndex;
this.adIndexInAdGroup = mediaPeriodId.adIndexInAdGroup;
this.windowSequenceNumber = mediaPeriodId.windowSequenceNumber;
this.nextAdGroupIndex = mediaPeriodId.nextAdGroupIndex;
}
private MediaPeriodId(
Object periodUid,
int adGroupIndex,
int adIndexInAdGroup,
long windowSequenceNumber,
int nextAdGroupIndex) {
this.periodUid = periodUid;
this.adGroupIndex = adGroupIndex;
this.adIndexInAdGroup = adIndexInAdGroup;
this.windowSequenceNumber = windowSequenceNumber;
this.nextAdGroupIndex = nextAdGroupIndex;
}
/** Returns a copy of this period identifier but with {@code newPeriodUid} as its period uid. */
public MediaPeriodId copyWithPeriodUid(Object newPeriodUid) {
return periodUid.equals(newPeriodUid)
? this
: new MediaPeriodId(
newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, nextAdGroupIndex);
}
/** Returns whether this period identifier identifies an ad in an ad group in a period. */
public boolean isAd() {
return adGroupIndex != C.INDEX_UNSET;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof MediaPeriodId)) {
return false;
}
MediaPeriodId periodId = (MediaPeriodId) obj;
return periodUid.equals(periodId.periodUid)
&& adGroupIndex == periodId.adGroupIndex
&& adIndexInAdGroup == periodId.adIndexInAdGroup
&& windowSequenceNumber == periodId.windowSequenceNumber
&& nextAdGroupIndex == periodId.nextAdGroupIndex;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + periodUid.hashCode();
result = 31 * result + adGroupIndex;
result = 31 * result + adIndexInAdGroup;
result = 31 * result + (int) windowSequenceNumber;
result = 31 * result + nextAdGroupIndex;
return result;
}
}
...@@ -23,20 +23,10 @@ import com.google.android.exoplayer2.Format; ...@@ -23,20 +23,10 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.util.Arrays; import java.util.Arrays;
// TODO: Add an allowMultipleStreams boolean to indicate where the one stream per group restriction /** Defines an immutable group of tracks identified by their format identity. */
// does not apply.
/**
* Defines a group of tracks exposed by a {@link MediaPeriod}.
*
* <p>A {@link MediaPeriod} is only able to provide one {@link SampleStream} corresponding to a
* group at any given time, however this {@link SampleStream} may adapt between multiple tracks
* within the group.
*/
public final class TrackGroup implements Parcelable { public final class TrackGroup implements Parcelable {
/** /** The number of tracks in the group. */
* The number of tracks in the group.
*/
public final int length; public final int length;
private final Format[] formats; private final Format[] formats;
...@@ -45,7 +35,7 @@ public final class TrackGroup implements Parcelable { ...@@ -45,7 +35,7 @@ public final class TrackGroup implements Parcelable {
private int hashCode; private int hashCode;
/** /**
* @param formats The track formats. Must not be null, contain null elements or be of length 0. * @param formats The track formats. At least one {@link Format} must be provided.
*/ */
public TrackGroup(Format... formats) { public TrackGroup(Format... formats) {
Assertions.checkState(formats.length > 0); Assertions.checkState(formats.length > 0);
......
No preview for this file type
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