Commit ffea2a64 by Ian Baker Committed by GitHub

Merge pull request #8300 from google/dev-v2-r2.12.2

r2.12.2
parents 59b34ede 1cb346ee
Showing with 1093 additions and 312 deletions
# Release notes
### 2.12.2 (2020-12-01) ###
* Core library:
* Suppress exceptions from registering/unregistering the stream volume
receiver ([#8087](https://github.com/google/ExoPlayer/issues/8087)),
([#8106](https://github.com/google/ExoPlayer/issues/8106)).
* Suppress ProGuard warnings caused by Guava's compile-only dependencies
([#8103](https://github.com/google/ExoPlayer/issues/8103)).
* Fix issue that could cause playback to freeze when selecting tracks, if
extension audio renderers are being used
([#8203](https://github.com/google/ExoPlayer/issues/8203)).
* UI:
* Fix incorrect color and text alignment of the `StyledPlayerControlView`
fast forward and rewind buttons, when used together with the
`com.google.android.material` library
([#7898](https://github.com/google/ExoPlayer/issues/7898)).
* Add `dispatchPrepare(Player)` to `ControlDispatcher` and implement it in
`DefaultControlDispatcher`. Deprecate `PlaybackPreparer` and
`setPlaybackPreparer` in `StyledPlayerView`, `StyledPlayerControlView`,
`PlayerView`, `PlayerControlView`, `PlayerNotificationManager` and
`LeanbackPlayerAdapter` and use `ControlDispatcher` for dispatching
prepare instead
([#7882](https://github.com/google/ExoPlayer/issues/7882)).
* Increase seekbar's touch target height in `StyledPlayerControlView`.
* Update `StyledPlayerControlView` menu items to behave correctly for
right-to-left languages.
* Support enabling the previous and next actions individually in
`PlayerNotificationManager`.
* Audio:
* Retry playback after some types of `AudioTrack` error.
* Work around `AudioManager` crashes when calling `getStreamVolume`
([#8191](https://github.com/google/ExoPlayer/issues/8191)).
* Extractors:
* Matroska: Add support for 32-bit floating point PCM, and 8-bit and
16-bit big endian integer PCM
([#8142](https://github.com/google/ExoPlayer/issues/8142)).
* MP4: Add support for mpeg1 video box
([#8257](https://github.com/google/ExoPlayer/issues/8257)).
* IMA extension:
* Upgrade IMA SDK dependency to 3.21.0, and release the `AdsLoader`
([#7344](https://github.com/google/ExoPlayer/issues/7344)).
* Improve handling of ad tags with unsupported VPAID ads
([#7832](https://github.com/google/ExoPlayer/issues/7832)).
* Fix a bug that caused multiple ads in an ad pod to be skipped when one
ad in the ad pod was skipped.
* Fix a bug that caused ad progress not to be updated if the player
resumed after buffering during an ad
([#8239](https://github.com/google/ExoPlayer/issues/8239)).
* Fix passing an ads response to the `ImaAdsLoader` builder.
* Set the overlay language based on the device locale by default.
* Cronet extension:
* Fix handling of HTTP status code 200 when making unbounded length range
requests ([#8090](https://github.com/google/ExoPlayer/issues/8090)).
* Text
* Allow tx3g subtitles with `styl` boxes with start and/or end offsets
that lie outside the length of the cue text.
* Media2 extension:
* Notify onBufferingEnded when the state of origin player becomes
STATE_IDLE or STATE_ENDED.
* Allow to remove all playlist items that makes the player reset.
### 2.12.1 (2020-10-23) ###
* Core library:
......@@ -7,6 +68,7 @@
argument ([#8024](https://github.com/google/ExoPlayer/issues/8024)).
* Fix bug where streams with highly uneven track durations may get stuck
in a buffering state
([#7943](https://github.com/google/ExoPlayer/issues/7943)).
* Switch Guava dependency from `implementation` to `api`
([#7905](https://github.com/google/ExoPlayer/issues/7905),
[#7993](https://github.com/google/ExoPlayer/issues/7993)).
......@@ -54,6 +116,9 @@
([#7992](https://github.com/google/ExoPlayer/issues/7992)).
* FLV: Make files seekable by using the key frame index
([#7378](https://github.com/google/ExoPlayer/issues/7378)).
* Downloads: Fix issue retrying progressive downloads, which could also result
in a crash in `DownloadManager.InternalHandler.onContentLengthChanged`
([#8078](https://github.com/google/ExoPlayer/issues/8078).
* HLS: Fix crash affecting chunkful preparation of master playlists that start
with an I-FRAME only variant
([#8025](https://github.com/google/ExoPlayer/issues/8025)).
......@@ -63,12 +128,12 @@
* Allow apps to specify a `VideoAdPlayerCallback`
([#7944](https://github.com/google/ExoPlayer/issues/7944)).
* Accept ad tags via the `AdsMediaSource` constructor and deprecate
passing them via the `ImaAdsLoader` constructor/builders. Passing the
ad tag via media item playback properties continues to be supported.
This is in preparation for supporting ads in playlists
passing them via the `ImaAdsLoader` constructor/builders. Passing the ad
tag via media item playback properties continues to be supported. This
is in preparation for supporting ads in playlists
([#3750](https://github.com/google/ExoPlayer/issues/3750)).
* Add a way to override ad media MIME types
([#7961)(https://github.com/google/ExoPlayer/issues/7961)).
([#7961](https://github.com/google/ExoPlayer/issues/7961)).
* Fix incorrect truncation of large cue point positions
([#8067](https://github.com/google/ExoPlayer/issues/8067)).
* Upgrade IMA SDK dependency to 3.20.1. This brings in a fix for
......
......@@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.12.1'
releaseVersionCode = 2012001
releaseVersion = '2.12.2'
releaseVersionCode = 2012002
minSdkVersion = 16
appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
......
......@@ -98,20 +98,20 @@ public class DownloadTracker {
}
public boolean isDownloaded(MediaItem mediaItem) {
Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
@Nullable Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
return download != null && download.state != Download.STATE_FAILED;
}
@Nullable
public DownloadRequest getDownloadRequest(Uri uri) {
Download download = downloads.get(uri);
@Nullable Download download = downloads.get(uri);
return download != null && download.state != Download.STATE_FAILED ? download.request : null;
}
public void toggleDownload(
FragmentManager fragmentManager, MediaItem mediaItem, RenderersFactory renderersFactory) {
Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
if (download != null) {
@Nullable Download download = downloads.get(checkNotNull(mediaItem.playbackProperties).uri);
if (download != null && download.state != Download.STATE_FAILED) {
DownloadService.sendRemoveDownload(
context, DemoDownloadService.class, download.request.id, /* foreground= */ false);
} else {
......@@ -223,7 +223,7 @@ public class DownloadTracker {
widevineOfflineLicenseFetchTask =
new WidevineOfflineLicenseFetchTask(
format,
mediaItem.playbackProperties.drmConfiguration.licenseUri,
mediaItem.playbackProperties.drmConfiguration,
httpDataSourceFactory,
/* dialogHelper= */ this,
helper);
......@@ -373,7 +373,7 @@ public class DownloadTracker {
private static final class WidevineOfflineLicenseFetchTask extends AsyncTask<Void, Void, Void> {
private final Format format;
private final Uri licenseUri;
private final MediaItem.DrmConfiguration drmConfiguration;
private final HttpDataSource.Factory httpDataSourceFactory;
private final StartDownloadDialogHelper dialogHelper;
private final DownloadHelper downloadHelper;
......@@ -383,12 +383,12 @@ public class DownloadTracker {
public WidevineOfflineLicenseFetchTask(
Format format,
Uri licenseUri,
MediaItem.DrmConfiguration drmConfiguration,
HttpDataSource.Factory httpDataSourceFactory,
StartDownloadDialogHelper dialogHelper,
DownloadHelper downloadHelper) {
this.format = format;
this.licenseUri = licenseUri;
this.drmConfiguration = drmConfiguration;
this.httpDataSourceFactory = httpDataSourceFactory;
this.dialogHelper = dialogHelper;
this.downloadHelper = downloadHelper;
......@@ -398,8 +398,10 @@ public class DownloadTracker {
protected Void doInBackground(Void... voids) {
OfflineLicenseHelper offlineLicenseHelper =
OfflineLicenseHelper.newWidevineInstance(
licenseUri.toString(),
drmConfiguration.licenseUri.toString(),
drmConfiguration.forceDefaultLicenseUri,
httpDataSourceFactory,
drmConfiguration.requestHeaders,
new DrmSessionEventListener.EventDispatcher());
try {
keySetId = offlineLicenseHelper.downloadLicense(format);
......
......@@ -35,7 +35,6 @@ import androidx.appcompat.app.AppCompatActivity;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
......@@ -66,10 +65,11 @@ import java.net.CookiePolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/** An activity that plays media using {@link SimpleExoPlayer}. */
public class PlayerActivity extends AppCompatActivity
implements OnClickListener, PlaybackPreparer, StyledPlayerControlView.VisibilityListener {
implements OnClickListener, StyledPlayerControlView.VisibilityListener {
// Saved instance state keys.
......@@ -252,13 +252,6 @@ public class PlayerActivity extends AppCompatActivity
}
}
// PlaybackPreparer implementation
@Override
public void preparePlayback() {
player.prepare();
}
// PlayerControlView.VisibilityListener implementation
@Override
......@@ -304,7 +297,6 @@ public class PlayerActivity extends AppCompatActivity
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.setPlayWhenReady(startAutoPlay);
playerView.setPlayer(player);
playerView.setPlaybackPreparer(this);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
}
......@@ -335,6 +327,7 @@ public class PlayerActivity extends AppCompatActivity
if (!Util.checkCleartextTrafficPermitted(mediaItem)) {
showToast(R.string.error_cleartext_not_permitted);
finish();
return Collections.emptyList();
}
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, mediaItem)) {
......@@ -551,7 +544,9 @@ public class PlayerActivity extends AppCompatActivity
.setCustomCacheKey(downloadRequest.customCacheKey)
.setMimeType(downloadRequest.mimeType)
.setStreamKeys(downloadRequest.streamKeys)
.setDrmKeySetId(downloadRequest.keySetId);
.setDrmKeySetId(downloadRequest.keySetId)
.setDrmLicenseRequestHeaders(getDrmRequestHeaders(item));
mediaItems.add(builder.build());
} else {
mediaItems.add(item);
......@@ -559,4 +554,10 @@ public class PlayerActivity extends AppCompatActivity
}
return mediaItems;
}
@Nullable
private static Map<String, String> getDrmRequestHeaders(MediaItem item) {
MediaItem.DrmConfiguration drmConfiguration = item.playbackProperties.drmConfiguration;
return drmConfiguration != null ? drmConfiguration.requestHeaders : null;
}
}
......@@ -21,7 +21,7 @@
<string name="unexpected_intent_action">Unexpected intent action: <xliff:g id="action">%1$s</xliff:g></string>
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
<string name="error_cleartext_not_permitted">Cleartext HTTP traffic not permitted. See https://exoplayer.dev/issues/cleartext-not-permitted</string>
<string name="error_generic">Playback failed</string>
......
......@@ -27,6 +27,10 @@ import com.google.android.gms.cast.MediaTrack;
*/
/* package */ final class CastUtils {
/** The duration returned by {@link MediaInfo#getStreamDuration()} for live streams. */
// TODO: Remove once [Internal ref: b/171657375] is fixed.
private static final long LIVE_STREAM_DURATION = -1000;
/**
* Returns the duration in microseconds advertised by a media info, or {@link C#TIME_UNSET} if
* unknown or not applicable.
......@@ -39,7 +43,9 @@ import com.google.android.gms.cast.MediaTrack;
return C.TIME_UNSET;
}
long durationMs = mediaInfo.getStreamDuration();
return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
return durationMs != MediaInfo.UNKNOWN_DURATION && durationMs != LIVE_STREAM_DURATION
? C.msToUs(durationMs)
: C.TIME_UNSET;
}
/**
......
......@@ -443,8 +443,14 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
transferInitializing(dataSpec);
try {
boolean connectionOpened = blockUntilConnectTimeout();
if (exception != null) {
throw new OpenException(exception, dataSpec, getStatus(urlRequest));
@Nullable IOException connectionOpenException = exception;
if (connectionOpenException != null) {
@Nullable String message = connectionOpenException.getMessage();
if (message != null
&& Util.toLowerInvariant(message).contains("err_cleartext_not_permitted")) {
throw new CleartextNotPermittedException(connectionOpenException, dataSpec);
}
throw new OpenException(connectionOpenException, dataSpec, getStatus(urlRequest));
} else if (!connectionOpened) {
// The timeout was reached before the connection was opened.
throw new OpenException(new SocketTimeoutException(), dataSpec, getStatus(urlRequest));
......@@ -506,7 +512,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length;
} else {
bytesRemaining = getContentLength(responseInfo);
long contentLength = getContentLength(responseInfo);
bytesRemaining =
contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
}
} else {
// If the response is compressed then the content length will be that of the compressed data
......
......@@ -534,7 +534,8 @@ public final class CronetDataSourceTest {
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
dataSourceUnderTest.open(testDataSpec);
long length = dataSourceUnderTest.open(testDataSpec);
assertThat(length).isEqualTo(5000);
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
......@@ -551,7 +552,26 @@ public final class CronetDataSourceTest {
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
dataSourceUnderTest.open(testDataSpec);
long length = dataSourceUnderTest.open(testDataSpec);
assertThat(length).isEqualTo(5000);
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
assertThat(bytesRead).isEqualTo(16);
assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
verify(mockTransferListener)
.onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
}
@Test
public void unboundedRangeRequestWith200Response() throws HttpDataSourceException {
mockResponseStartSuccess();
mockReadSuccess(0, (int) TEST_CONTENT_LENGTH);
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, C.LENGTH_UNSET);
long length = dataSourceUnderTest.open(testDataSpec);
assertThat(length).isEqualTo(TEST_CONTENT_LENGTH - 1000);
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
......@@ -777,7 +797,8 @@ public final class CronetDataSourceTest {
testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
dataSourceUnderTest.open(testDataSpec);
long length = dataSourceUnderTest.open(testDataSpec);
assertThat(length).isEqualTo(5000);
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
......@@ -796,7 +817,8 @@ public final class CronetDataSourceTest {
testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
dataSourceUnderTest.open(testDataSpec);
long length = dataSourceUnderTest.open(testDataSpec);
assertThat(length).isEqualTo(5000);
ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
int bytesRead = dataSourceUnderTest.read(returnedBuffer);
......
......@@ -25,7 +25,7 @@ android {
}
dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.20.1'
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.21.0'
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
......
......@@ -47,8 +47,10 @@ import com.google.android.exoplayer2.testutil.HostActivity;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
......@@ -78,7 +80,7 @@ public final class ImaPlaybackTest {
@Test
public void playbackWithPrerollAdTag_playsAdAndContent() throws Exception {
String adsResponse =
TestUtil.getString(/* context= */ testRule.getActivity(), "ad-responses/preroll.xml");
TestUtil.getString(/* context= */ testRule.getActivity(), "media/ad-responses/preroll.xml");
AdId[] expectedAdIds = new AdId[] {ad(0), CONTENT};
ImaHostedTest hostedTest =
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
......@@ -90,7 +92,8 @@ public final class ImaPlaybackTest {
public void playbackWithMidrolls_playsAdAndContent() throws Exception {
String adsResponse =
TestUtil.getString(
/* context= */ testRule.getActivity(), "ad-responses/preroll_midroll6s_postroll.xml");
/* context= */ testRule.getActivity(),
"media/ad-responses/preroll_midroll6s_postroll.xml");
AdId[] expectedAdIds = new AdId[] {ad(0), CONTENT, ad(1), CONTENT, ad(2), CONTENT};
ImaHostedTest hostedTest =
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
......@@ -102,7 +105,7 @@ public final class ImaPlaybackTest {
public void playbackWithMidrolls1And7_playsAdsAndContent() throws Exception {
String adsResponse =
TestUtil.getString(
/* context= */ testRule.getActivity(), "ad-responses/midroll1s_midroll7s.xml");
/* context= */ testRule.getActivity(), "media/ad-responses/midroll1s_midroll7s.xml");
AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT};
ImaHostedTest hostedTest =
new ImaHostedTest(Uri.parse(CONTENT_URI_SHORT), adsResponse, expectedAdIds);
......@@ -114,7 +117,7 @@ public final class ImaPlaybackTest {
public void playbackWithMidrolls10And20WithSeekTo12_playsAdsAndContent() throws Exception {
String adsResponse =
TestUtil.getString(
/* context= */ testRule.getActivity(), "ad-responses/midroll10s_midroll20s.xml");
/* context= */ testRule.getActivity(), "media/ad-responses/midroll10s_midroll20s.xml");
AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT};
ImaHostedTest hostedTest =
new ImaHostedTest(Uri.parse(CONTENT_URI_LONG), adsResponse, expectedAdIds);
......@@ -131,7 +134,7 @@ public final class ImaPlaybackTest {
public void playbackWithMidrolls10And20WithSeekTo18_playsAdsAndContent() throws Exception {
String adsResponse =
TestUtil.getString(
/* context= */ testRule.getActivity(), "ad-responses/midroll10s_midroll20s.xml");
/* context= */ testRule.getActivity(), "media/ad-responses/midroll10s_midroll20s.xml");
AdId[] expectedAdIds = new AdId[] {CONTENT, ad(0), CONTENT, ad(1), CONTENT};
ImaHostedTest hostedTest =
new ImaHostedTest(Uri.parse(CONTENT_URI_LONG), adsResponse, expectedAdIds);
......@@ -190,7 +193,7 @@ public final class ImaPlaybackTest {
private static final class ImaHostedTest extends ExoHostedTest implements EventListener {
private final Uri contentUri;
private final String adsResponse;
private final DataSpec adTagDataSpec;
private final List<AdId> expectedAdIds;
private final List<AdId> seenAdIds;
private @MonotonicNonNull ImaAdsLoader imaAdsLoader;
......@@ -201,7 +204,9 @@ public final class ImaPlaybackTest {
// duration due to ad playback, so the hosted test shouldn't assert the playing duration.
super(ImaPlaybackTest.class.getSimpleName(), /* fullPlaybackNoSeeking= */ false);
this.contentUri = contentUri;
this.adsResponse = adsResponse;
this.adTagDataSpec =
new DataSpec(
Util.getDataUriForString(/* mimeType= */ "text/xml", /* data= */ adsResponse));
this.expectedAdIds = Arrays.asList(expectedAdIds);
seenAdIds = new ArrayList<>();
}
......@@ -226,7 +231,7 @@ public final class ImaPlaybackTest {
}
});
Context context = host.getApplicationContext();
imaAdsLoader = new ImaAdsLoader.Builder(context).buildForAdsResponse(adsResponse);
imaAdsLoader = new ImaAdsLoader.Builder(context).build();
imaAdsLoader.setPlayer(player);
return player;
}
......@@ -242,7 +247,8 @@ public final class ImaPlaybackTest {
new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(contentUri));
return new AdsMediaSource(
contentMediaSource,
dataSourceFactory,
adTagDataSpec,
new DefaultMediaSourceFactory(dataSourceFactory),
Assertions.checkNotNull(imaAdsLoader),
new AdViewProvider() {
......
......@@ -82,6 +82,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
......@@ -705,7 +706,9 @@ public final class ImaAdsLoader
if (adTagUri != null) {
adTagDataSpec = new DataSpec(adTagUri);
} else if (adsResponse != null) {
adTagDataSpec = new DataSpec(Util.getDataUriForString(adsResponse, "text/xml"));
adTagDataSpec =
new DataSpec(
Util.getDataUriForString(/* mimeType= */ "text/xml", /* data= */ adsResponse));
} else {
throw new IllegalStateException();
}
......@@ -871,6 +874,7 @@ public final class ImaAdsLoader
if (configuration.applicationAdErrorListener != null) {
adsLoader.removeAdErrorListener(configuration.applicationAdErrorListener);
}
adsLoader.release();
}
imaPausedContent = false;
imaAdState = IMA_AD_STATE_NONE;
......@@ -1118,6 +1122,10 @@ public final class ImaAdsLoader
private void updateAdProgress() {
VideoProgressUpdate videoProgressUpdate = getAdVideoProgressUpdate();
if (configuration.debugModeEnabled) {
Log.d(TAG, "Ad progress: " + ImaUtil.getStringForVideoProgressUpdate(videoProgressUpdate));
}
AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onAdProgress(adMediaInfo, videoProgressUpdate);
......@@ -1211,17 +1219,31 @@ public final class ImaAdsLoader
if (imaAdInfo != null) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(imaAdInfo.adGroupIndex);
updateAdPlaybackState();
} else if (adPlaybackState.adGroupCount == 1 && adPlaybackState.adGroupTimesUs[0] == 0) {
// For incompatible VPAID ads with one preroll, content is resumed immediately. In this case
// we haven't received ad info (the ad never loaded), but there is only one ad group to skip.
adPlaybackState = adPlaybackState.withSkippedAdGroup(/* adGroupIndex= */ 0);
updateAdPlaybackState();
} else {
// Mark any ads for the current/reported player position that haven't loaded as being in the
// error state, to force resuming content. This includes VPAID ads that never load.
long playerPositionUs;
if (player != null) {
playerPositionUs = C.msToUs(getContentPeriodPositionMs(player, timeline, period));
} else if (!VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(lastContentProgress)) {
// Playback is backgrounded so use the last reported content position.
playerPositionUs = C.msToUs(lastContentProgress.getCurrentTimeMs());
} else {
return;
}
int adGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(
playerPositionUs, C.msToUs(contentDurationMs));
if (adGroupIndex != C.INDEX_UNSET) {
markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex);
}
}
}
private void handlePlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
if (playingAd && imaAdState == IMA_AD_STATE_PLAYING) {
if (!bufferingAd && playbackState == Player.STATE_BUFFERING) {
bufferingAd = true;
AdMediaInfo adMediaInfo = checkNotNull(imaAdMediaInfo);
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onBuffering(adMediaInfo);
......@@ -1282,13 +1304,18 @@ public final class ImaAdsLoader
if (adMediaInfo == null) {
Log.w(TAG, "onEnded without ad media info");
} else {
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded(adMediaInfo);
@Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo);
if (playingAdIndexInAdGroup == C.INDEX_UNSET
|| (adInfo != null && adInfo.adIndexInAdGroup < playingAdIndexInAdGroup)) {
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onEnded(adMediaInfo);
}
if (configuration.debugModeEnabled) {
Log.d(
TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
}
}
}
if (configuration.debugModeEnabled) {
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
}
}
if (!sentContentComplete && !wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) {
int adGroupIndex = player.getCurrentAdGroupIndex();
......@@ -1716,15 +1743,9 @@ public final class ImaAdsLoader
public VideoProgressUpdate getContentProgress() {
VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate();
if (configuration.debugModeEnabled) {
if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) {
Log.d(TAG, "Content progress: not ready");
} else {
Log.d(
TAG,
Util.formatInvariant(
"Content progress: %.1f of %.1f s",
videoProgressUpdate.getCurrentTime(), videoProgressUpdate.getDuration()));
}
Log.d(
TAG,
"Content progress: " + ImaUtil.getStringForVideoProgressUpdate(videoProgressUpdate));
}
if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) {
......@@ -1893,7 +1914,9 @@ public final class ImaAdsLoader
private static final class DefaultImaFactory implements ImaUtil.ImaFactory {
@Override
public ImaSdkSettings createImaSdkSettings() {
return ImaSdkFactory.getInstance().createImaSdkSettings();
ImaSdkSettings settings = ImaSdkFactory.getInstance().createImaSdkSettings();
settings.setLanguage(getImaLanguageCodeForDefaultLocale());
return settings;
}
@Override
......@@ -1934,5 +1957,17 @@ public final class ImaAdsLoader
return ImaSdkFactory.getInstance()
.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
}
/**
* Returns a language code that's suitable for passing to {@link ImaSdkSettings#setLanguage} and
* corresponds to the device's {@link Locale#getDefault() default Locale}. IMA will fall back to
* its default language code ("en") if the value returned is unsupported.
*/
// TODO: It may be possible to define a better mapping onto IMA's supported language codes. See:
// https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/localization.
// [Internal ref: b/174042000] will help if implemented.
private static String getImaLanguageCodeForDefaultLocale() {
return Util.splitAtFirst(Util.getSystemLanguageCodes()[0], "-")[0];
}
}
}
......@@ -33,6 +33,7 @@ import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
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.VideoProgressUpdate;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo;
......@@ -202,5 +203,16 @@ import java.util.Set;
|| adError.getErrorCode() == AdError.AdErrorCode.UNKNOWN_ERROR;
}
/** Returns a human-readable representation of a video progress update. */
public static String getStringForVideoProgressUpdate(VideoProgressUpdate videoProgressUpdate) {
if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) {
return "not ready";
} else {
return Util.formatInvariant(
"%d ms of %d ms",
videoProgressUpdate.getCurrentTimeMs(), videoProgressUpdate.getDurationMs());
}
}
private ImaUtil() {}
}
......@@ -78,10 +78,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}.
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The adapter calls
* {@link ControlDispatcher#dispatchPrepare(Player)} instead of {@link
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the adapter
* uses by default, calls {@link Player#prepare()}. If you wish to customize this behaviour,
* you can provide a custom implementation of {@link
* ControlDispatcher#dispatchPrepare(Player)}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
this.playbackPreparer = playbackPreparer;
}
......@@ -167,11 +172,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
return player.getPlaybackState() == Player.STATE_IDLE ? -1 : player.getCurrentPosition();
}
// Calls deprecated method to provide backwards compatibility.
@SuppressWarnings("deprecation")
@Override
public void play() {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
} else {
controlDispatcher.dispatchPrepare(player);
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.ext.media2;
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_IDLE;
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_PAUSED;
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_PLAYING;
import static androidx.media2.common.SessionPlayer.PlayerResult.RESULT_INFO_SKIPPED;
......@@ -60,6 +61,7 @@ import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
......@@ -762,13 +764,11 @@ public class SessionPlayerConnectorTest {
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_setsPlaylistAndCurrentMediaItem() throws Exception {
List<MediaItem> playlist = TestUtils.createPlaylist(10);
CountDownLatch onCurrentMediaItemChangedLatch = new CountDownLatch(1);
sessionPlayerConnector.registerPlayerCallback(
executor, new PlayerCallbackForPlaylist(playlist, onCurrentMediaItemChangedLatch));
PlayerCallbackForPlaylist callback = new PlayerCallbackForPlaylist(playlist, 1);
sessionPlayerConnector.registerPlayerCallback(executor, callback);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, null));
assertThat(onCurrentMediaItemChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS))
.isTrue();
assertThat(callback.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
assertThat(sessionPlayerConnector.getPlaylist()).isEqualTo(playlist);
assertThat(sessionPlayerConnector.getCurrentMediaItem()).isEqualTo(playlist.get(0));
......@@ -777,6 +777,32 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylistAndRemoveAllPlaylistItem_playerStateBecomesIdle() throws Exception {
List<MediaItem> playlist = new ArrayList<>();
playlist.add(TestUtils.createMediaItem(R.raw.video_1));
PlayerCallbackForPlaylist callback =
new PlayerCallbackForPlaylist(playlist, 2) {
@Override
public void onPlayerStateChanged(@NonNull SessionPlayer player, int playerState) {
countDown();
}
};
sessionPlayerConnector.registerPlayerCallback(executor, callback);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, null));
assertPlayerResultSuccess(sessionPlayerConnector.prepare());
assertThat(callback.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(PLAYER_STATE_PAUSED);
callback.resetLatch(1);
assertPlayerResultSuccess(sessionPlayerConnector.removePlaylistItem(0));
assertThat(callback.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(PLAYER_STATE_IDLE);
}
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
List<MediaItem> playlist = TestUtils.createPlaylist(10);
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(2);
......@@ -826,7 +852,6 @@ public class SessionPlayerConnectorTest {
}
}
});
sessionPlayerConnector.setPlaylist(playlistToSessionPlayer, /* metadata= */ null);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(() -> playerTestRule.getSimpleExoPlayer().setMediaItems(exoMediaItems));
assertThat(onPlaylistChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
......@@ -959,14 +984,12 @@ public class SessionPlayerConnectorTest {
int listSize = 2;
List<MediaItem> playlist = TestUtils.createPlaylist(listSize);
CountDownLatch onCurrentMediaItemChangedLatch = new CountDownLatch(1);
sessionPlayerConnector.registerPlayerCallback(
executor, new PlayerCallbackForPlaylist(playlist, onCurrentMediaItemChangedLatch));
PlayerCallbackForPlaylist callback = new PlayerCallbackForPlaylist(playlist, 1);
sessionPlayerConnector.registerPlayerCallback(executor, callback);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, null));
assertThat(sessionPlayerConnector.getCurrentMediaItemIndex()).isEqualTo(0);
assertThat(onCurrentMediaItemChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS))
.isTrue();
assertThat(callback.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
}
@Test
......@@ -1194,16 +1217,15 @@ public class SessionPlayerConnectorTest {
int listSize = playlist.size();
// Any value more than list size + 1, to see repeat mode with the recorded video.
CountDownLatch onCurrentMediaItemChangedLatch = new CountDownLatch(listSize + 2);
CopyOnWriteArrayList<MediaItem> currentMediaItemChanges = new CopyOnWriteArrayList<>();
PlayerCallbackForPlaylist callback =
new PlayerCallbackForPlaylist(playlist, onCurrentMediaItemChangedLatch) {
new PlayerCallbackForPlaylist(playlist, listSize + 2) {
@Override
public void onCurrentMediaItemChanged(
@NonNull SessionPlayer player, @NonNull MediaItem item) {
super.onCurrentMediaItemChanged(player, item);
currentMediaItemChanges.add(item);
onCurrentMediaItemChangedLatch.countDown();
countDown();
}
@Override
......@@ -1224,7 +1246,7 @@ public class SessionPlayerConnectorTest {
assertWithMessage(
"Current media item didn't change as expected. Actual changes were %s",
currentMediaItemChanges)
.that(onCurrentMediaItemChangedLatch.await(PLAYBACK_COMPLETED_WAIT_TIME_MS, MILLISECONDS))
.that(callback.await(PLAYBACK_COMPLETED_WAIT_TIME_MS, MILLISECONDS))
.isTrue();
int expectedMediaItemIndex = 0;
......@@ -1286,9 +1308,9 @@ public class SessionPlayerConnectorTest {
private List<MediaItem> playlist;
private CountDownLatch onCurrentMediaItemChangedLatch;
PlayerCallbackForPlaylist(List<MediaItem> playlist, CountDownLatch latch) {
PlayerCallbackForPlaylist(List<MediaItem> playlist, int count) {
this.playlist = playlist;
onCurrentMediaItemChangedLatch = latch;
onCurrentMediaItemChangedLatch = new CountDownLatch(count);
}
@Override
......@@ -1297,5 +1319,17 @@ public class SessionPlayerConnectorTest {
assertThat(sessionPlayerConnector.getCurrentMediaItemIndex()).isEqualTo(currentIndex);
onCurrentMediaItemChangedLatch.countDown();
}
public void resetLatch(int count) {
onCurrentMediaItemChangedLatch = new CountDownLatch(count);
}
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
return onCurrentMediaItemChangedLatch.await(timeout, unit);
}
public void countDown() {
onCurrentMediaItemChangedLatch.countDown();
}
}
}
......@@ -559,12 +559,16 @@ public final class SessionPlayerConnector extends SessionPlayer {
}
}
// TODO: Remove this suppress warnings and call onCurrentMediaItemChanged with a null item
// once AndroidX media2 1.2.0 is released
@SuppressWarnings("nullness:argument.type.incompatible")
private void handlePlaylistChangedOnHandler() {
List<MediaItem> currentPlaylist = player.getPlaylist();
MediaMetadata playlistMetadata = player.getPlaylistMetadata();
MediaItem currentMediaItem = player.getCurrentMediaItem();
boolean notifyCurrentMediaItem = !ObjectsCompat.equals(this.currentMediaItem, currentMediaItem);
boolean notifyCurrentMediaItem =
!ObjectsCompat.equals(this.currentMediaItem, currentMediaItem) && currentMediaItem != null;
this.currentMediaItem = currentMediaItem;
long currentPosition = getCurrentPosition();
......@@ -573,9 +577,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
callback.onPlaylistChanged(
SessionPlayerConnector.this, currentPlaylist, playlistMetadata);
if (notifyCurrentMediaItem) {
Assertions.checkNotNull(
currentMediaItem, "PlaylistManager#currentMediaItem() cannot be changed to null");
callback.onCurrentMediaItemChanged(SessionPlayerConnector.this, currentMediaItem);
// Workaround for MediaSession's issue that current media item change isn't propagated
......
......@@ -1147,6 +1147,8 @@ public final class MediaSessionConnector {
if (player.getPlaybackState() == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.onPrepare(/* playWhenReady= */ true);
} else {
controlDispatcher.dispatchPrepare(player);
}
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
......
......@@ -242,6 +242,11 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
responseBody = Assertions.checkNotNull(response.body());
responseByteStream = responseBody.byteStream();
} catch (IOException e) {
@Nullable String message = e.getMessage();
if (message != null
&& Util.toLowerInvariant(message).matches("cleartext communication.*not permitted.*")) {
throw new CleartextNotPermittedException(e, dataSpec);
}
throw new HttpDataSourceException(
"Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
}
......
......@@ -7,3 +7,12 @@
# From https://github.com/google/guava/wiki/UsingProGuardWithGuava
-dontwarn java.lang.ClassValue
-dontwarn java.lang.SafeVarargs
-dontwarn javax.lang.model.element.Modifier
-dontwarn sun.misc.Unsafe
# Don't warn about Guava's compile-only dependencies.
# These lines are needed for ProGuard but not R8.
-dontwarn com.google.errorprone.annotations.**
-dontwarn com.google.j2objc.annotations.**
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
......@@ -253,8 +253,7 @@ public final class C {
/**
* Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link
* #STREAM_TYPE_DTMF}, {@link #STREAM_TYPE_MUSIC}, {@link #STREAM_TYPE_NOTIFICATION}, {@link
* #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM}, {@link #STREAM_TYPE_VOICE_CALL} or {@link
* #STREAM_TYPE_USE_DEFAULT}.
* #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM} or {@link #STREAM_TYPE_VOICE_CALL}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
......@@ -265,8 +264,7 @@ public final class C {
STREAM_TYPE_NOTIFICATION,
STREAM_TYPE_RING,
STREAM_TYPE_SYSTEM,
STREAM_TYPE_VOICE_CALL,
STREAM_TYPE_USE_DEFAULT
STREAM_TYPE_VOICE_CALL
})
public @interface StreamType {}
/**
......@@ -297,13 +295,7 @@ public final class C {
* @see AudioManager#STREAM_VOICE_CALL
*/
public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
/**
* @see AudioManager#USE_DEFAULT_STREAM_TYPE
*/
public static final int STREAM_TYPE_USE_DEFAULT = AudioManager.USE_DEFAULT_STREAM_TYPE;
/**
* The default stream type used by audio renderers.
*/
/** The default stream type used by audio renderers. Equal to {@link #STREAM_TYPE_MUSIC}. */
public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
/**
......
......@@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo {
/** 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.
public static final String VERSION = "2.12.1";
public static final String VERSION = "2.12.2";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.12.1";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.12.2";
/**
* The version of the library expressed as an integer, for example 1002003.
......@@ -44,7 +44,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2012001;
public static final int VERSION_INT = 2012002;
/** The default user agent for requests made by the library. */
public static final String DEFAULT_USER_AGENT =
......
......@@ -216,7 +216,7 @@ public final class MediaItem {
}
/**
* Sets the optional DRM license server URI. If this URI is set, the {@link
* Sets the optional default DRM license server URI. If this URI is set, the {@link
* DrmConfiguration#uuid} needs to be specified as well.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM license server URI is used to
......@@ -228,7 +228,7 @@ public final class MediaItem {
}
/**
* Sets the optional DRM license server URI. If this URI is set, the {@link
* Sets the optional default DRM license server URI. If this URI is set, the {@link
* DrmConfiguration#uuid} needs to be specified as well.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM license server URI is used to
......@@ -279,8 +279,8 @@ public final class MediaItem {
}
/**
* Sets whether to use the DRM license server URI of the media item for key requests that
* include their own DRM license server URI.
* Sets whether to force use the default DRM license server URI even if the media specifies its
* own DRM license server URI.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM force default license flag is
* used to create a {@link PlaybackProperties} object. Otherwise it will be ignored.
......@@ -482,8 +482,8 @@ public final class MediaItem {
public final UUID uuid;
/**
* Optional DRM license server {@link Uri}. If {@code null} then the DRM license server must be
* specified by the media.
* Optional default DRM license server {@link Uri}. If {@code null} then the DRM license server
* must be specified by the media.
*/
@Nullable public final Uri licenseUri;
......@@ -500,8 +500,8 @@ public final class MediaItem {
public final boolean playClearContentWithoutKey;
/**
* Sets whether to use the DRM license server URI of the media item for key requests that
* include their own DRM license server URI.
* Whether to force use of {@link #licenseUri} even if the media specifies its own DRM license
* server URI.
*/
public final boolean forceDefaultLicenseUri;
......@@ -519,6 +519,7 @@ public final class MediaItem {
boolean playClearContentWithoutKey,
List<Integer> drmSessionForClearTypes,
@Nullable byte[] keySetId) {
Assertions.checkArgument(!(forceDefaultLicenseUri && licenseUri == null));
this.uuid = uuid;
this.licenseUri = licenseUri;
this.requestHeaders = requestHeaders;
......
......@@ -271,7 +271,24 @@ public interface HttpDataSource extends DataSource {
this.dataSpec = dataSpec;
this.type = type;
}
}
/**
* Thrown when cleartext HTTP traffic is not permitted. For more information including how to
* enable cleartext traffic, see the <a
* href="https://exoplayer.dev/issues/cleartext-not-permitted">corresponding troubleshooting
* topic</a>.
*/
final class CleartextNotPermittedException extends HttpDataSourceException {
public CleartextNotPermittedException(IOException cause, DataSpec dataSpec) {
super(
"Cleartext HTTP traffic not permitted. See"
+ " https://exoplayer.dev/issues/cleartext-not-permitted",
cause,
dataSpec,
TYPE_OPEN);
}
}
/**
......@@ -285,7 +302,6 @@ public interface HttpDataSource extends DataSource {
super("Invalid content type: " + contentType, dataSpec, TYPE_OPEN);
this.contentType = contentType;
}
}
/**
......
......@@ -240,6 +240,50 @@ public final class MimeTypes {
}
/**
* Returns whether the given {@code codecs} string contains a codec which corresponds to the given
* {@code mimeType}.
*
* @param codecs An RFC 6381 codecs string.
* @param mimeType A MIME type to look for.
* @return Whether the given {@code codecs} string contains a codec which corresponds to the given
* {@code mimeType}.
*/
public static boolean containsCodecsCorrespondingToMimeType(
@Nullable String codecs, String mimeType) {
return getCodecsCorrespondingToMimeType(codecs, mimeType) != null;
}
/**
* Returns a subsequence of {@code codecs} containing the codec strings that correspond to the
* given {@code mimeType}. Returns null if {@code mimeType} is null, {@code codecs} is null, or
* {@code codecs} does not contain a codec that corresponds to {@code mimeType}.
*
* @param codecs An RFC 6381 codecs string.
* @param mimeType A MIME type to look for.
* @return A subsequence of {@code codecs} containing the codec strings that correspond to the
* given {@code mimeType}. Returns null if {@code mimeType} is null, {@code codecs} is null,
* or {@code codecs} does not contain a codec that corresponds to {@code mimeType}.
*/
@Nullable
public static String getCodecsCorrespondingToMimeType(
@Nullable String codecs, @Nullable String mimeType) {
if (codecs == null || mimeType == null) {
return null;
}
String[] codecList = Util.splitCodecs(codecs);
StringBuilder builder = new StringBuilder();
for (String codec : codecList) {
if (mimeType.equals(getMediaMimeType(codec))) {
if (builder.length() > 0) {
builder.append(",");
}
builder.append(codec);
}
}
return builder.length() > 0 ? builder.toString() : null;
}
/**
* Returns the first audio MIME type derived from an RFC 6381 codecs string.
*
* @param codecs An RFC 6381 codecs string.
......
......@@ -515,8 +515,8 @@ public final class ParsableByteArray {
* Reads a line of text.
*
* <p>A line is considered to be terminated by any one of a carriage return ('\r'), a line feed
* ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The system's default
* charset (UTF-8) is used. This method discards leading UTF-8 byte order marks, if present.
* ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The UTF-8 charset is
* used. This method discards leading UTF-8 byte order marks, if present.
*
* @return The line not including any line-termination characters, or null if the end of the data
* has already been reached.
......
......@@ -1485,6 +1485,18 @@ public final class Util {
+ ") " + ExoPlayerLibraryInfo.VERSION_SLASHY;
}
/** Returns the number of codec strings in {@code codecs} whose type matches {@code trackType}. */
public static int getCodecCountOfType(@Nullable String codecs, int trackType) {
String[] codecArray = splitCodecs(codecs);
int count = 0;
for (String codec : codecArray) {
if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) {
count++;
}
}
return count;
}
/**
* Returns a copy of {@code codecs} without the codecs whose track type doesn't match {@code
* trackType}.
......@@ -1677,7 +1689,6 @@ public final class Util {
return C.USAGE_ASSISTANCE_SONIFICATION;
case C.STREAM_TYPE_VOICE_CALL:
return C.USAGE_VOICE_COMMUNICATION;
case C.STREAM_TYPE_USE_DEFAULT:
case C.STREAM_TYPE_MUSIC:
default:
return C.USAGE_MEDIA;
......@@ -1698,7 +1709,6 @@ public final class Util {
return C.CONTENT_TYPE_SONIFICATION;
case C.STREAM_TYPE_VOICE_CALL:
return C.CONTENT_TYPE_SPEECH;
case C.STREAM_TYPE_USE_DEFAULT:
case C.STREAM_TYPE_MUSIC:
default:
return C.CONTENT_TYPE_MUSIC;
......
......@@ -29,6 +29,69 @@ import org.junit.runner.RunWith;
public final class MimeTypesTest {
@Test
public void containsCodecsCorrespondingToMimeType_returnsCorrectResult() {
assertThat(
MimeTypes.containsCodecsCorrespondingToMimeType(
/* codecs= */ "ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AAC))
.isTrue();
assertThat(
MimeTypes.containsCodecsCorrespondingToMimeType(
/* codecs= */ "ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AC3))
.isTrue();
assertThat(
MimeTypes.containsCodecsCorrespondingToMimeType(
/* codecs= */ "ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.VIDEO_H264))
.isTrue();
assertThat(
MimeTypes.containsCodecsCorrespondingToMimeType(
/* codecs= */ "unknown-codec,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AAC))
.isTrue();
assertThat(
MimeTypes.containsCodecsCorrespondingToMimeType(
/* codecs= */ "unknown-codec,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AC3))
.isFalse();
assertThat(
MimeTypes.containsCodecsCorrespondingToMimeType(
/* codecs= */ null, MimeTypes.AUDIO_AC3))
.isFalse();
assertThat(
MimeTypes.containsCodecsCorrespondingToMimeType(/* codecs= */ "", MimeTypes.AUDIO_AC3))
.isFalse();
}
@Test
public void getCodecsCorrespondingToMimeType_returnsCorrectResult() {
assertThat(
MimeTypes.getCodecsCorrespondingToMimeType(
/* codecs= */ "avc1.4D5015,ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AAC))
.isEqualTo("mp4a.40.2");
assertThat(
MimeTypes.getCodecsCorrespondingToMimeType(
/* codecs= */ "avc1.4D5015,ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.VIDEO_H264))
.isEqualTo("avc1.4D5015,avc1.4D4015");
assertThat(
MimeTypes.getCodecsCorrespondingToMimeType(
/* codecs= */ "avc1.4D5015,ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AC3))
.isEqualTo("ac-3");
assertThat(
MimeTypes.getCodecsCorrespondingToMimeType(
/* codecs= */ "unknown-codec,ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.AUDIO_AC3))
.isEqualTo("ac-3");
assertThat(
MimeTypes.getCodecsCorrespondingToMimeType(
/* codecs= */ "avc1.4D5015,ac-3,mp4a.40.2,avc1.4D4015", MimeTypes.VIDEO_H265))
.isNull();
assertThat(
MimeTypes.getCodecsCorrespondingToMimeType(
/* codecs= */ "avc1.4D5015,ac-3,mp4a.40.2,avc1.4D4015", null))
.isNull();
assertThat(MimeTypes.getCodecsCorrespondingToMimeType(/* codecs= */ null, MimeTypes.AUDIO_AAC))
.isNull();
}
@Test
public void isText_returnsCorrectResult() {
assertThat(MimeTypes.isText(MimeTypes.TEXT_VTT)).isTrue();
assertThat(MimeTypes.isText(MimeTypes.TEXT_SSA)).isTrue();
......
......@@ -27,6 +27,14 @@ import com.google.android.exoplayer2.Player.RepeatMode;
public interface ControlDispatcher {
/**
* Dispatches a {@link Player#prepare()} operation.
*
* @param player The {@link Player} to which the operation should be dispatched.
* @return True if the operation was dispatched. False if suppressed.
*/
boolean dispatchPrepare(Player player);
/**
* Dispatches a {@link Player#setPlayWhenReady(boolean)} operation.
*
* @param player The {@link Player} to which the operation should be dispatched.
......
......@@ -53,6 +53,12 @@ public class DefaultControlDispatcher implements ControlDispatcher {
}
@Override
public boolean dispatchPrepare(Player player) {
player.prepare();
return true;
}
@Override
public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) {
player.setPlayWhenReady(playWhenReady);
return true;
......
......@@ -15,9 +15,11 @@
*/
package com.google.android.exoplayer2;
/** Called to prepare a playback. */
/** @deprecated Use {@link ControlDispatcher} instead. */
@Deprecated
public interface PlaybackPreparer {
/** Called to prepare a playback. */
/** @deprecated Use {@link ControlDispatcher#dispatchPrepare(Player)} instead. */
@Deprecated
void preparePlayback();
}
......@@ -722,15 +722,21 @@ public interface Player {
@IntDef({REPEAT_MODE_OFF, REPEAT_MODE_ONE, REPEAT_MODE_ALL})
@interface RepeatMode {}
/**
* Normal playback without repetition.
* Normal playback without repetition. "Previous" and "Next" actions move to the previous and next
* windows respectively, and do nothing when there is no previous or next window to move to.
*/
int REPEAT_MODE_OFF = 0;
/**
* "Repeat One" mode to repeat the currently playing window infinitely.
* Repeats the currently playing window infinitely during ongoing playback. "Previous" and "Next"
* actions behave as they do in {@link #REPEAT_MODE_OFF}, moving to the previous and next windows
* respectively, and doing nothing when there is no previous or next window to move to.
*/
int REPEAT_MODE_ONE = 1;
/**
* "Repeat All" mode to repeat the entire timeline infinitely.
* Repeats the entire timeline infinitely. "Previous" and "Next" actions behave as they do in
* {@link #REPEAT_MODE_OFF}, but with looping at the ends so that "Previous" when playing the
* first window will move to the last window, and "Next" when playing the last window will move to
* the first window.
*/
int REPEAT_MODE_ALL = 2;
......@@ -1126,26 +1132,41 @@ public interface Player {
/**
* Returns whether a previous window exists, which may depend on the current repeat mode and
* whether shuffle mode is enabled.
*
* <p>Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when
* the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more
* details.
*/
boolean hasPrevious();
/**
* Seeks to the default position of the previous window in the timeline, which may depend on the
* current repeat mode and whether shuffle mode is enabled. Does nothing if {@link #hasPrevious()}
* is {@code false}.
* Seeks to the default position of the previous window, which may depend on the current repeat
* mode and whether shuffle mode is enabled. Does nothing if {@link #hasPrevious()} is {@code
* false}.
*
* <p>Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when
* the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more
* details.
*/
void previous();
/**
* Returns whether a next window exists, which may depend on the current repeat mode and whether
* shuffle mode is enabled.
*
* <p>Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when
* the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more
* details.
*/
boolean hasNext();
/**
* Seeks to the default position of the next window in the timeline, which may depend on the
* current repeat mode and whether shuffle mode is enabled. Does nothing if {@link #hasNext()} is
* {@code false}.
* Seeks to the default position of the next window, which may depend on the current repeat mode
* and whether shuffle mode is enabled. Does nothing if {@link #hasNext()} is {@code false}.
*
* <p>Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when
* the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more
* details.
*/
void next();
......@@ -1254,18 +1275,24 @@ public interface Player {
int getCurrentWindowIndex();
/**
* Returns the index of the next timeline window to be played, which may depend on the current
* repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window
* currently being played is the last window or if the {@link #getCurrentTimeline() current
* timeline} is empty.
* Returns the index of the window that will be played if {@link #next()} is called, which may
* depend on the current repeat mode and whether shuffle mode is enabled. Returns {@link
* C#INDEX_UNSET} if {@link #hasNext()} is {@code false}.
*
* <p>Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when
* the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more
* details.
*/
int getNextWindowIndex();
/**
* Returns the index of the previous timeline window to be played, which may depend on the current
* repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window
* currently being played is the first window or if the {@link #getCurrentTimeline() current
* timeline} is empty.
* Returns the index of the window that will be played if {@link #previous()} is called, which may
* depend on the current repeat mode and whether shuffle mode is enabled. Returns {@link
* C#INDEX_UNSET} if {@link #hasPrevious()} is {@code false}.
*
* <p>Note: When the repeat mode is {@link #REPEAT_MODE_ONE}, this method behaves the same as when
* the current repeat mode is {@link #REPEAT_MODE_OFF}. See {@link #REPEAT_MODE_ONE} for more
* details.
*/
int getPreviousWindowIndex();
......
......@@ -21,7 +21,9 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Handler;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
/** A manager that wraps {@link AudioManager} to control/listen audio stream volume. */
......@@ -37,6 +39,8 @@ import com.google.android.exoplayer2.util.Util;
void onStreamVolumeChanged(int streamVolume, boolean streamMuted);
}
private static final String TAG = "StreamVolumeManager";
// TODO(b/151280453): Replace the hidden intent action with an official one.
// Copied from AudioManager#VOLUME_CHANGED_ACTION
private static final String VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION";
......@@ -48,12 +52,11 @@ import com.google.android.exoplayer2.util.Util;
private final Handler eventHandler;
private final Listener listener;
private final AudioManager audioManager;
private final VolumeChangeReceiver receiver;
@Nullable private VolumeChangeReceiver receiver;
@C.StreamType private int streamType;
private int volume;
private boolean muted;
private boolean released;
/** Creates a manager. */
public StreamVolumeManager(Context context, Handler eventHandler, Listener listener) {
......@@ -68,9 +71,14 @@ import com.google.android.exoplayer2.util.Util;
volume = getVolumeFromManager(audioManager, streamType);
muted = getMutedFromManager(audioManager, streamType);
receiver = new VolumeChangeReceiver();
VolumeChangeReceiver receiver = new VolumeChangeReceiver();
IntentFilter filter = new IntentFilter(VOLUME_CHANGED_ACTION);
applicationContext.registerReceiver(receiver, filter);
try {
applicationContext.registerReceiver(receiver, filter);
this.receiver = receiver;
} catch (RuntimeException e) {
Log.w(TAG, "Error registering stream volume receiver", e);
}
}
/** Sets the audio stream type. */
......@@ -159,11 +167,14 @@ import com.google.android.exoplayer2.util.Util;
/** Releases the manager. It must be called when the manager is no longer required. */
public void release() {
if (released) {
return;
if (receiver != null) {
try {
applicationContext.unregisterReceiver(receiver);
} catch (RuntimeException e) {
Log.w(TAG, "Error unregistering stream volume receiver", e);
}
receiver = null;
}
applicationContext.unregisterReceiver(receiver);
released = true;
}
private void updateVolumeAndNotifyIfChanged() {
......@@ -177,7 +188,14 @@ import com.google.android.exoplayer2.util.Util;
}
private static int getVolumeFromManager(AudioManager audioManager, @C.StreamType int streamType) {
return audioManager.getStreamVolume(streamType);
// AudioManager#getStreamVolume(int) throws an exception on some devices. See
// https://github.com/google/ExoPlayer/issues/8191.
try {
return audioManager.getStreamVolume(streamType);
} catch (RuntimeException e) {
Log.w(TAG, "Could not retrieve stream volume for stream type " + streamType, e);
return audioManager.getStreamMaxVolume(streamType);
}
}
private static boolean getMutedFromManager(
......@@ -185,7 +203,7 @@ import com.google.android.exoplayer2.util.Util;
if (Util.SDK_INT >= 23) {
return audioManager.isStreamMute(streamType);
} else {
return audioManager.getStreamVolume(streamType) == 0;
return getVolumeFromManager(audioManager, streamType) == 0;
}
}
......
......@@ -113,8 +113,15 @@ public final class DefaultAudioSink implements AudioSink {
boolean applySkipSilenceEnabled(boolean skipSilenceEnabled);
/**
* Scales the specified playout duration to take into account speedup due to audio processing,
* returning an input media duration, in arbitrary units.
* Returns the media duration corresponding to the specified playout duration, taking speed
* adjustment due to audio processing into account.
*
* <p>The scaling performed by this method will use the actual playback speed achieved by the
* audio processor chain, on average, since it was last flushed. This may differ very slightly
* from the target playback speed.
*
* @param playoutDuration The playout duration to scale.
* @return The corresponding media duration, in the same units as {@code duration}.
*/
long getMediaDuration(long playoutDuration);
......@@ -173,9 +180,9 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters) {
float speed = sonicAudioProcessor.setSpeed(playbackParameters.speed);
float pitch = sonicAudioProcessor.setPitch(playbackParameters.pitch);
return new PlaybackParameters(speed, pitch);
sonicAudioProcessor.setSpeed(playbackParameters.speed);
sonicAudioProcessor.setPitch(playbackParameters.pitch);
return playbackParameters;
}
@Override
......@@ -186,7 +193,7 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public long getMediaDuration(long playoutDuration) {
return sonicAudioProcessor.scaleDurationForSpeedup(playoutDuration);
return sonicAudioProcessor.getMediaDuration(playoutDuration);
}
@Override
......@@ -1369,21 +1376,33 @@ public final class DefaultAudioSink implements AudioSink {
mediaPositionParameters = mediaPositionParametersCheckpoints.remove();
}
long playoutDurationSinceLastCheckpoint =
long playoutDurationSinceLastCheckpointUs =
positionUs - mediaPositionParameters.audioTrackPositionUs;
if (!mediaPositionParameters.playbackParameters.equals(PlaybackParameters.DEFAULT)) {
if (mediaPositionParametersCheckpoints.isEmpty()) {
playoutDurationSinceLastCheckpoint =
audioProcessorChain.getMediaDuration(playoutDurationSinceLastCheckpoint);
} else {
// Playing data at a previous playback speed, so fall back to multiplying by the speed.
playoutDurationSinceLastCheckpoint =
Util.getMediaDurationForPlayoutDuration(
playoutDurationSinceLastCheckpoint,
mediaPositionParameters.playbackParameters.speed);
}
if (mediaPositionParameters.playbackParameters.equals(PlaybackParameters.DEFAULT)) {
return mediaPositionParameters.mediaTimeUs + playoutDurationSinceLastCheckpointUs;
} else if (mediaPositionParametersCheckpoints.isEmpty()) {
long mediaDurationSinceLastCheckpointUs =
audioProcessorChain.getMediaDuration(playoutDurationSinceLastCheckpointUs);
return mediaPositionParameters.mediaTimeUs + mediaDurationSinceLastCheckpointUs;
} else {
// The processor chain has been configured with new parameters, but we're still playing audio
// that was processed using previous parameters. We can't scale the playout duration using the
// processor chain in this case, so we fall back to scaling using the previous parameters'
// target speed instead. Since the processor chain may not have achieved the target speed
// precisely, we scale the duration to the next checkpoint (which will always be small) rather
// than the duration from the previous checkpoint (which may be arbitrarily large). This
// limits the amount of error that can be introduced due to a difference between the target
// and actual speeds.
MediaPositionParameters nextMediaPositionParameters =
mediaPositionParametersCheckpoints.getFirst();
long playoutDurationUntilNextCheckpointUs =
nextMediaPositionParameters.audioTrackPositionUs - positionUs;
long mediaDurationUntilNextCheckpointUs =
Util.getMediaDurationForPlayoutDuration(
playoutDurationUntilNextCheckpointUs,
mediaPositionParameters.playbackParameters.speed);
return nextMediaPositionParameters.mediaTimeUs - mediaDurationUntilNextCheckpointUs;
}
return mediaPositionParameters.mediaTimeUs + playoutDurationSinceLastCheckpoint;
}
private long applySkipping(long positionUs) {
......
......@@ -97,6 +97,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private long currentPositionUs;
private boolean allowFirstBufferPositionDiscontinuity;
private boolean allowPositionDiscontinuity;
private boolean audioSinkNeedsReset;
private boolean experimentalKeepAudioTrackOnSeek;
......@@ -507,6 +508,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
protected void onDisabled() {
audioSinkNeedsReset = true;
try {
audioSink.flush();
} finally {
......@@ -523,7 +525,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
try {
super.onReset();
} finally {
audioSink.reset();
if (audioSinkNeedsReset) {
audioSinkNeedsReset = false;
audioSink.reset();
}
}
}
......
......@@ -84,6 +84,14 @@ import java.util.Arrays;
}
/**
* Returns the number of bytes that have been input, but will not be processed until more input
* data is provided.
*/
public int getPendingInputBytes() {
return inputFrameCount * channelCount * BYTES_PER_SAMPLE;
}
/**
* Queues remaining data from {@code buffer}, and advances its position by the number of bytes
* consumed.
*
......
......@@ -15,10 +15,11 @@
*/
package com.google.android.exoplayer2.audio;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
......@@ -36,10 +37,10 @@ public final class SonicAudioProcessor implements AudioProcessor {
private static final float CLOSE_THRESHOLD = 0.01f;
/**
* The minimum number of output bytes at which the speedup is calculated using the input/output
* byte counts, rather than using the current playback parameters speed.
* The minimum number of output bytes required for duration scaling to be calculated using the
* input and output byte counts, rather than using the current playback speed.
*/
private static final int MIN_BYTES_FOR_SPEEDUP_CALCULATION = 1024;
private static final int MIN_BYTES_FOR_DURATION_SCALING_CALCULATION = 1024;
private int pendingOutputSampleRate;
private float speed;
......@@ -74,35 +75,31 @@ public final class SonicAudioProcessor implements AudioProcessor {
}
/**
* Sets the playback speed. This method may only be called after draining data through the
* Sets the target playback speed. This method may only be called after draining data through the
* processor. The value returned by {@link #isActive()} may change, and the processor must be
* {@link #flush() flushed} before queueing more data.
*
* @param speed The requested new playback speed.
* @return The actual new playback speed.
* @param speed The target playback speed.
*/
public float setSpeed(float speed) {
public void setSpeed(float speed) {
if (this.speed != speed) {
this.speed = speed;
pendingSonicRecreation = true;
}
return speed;
}
/**
* Sets the playback pitch. This method may only be called after draining data through the
* Sets the target playback pitch. This method may only be called after draining data through the
* processor. The value returned by {@link #isActive()} may change, and the processor must be
* {@link #flush() flushed} before queueing more data.
*
* @param pitch The requested new pitch.
* @return The actual new pitch.
* @param pitch The target pitch.
*/
public float setPitch(float pitch) {
public void setPitch(float pitch) {
if (this.pitch != pitch) {
this.pitch = pitch;
pendingSonicRecreation = true;
}
return pitch;
}
/**
......@@ -118,23 +115,27 @@ public final class SonicAudioProcessor implements AudioProcessor {
}
/**
* Returns the specified duration scaled to take into account the speedup factor of this instance,
* in the same units as {@code duration}.
* Returns the media duration corresponding to the specified playout duration, taking speed
* adjustment into account.
*
* <p>The scaling performed by this method will use the actual playback speed achieved by the
* audio processor, on average, since it was last flushed. This may differ very slightly from the
* target playback speed.
*
* @param duration The duration to scale taking into account speedup.
* @return The specified duration scaled to take into account speedup, in the same units as
* {@code duration}.
* @param playoutDuration The playout duration to scale.
* @return The corresponding media duration, in the same units as {@code duration}.
*/
public long scaleDurationForSpeedup(long duration) {
if (outputBytes >= MIN_BYTES_FOR_SPEEDUP_CALCULATION) {
public long getMediaDuration(long playoutDuration) {
if (outputBytes >= MIN_BYTES_FOR_DURATION_SCALING_CALCULATION) {
long processedInputBytes = inputBytes - checkNotNull(sonic).getPendingInputBytes();
return outputAudioFormat.sampleRate == inputAudioFormat.sampleRate
? Util.scaleLargeTimestamp(duration, inputBytes, outputBytes)
? Util.scaleLargeTimestamp(playoutDuration, processedInputBytes, outputBytes)
: Util.scaleLargeTimestamp(
duration,
inputBytes * outputAudioFormat.sampleRate,
playoutDuration,
processedInputBytes * outputAudioFormat.sampleRate,
outputBytes * inputAudioFormat.sampleRate);
} else {
return (long) ((double) speed * duration);
return (long) ((double) speed * playoutDuration);
}
}
......@@ -164,7 +165,7 @@ public final class SonicAudioProcessor implements AudioProcessor {
@Override
public void queueInput(ByteBuffer inputBuffer) {
Sonic sonic = Assertions.checkNotNull(this.sonic);
Sonic sonic = checkNotNull(this.sonic);
if (inputBuffer.hasRemaining()) {
ShortBuffer shortBuffer = inputBuffer.asShortBuffer();
int inputSize = inputBuffer.remaining();
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.drm;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
......@@ -27,6 +28,7 @@ import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCode
import com.google.android.exoplayer2.upstream.StatsDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
......@@ -39,29 +41,35 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
private static final int MAX_MANUAL_REDIRECTS = 5;
private final HttpDataSource.Factory dataSourceFactory;
private final String defaultLicenseUrl;
@Nullable private final String defaultLicenseUrl;
private final boolean forceDefaultLicenseUrl;
private final Map<String, String> keyRequestProperties;
/**
* @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
* their own license URL.
* their own license URL. May be {@code null} if it's known that all key requests will specify
* their own URLs.
* @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
*/
public HttpMediaDrmCallback(String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) {
public HttpMediaDrmCallback(
@Nullable String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) {
this(defaultLicenseUrl, /* forceDefaultLicenseUrl= */ false, dataSourceFactory);
}
/**
* @param defaultLicenseUrl The default license URL. Used for key requests that do not specify
* their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is
* set to true.
* @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that
* include their own license URL.
* their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is set to
* true. May be {@code null} if {@code forceDefaultLicenseUrl} is {@code false} and if it's
* known that all key requests will specify their own URLs.
* @param forceDefaultLicenseUrl Whether to force use of {@code defaultLicenseUrl} for key
* requests that include their own license URL.
* @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
*/
public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl,
public HttpMediaDrmCallback(
@Nullable String defaultLicenseUrl,
boolean forceDefaultLicenseUrl,
HttpDataSource.Factory dataSourceFactory) {
Assertions.checkArgument(!(forceDefaultLicenseUrl && TextUtils.isEmpty(defaultLicenseUrl)));
this.dataSourceFactory = dataSourceFactory;
this.defaultLicenseUrl = defaultLicenseUrl;
this.forceDefaultLicenseUrl = forceDefaultLicenseUrl;
......@@ -121,6 +129,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
url = defaultLicenseUrl;
}
if (TextUtils.isEmpty(url)) {
throw new MediaDrmCallbackException(
new DataSpec.Builder().setUri(Uri.EMPTY).build(),
Uri.EMPTY,
/* responseHeaders= */ ImmutableMap.of(),
/* bytesLoaded= */ 0,
/* cause= */ new IllegalStateException("No license URL"));
}
Map<String, String> requestProperties = new HashMap<>();
// Add standard request properties for supported schemes.
String contentType = C.PLAYREADY_UUID.equals(uuid) ? "text/xml"
......
......@@ -38,6 +38,7 @@ public final class ProgressiveDownloader implements Downloader {
private final Executor executor;
private final DataSpec dataSpec;
private final CacheDataSource dataSource;
private final CacheWriter cacheWriter;
@Nullable private final PriorityTaskManager priorityTaskManager;
@Nullable private ProgressListener progressListener;
......@@ -101,6 +102,15 @@ public final class ProgressiveDownloader implements Downloader {
.setFlags(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION)
.build();
dataSource = cacheDataSourceFactory.createDataSourceForDownloading();
@SuppressWarnings("methodref.receiver.bound.invalid")
CacheWriter.ProgressListener progressListener = this::onProgress;
cacheWriter =
new CacheWriter(
dataSource,
dataSpec,
/* allowShortContent= */ false,
/* temporaryBuffer= */ null,
progressListener);
priorityTaskManager = cacheDataSourceFactory.getUpstreamPriorityTaskManager();
}
......@@ -108,28 +118,19 @@ public final class ProgressiveDownloader implements Downloader {
public void download(@Nullable ProgressListener progressListener)
throws IOException, InterruptedException {
this.progressListener = progressListener;
if (downloadRunnable == null) {
CacheWriter cacheWriter =
new CacheWriter(
dataSource,
dataSpec,
/* allowShortContent= */ false,
/* temporaryBuffer= */ null,
this::onProgress);
downloadRunnable =
new RunnableFutureTask<Void, IOException>() {
@Override
protected Void doWork() throws IOException {
cacheWriter.cache();
return null;
}
downloadRunnable =
new RunnableFutureTask<Void, IOException>() {
@Override
protected Void doWork() throws IOException {
cacheWriter.cache();
return null;
}
@Override
protected void cancelWork() {
cacheWriter.cancel();
}
};
}
@Override
protected void cancelWork() {
cacheWriter.cancel();
}
};
if (priorityTaskManager != null) {
priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
......
......@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source;
import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaItem;
......@@ -68,7 +67,7 @@ public final class MediaSourceDrmHelper {
Assertions.checkNotNull(mediaItem.playbackProperties);
@Nullable
MediaItem.DrmConfiguration drmConfiguration = mediaItem.playbackProperties.drmConfiguration;
if (drmConfiguration == null || drmConfiguration.licenseUri == null || Util.SDK_INT < 18) {
if (drmConfiguration == null || Util.SDK_INT < 18) {
return DrmSessionManager.getDummyDrmSessionManager();
}
HttpDataSource.Factory dataSourceFactory =
......@@ -77,7 +76,7 @@ public final class MediaSourceDrmHelper {
: new DefaultHttpDataSourceFactory(userAgent != null ? userAgent : DEFAULT_USER_AGENT);
HttpMediaDrmCallback httpDrmCallback =
new HttpMediaDrmCallback(
castNonNull(drmConfiguration.licenseUri).toString(),
drmConfiguration.licenseUri == null ? null : drmConfiguration.licenseUri.toString(),
drmConfiguration.forceDefaultLicenseUri,
dataSourceFactory);
for (Map.Entry<String, String> entry : drmConfiguration.requestHeaders.entrySet()) {
......
......@@ -435,6 +435,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
pendingResetPositionUs = positionUs;
loadingFinished = false;
if (loader.isLoading()) {
// Discard as much as we can synchronously.
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.discardToEnd();
}
loader.cancelLoading();
} else {
loader.clearFatalError();
......
......@@ -894,6 +894,11 @@ public class SampleQueue implements TrackOutput {
if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
// We've found a suitable sample.
sampleCountToTarget = i;
if (timesUs[searchIndex] == timeUs) {
// Stop the search if we found a sample at the specified time to avoid returning a later
// sample with the same exactly matching timestamp.
break;
}
}
searchIndex++;
if (searchIndex == capacity) {
......
......@@ -315,6 +315,11 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
mediaChunks.clear();
nextNotifyPrimaryFormatMediaChunkIndex = 0;
if (loader.isLoading()) {
// Discard as much as we can synchronously.
primarySampleQueue.discardToEnd();
for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
embeddedSampleQueue.discardToEnd();
}
loader.cancelLoading();
} else {
loader.clearFatalError();
......
......@@ -31,6 +31,7 @@ import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Charsets;
......@@ -43,6 +44,8 @@ import java.util.List;
*/
public final class Tx3gDecoder extends SimpleSubtitleDecoder {
private static final String TAG = "Tx3gDecoder";
private static final char BOM_UTF16_BE = '\uFEFF';
private static final char BOM_UTF16_LE = '\uFFFE';
......@@ -185,6 +188,16 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder {
int fontFace = parsableByteArray.readUnsignedByte();
parsableByteArray.skipBytes(1); // font size
int colorRgba = parsableByteArray.readInt();
if (end > cueText.length()) {
Log.w(
TAG, "Truncating styl end (" + end + ") to cueText.length() (" + cueText.length() + ").");
end = cueText.length();
}
if (start >= end) {
Log.w(TAG, "Ignoring styl with start (" + start + ") >= end (" + end + ").");
return;
}
attachFontFace(cueText, fontFace, defaultFontFace, start, end, SPAN_PRIORITY_HIGH);
attachColor(cueText, colorRgba, defaultColorRgba, start, end, SPAN_PRIORITY_HIGH);
}
......
......@@ -306,6 +306,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
try {
connection = makeConnection(dataSpec);
} catch (IOException e) {
@Nullable String message = e.getMessage();
if (message != null
&& Util.toLowerInvariant(message).matches("cleartext http traffic.*not permitted.*")) {
throw new CleartextNotPermittedException(e, dataSpec);
}
throw new HttpDataSourceException(
"Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
}
......
......@@ -19,6 +19,7 @@ import static java.lang.Math.min;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.upstream.HttpDataSource.CleartextNotPermittedException;
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer2.upstream.Loader.UnexpectedLoaderException;
import java.io.FileNotFoundException;
......@@ -86,14 +87,16 @@ public class DefaultLoadErrorHandlingPolicy implements LoadErrorHandlingPolicy {
/**
* Retries for any exception that is not a subclass of {@link ParserException}, {@link
* FileNotFoundException} or {@link UnexpectedLoaderException}. The retry delay is calculated as
* {@code Math.min((errorCount - 1) * 1000, 5000)}.
* FileNotFoundException}, {@link CleartextNotPermittedException} or {@link
* UnexpectedLoaderException}. The retry delay is calculated as {@code Math.min((errorCount - 1) *
* 1000, 5000)}.
*/
@Override
public long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) {
IOException exception = loadErrorInfo.exception;
return exception instanceof ParserException
|| exception instanceof FileNotFoundException
|| exception instanceof CleartextNotPermittedException
|| exception instanceof UnexpectedLoaderException
? C.TIME_UNSET
: min((loadErrorInfo.errorCount - 1) * 1000, 5000);
......
/*
* 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.offline;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link ActionFile}. */
@SuppressWarnings("deprecation")
@RunWith(AndroidJUnit4.class)
public class ProgressiveDownloaderTest {
private File testDir;
private Cache downloadCache;
@Before
public void createDownloadCache() throws Exception {
testDir =
Util.createTempFile(
ApplicationProvider.getApplicationContext(), "ProgressiveDownloaderTest");
assertThat(testDir.delete()).isTrue();
assertThat(testDir.mkdirs()).isTrue();
DatabaseProvider databaseProvider = TestUtil.getInMemoryDatabaseProvider();
downloadCache = new SimpleCache(testDir, new NoOpCacheEvictor(), databaseProvider);
}
@After
public void deleteDownloadCache() {
downloadCache.release();
Util.recursiveDelete(testDir);
}
@Test
public void download_afterSingleFailure_succeeds() throws Exception {
Uri uri = Uri.parse("test:///test.mp4");
// Fake data has a built in failure after 10 bytes.
FakeDataSet data = new FakeDataSet();
data.newData(uri).appendReadData(10).appendReadError(new IOException()).appendReadData(20);
DataSource.Factory upstreamDataSource = new FakeDataSource.Factory().setFakeDataSet(data);
MediaItem mediaItem = MediaItem.fromUri(uri);
CacheDataSource.Factory cacheDataSourceFactory =
new CacheDataSource.Factory()
.setCache(downloadCache)
.setUpstreamDataSourceFactory(upstreamDataSource);
ProgressiveDownloader downloader = new ProgressiveDownloader(mediaItem, cacheDataSourceFactory);
TestProgressListener progressListener = new TestProgressListener();
// Failure expected after 10 bytes.
assertThrows(IOException.class, () -> downloader.download(progressListener));
assertThat(progressListener.bytesDownloaded).isEqualTo(10);
// Retry should succeed.
downloader.download(progressListener);
assertThat(progressListener.bytesDownloaded).isEqualTo(30);
}
private static final class TestProgressListener implements Downloader.ProgressListener {
public long bytesDownloaded;
@Override
public void onProgress(long contentLength, long bytesDownloaded, float percentDownloaded) {
this.bytesDownloaded = bytesDownloaded;
}
}
}
......@@ -862,6 +862,53 @@ public final class SampleQueueTest {
}
@Test
public void discardTo_withDuplicateTimestamps_discardsOnlyToFirstMatch() {
writeTestData(
DATA,
SAMPLE_SIZES,
SAMPLE_OFFSETS,
/* sampleTimestamps= */ new long[] {0, 1000, 1000, 1000, 2000, 2000, 2000, 2000},
SAMPLE_FORMATS,
/* sampleFlags= */ new int[] {
BUFFER_FLAG_KEY_FRAME,
0,
BUFFER_FLAG_KEY_FRAME,
BUFFER_FLAG_KEY_FRAME,
0,
0,
BUFFER_FLAG_KEY_FRAME,
BUFFER_FLAG_KEY_FRAME
});
// Discard to first keyframe exactly matching the specified time.
sampleQueue.discardTo(
/* timeUs= */ 1000, /* toKeyframe= */ true, /* stopAtReadPosition= */ false);
assertThat(sampleQueue.getFirstIndex()).isEqualTo(2);
// Do nothing when trying again.
sampleQueue.discardTo(
/* timeUs= */ 1000, /* toKeyframe= */ true, /* stopAtReadPosition= */ false);
sampleQueue.discardTo(
/* timeUs= */ 1000, /* toKeyframe= */ false, /* stopAtReadPosition= */ false);
assertThat(sampleQueue.getFirstIndex()).isEqualTo(2);
// Discard to first frame exactly matching the specified time.
sampleQueue.discardTo(
/* timeUs= */ 2000, /* toKeyframe= */ false, /* stopAtReadPosition= */ false);
assertThat(sampleQueue.getFirstIndex()).isEqualTo(4);
// Do nothing when trying again.
sampleQueue.discardTo(
/* timeUs= */ 2000, /* toKeyframe= */ false, /* stopAtReadPosition= */ false);
assertThat(sampleQueue.getFirstIndex()).isEqualTo(4);
// Discard to first keyframe at same timestamp.
sampleQueue.discardTo(
/* timeUs= */ 2000, /* toKeyframe= */ true, /* stopAtReadPosition= */ false);
assertThat(sampleQueue.getFirstIndex()).isEqualTo(6);
}
@Test
public void discardToDontStopAtReadPosition() {
writeTestData();
// Shouldn't discard anything.
......
......@@ -128,6 +128,8 @@ public class MatroskaExtractor implements Extractor {
private static final String CODEC_ID_FLAC = "A_FLAC";
private static final String CODEC_ID_ACM = "A_MS/ACM";
private static final String CODEC_ID_PCM_INT_LIT = "A_PCM/INT/LIT";
private static final String CODEC_ID_PCM_INT_BIG = "A_PCM/INT/BIG";
private static final String CODEC_ID_PCM_FLOAT = "A_PCM/FLOAT/IEEE";
private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8";
private static final String CODEC_ID_ASS = "S_TEXT/ASS";
private static final String CODEC_ID_VOBSUB = "S_VOBSUB";
......@@ -1743,36 +1745,43 @@ public class MatroskaExtractor implements Extractor {
}
private static boolean isCodecSupported(String codecId) {
return CODEC_ID_VP8.equals(codecId)
|| CODEC_ID_VP9.equals(codecId)
|| CODEC_ID_AV1.equals(codecId)
|| CODEC_ID_MPEG2.equals(codecId)
|| CODEC_ID_MPEG4_SP.equals(codecId)
|| CODEC_ID_MPEG4_ASP.equals(codecId)
|| CODEC_ID_MPEG4_AP.equals(codecId)
|| CODEC_ID_H264.equals(codecId)
|| CODEC_ID_H265.equals(codecId)
|| CODEC_ID_FOURCC.equals(codecId)
|| CODEC_ID_THEORA.equals(codecId)
|| CODEC_ID_OPUS.equals(codecId)
|| CODEC_ID_VORBIS.equals(codecId)
|| CODEC_ID_AAC.equals(codecId)
|| CODEC_ID_MP2.equals(codecId)
|| CODEC_ID_MP3.equals(codecId)
|| CODEC_ID_AC3.equals(codecId)
|| CODEC_ID_E_AC3.equals(codecId)
|| CODEC_ID_TRUEHD.equals(codecId)
|| CODEC_ID_DTS.equals(codecId)
|| CODEC_ID_DTS_EXPRESS.equals(codecId)
|| CODEC_ID_DTS_LOSSLESS.equals(codecId)
|| CODEC_ID_FLAC.equals(codecId)
|| CODEC_ID_ACM.equals(codecId)
|| CODEC_ID_PCM_INT_LIT.equals(codecId)
|| CODEC_ID_SUBRIP.equals(codecId)
|| CODEC_ID_ASS.equals(codecId)
|| CODEC_ID_VOBSUB.equals(codecId)
|| CODEC_ID_PGS.equals(codecId)
|| CODEC_ID_DVBSUB.equals(codecId);
switch (codecId) {
case CODEC_ID_VP8:
case CODEC_ID_VP9:
case CODEC_ID_AV1:
case CODEC_ID_MPEG2:
case CODEC_ID_MPEG4_SP:
case CODEC_ID_MPEG4_ASP:
case CODEC_ID_MPEG4_AP:
case CODEC_ID_H264:
case CODEC_ID_H265:
case CODEC_ID_FOURCC:
case CODEC_ID_THEORA:
case CODEC_ID_OPUS:
case CODEC_ID_VORBIS:
case CODEC_ID_AAC:
case CODEC_ID_MP2:
case CODEC_ID_MP3:
case CODEC_ID_AC3:
case CODEC_ID_E_AC3:
case CODEC_ID_TRUEHD:
case CODEC_ID_DTS:
case CODEC_ID_DTS_EXPRESS:
case CODEC_ID_DTS_LOSSLESS:
case CODEC_ID_FLAC:
case CODEC_ID_ACM:
case CODEC_ID_PCM_INT_LIT:
case CODEC_ID_PCM_INT_BIG:
case CODEC_ID_PCM_FLOAT:
case CODEC_ID_SUBRIP:
case CODEC_ID_ASS:
case CODEC_ID_VOBSUB:
case CODEC_ID_PGS:
case CODEC_ID_DVBSUB:
return true;
default:
return false;
}
}
/**
......@@ -2102,8 +2111,44 @@ public class MatroskaExtractor implements Extractor {
if (pcmEncoding == C.ENCODING_INVALID) {
pcmEncoding = Format.NO_VALUE;
mimeType = MimeTypes.AUDIO_UNKNOWN;
Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to "
+ mimeType);
Log.w(
TAG,
"Unsupported little endian PCM bit depth: "
+ audioBitDepth
+ ". Setting mimeType to "
+ mimeType);
}
break;
case CODEC_ID_PCM_INT_BIG:
mimeType = MimeTypes.AUDIO_RAW;
if (audioBitDepth == 8) {
pcmEncoding = C.ENCODING_PCM_8BIT;
} else if (audioBitDepth == 16) {
pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN;
} else {
pcmEncoding = Format.NO_VALUE;
mimeType = MimeTypes.AUDIO_UNKNOWN;
Log.w(
TAG,
"Unsupported big endian PCM bit depth: "
+ audioBitDepth
+ ". Setting mimeType to "
+ mimeType);
}
break;
case CODEC_ID_PCM_FLOAT:
mimeType = MimeTypes.AUDIO_RAW;
if (audioBitDepth == 32) {
pcmEncoding = C.ENCODING_PCM_FLOAT;
} else {
pcmEncoding = Format.NO_VALUE;
mimeType = MimeTypes.AUDIO_UNKNOWN;
Log.w(
TAG,
"Unsupported floating point PCM bit depth: "
+ audioBitDepth
+ ". Setting mimeType to "
+ mimeType);
}
break;
case CODEC_ID_SUBRIP:
......
......@@ -278,6 +278,9 @@ import java.util.List;
public static final int TYPE_TTML = 0x54544d4c;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_m1v_ = 0x6d317620;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_mp4v = 0x6d703476;
@SuppressWarnings("ConstantCaseForConstants")
......
......@@ -853,6 +853,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
if (childAtomType == Atom.TYPE_avc1
|| childAtomType == Atom.TYPE_avc3
|| childAtomType == Atom.TYPE_encv
|| childAtomType == Atom.TYPE_m1v_
|| childAtomType == Atom.TYPE_mp4v
|| childAtomType == Atom.TYPE_hvc1
|| childAtomType == Atom.TYPE_hev1
......@@ -993,8 +994,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
// drmInitData = null;
// }
@Nullable List<byte[]> initializationData = null;
@Nullable String mimeType = null;
if (atomType == Atom.TYPE_m1v_) {
mimeType = MimeTypes.VIDEO_MPEG;
}
@Nullable List<byte[]> initializationData = null;
@Nullable String codecs = null;
@Nullable byte[] projectionData = null;
@C.StereoMode
......
......@@ -199,10 +199,10 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
// exist. If we know from the codec attribute that they don't exist, then we can
// explicitly ignore them even if they're declared.
if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
if (!MimeTypes.containsCodecsCorrespondingToMimeType(codecs, MimeTypes.AUDIO_AAC)) {
payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM;
}
if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
if (!MimeTypes.containsCodecsCorrespondingToMimeType(codecs, MimeTypes.VIDEO_H264)) {
payloadReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;
}
}
......
......@@ -603,6 +603,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
}
}
String codecs = selectedPlaylistFormats[0].codecs;
int numberOfVideoCodecs = Util.getCodecCountOfType(codecs, C.TRACK_TYPE_VIDEO);
int numberOfAudioCodecs = Util.getCodecCountOfType(codecs, C.TRACK_TYPE_AUDIO);
boolean codecsStringAllowsChunklessPreparation =
numberOfAudioCodecs <= 1
&& numberOfVideoCodecs <= 1
&& numberOfAudioCodecs + numberOfVideoCodecs > 0;
HlsSampleStreamWrapper sampleStreamWrapper =
buildSampleStreamWrapper(
C.TRACK_TYPE_DEFAULT,
......@@ -614,18 +620,16 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
positionUs);
sampleStreamWrappers.add(sampleStreamWrapper);
manifestUrlIndicesPerWrapper.add(selectedVariantIndices);
if (allowChunklessPreparation && codecs != null) {
boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null;
boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != null;
if (allowChunklessPreparation && codecsStringAllowsChunklessPreparation) {
List<TrackGroup> muxedTrackGroups = new ArrayList<>();
if (variantsContainVideoCodecs) {
if (numberOfVideoCodecs > 0) {
Format[] videoFormats = new Format[selectedVariantsCount];
for (int i = 0; i < videoFormats.length; i++) {
videoFormats[i] = deriveVideoFormat(selectedPlaylistFormats[i]);
}
muxedTrackGroups.add(new TrackGroup(videoFormats));
if (variantsContainAudioCodecs
if (numberOfAudioCodecs > 0
&& (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) {
muxedTrackGroups.add(
new TrackGroup(
......@@ -640,7 +644,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
muxedTrackGroups.add(new TrackGroup(ccFormats.get(i)));
}
}
} else if (variantsContainAudioCodecs) {
} else /* numberOfAudioCodecs > 0 */ {
// Variants only contain audio.
Format[] audioFormats = new Format[selectedVariantsCount];
for (int i = 0; i < audioFormats.length; i++) {
......@@ -651,9 +655,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
/* isPrimaryTrackInVariant= */ true);
}
muxedTrackGroups.add(new TrackGroup(audioFormats));
} else {
// Variants contain codecs but no video or audio entries could be identified.
throw new IllegalArgumentException("Unexpected codecs attribute: " + codecs);
}
TrackGroup id3TrackGroup =
......@@ -693,7 +694,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
continue;
}
boolean renditionsHaveCodecs = true;
boolean codecStringsAllowChunklessPreparation = true;
scratchPlaylistUrls.clear();
scratchPlaylistFormats.clear();
scratchIndicesList.clear();
......@@ -704,7 +705,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
scratchIndicesList.add(renditionIndex);
scratchPlaylistUrls.add(rendition.url);
scratchPlaylistFormats.add(rendition.format);
renditionsHaveCodecs &= rendition.format.codecs != null;
codecStringsAllowChunklessPreparation &=
Util.getCodecCountOfType(rendition.format.codecs, C.TRACK_TYPE_AUDIO) == 1;
}
}
......@@ -720,7 +722,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
manifestUrlsIndicesPerWrapper.add(Ints.toArray(scratchIndicesList));
sampleStreamWrappers.add(sampleStreamWrapper);
if (allowChunklessPreparation && renditionsHaveCodecs) {
if (allowChunklessPreparation && codecStringsAllowChunklessPreparation) {
Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]);
sampleStreamWrapper.prepareWithMasterPlaylistInfo(
new TrackGroup[] {new TrackGroup(renditionFormats)}, /* primaryTrackGroupIndex= */ 0);
......
......@@ -490,6 +490,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
loadingFinished = false;
mediaChunks.clear();
if (loader.isLoading()) {
if (sampleQueuesBuilt) {
// Discard as much as we can synchronously.
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.discardToEnd();
}
}
loader.cancelLoading();
} else {
loader.clearFatalError();
......@@ -1390,7 +1396,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* Derives a track sample format from the corresponding format in the master playlist, and a
* sample format that may have been obtained from a chunk belonging to a different track.
* sample format that may have been obtained from a chunk belonging to a different track in the
* same track group.
*
* @param playlistFormat The format information obtained from the master playlist.
* @param sampleFormat The format information obtained from the samples.
......@@ -1405,8 +1412,22 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
}
int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType);
@Nullable String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType);
@Nullable String sampleMimeType = MimeTypes.getMediaMimeType(codecs);
@Nullable String sampleMimeType;
@Nullable String codecs;
if (Util.getCodecCountOfType(playlistFormat.codecs, sampleTrackType) == 1) {
// We can unequivocally map this track to a playlist variant because only one codec string
// matches this track's type.
codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType);
sampleMimeType = MimeTypes.getMediaMimeType(codecs);
} else {
// The variant assigns more than one codec string to this track. We choose whichever codec
// string matches the sample mime type. This can happen when different languages are encoded
// using different codecs.
codecs =
MimeTypes.getCodecsCorrespondingToMimeType(
playlistFormat.codecs, sampleFormat.sampleMimeType);
sampleMimeType = sampleFormat.sampleMimeType;
}
Format.Builder formatBuilder =
sampleFormat
......
......@@ -152,6 +152,15 @@ public class DefaultTimeBar extends View implements TimeBar {
/** Default color for played ad markers. */
public static final int DEFAULT_PLAYED_AD_MARKER_COLOR = 0x33FFFF00;
// LINT.IfChange
/** Vertical gravity for progress bar to be located at the center in the view. */
public static final int BAR_GRAVITY_CENTER = 0;
/** Vertical gravity for progress bar to be located at the bottom in the view. */
public static final int BAR_GRAVITY_BOTTOM = 1;
/** Vertical gravity for progress bar to be located at the top in the view. */
public static final int BAR_GRAVITY_TOP = 2;
// LINT.ThenChange(../../../../../../../../../ui/src/main/res/values/attrs.xml)
/** The threshold in dps above the bar at which touch events trigger fine scrub mode. */
private static final int FINE_SCRUB_Y_THRESHOLD_DP = -50;
/** The ratio by which times are reduced in fine scrub mode. */
......@@ -186,6 +195,7 @@ public class DefaultTimeBar extends View implements TimeBar {
@Nullable private final Drawable scrubberDrawable;
private final int barHeight;
private final int touchTargetHeight;
private final int barGravity;
private final int adMarkerWidth;
private final int scrubberEnabledSize;
private final int scrubberDisabledSize;
......@@ -286,6 +296,7 @@ public class DefaultTimeBar extends View implements TimeBar {
defaultBarHeight);
touchTargetHeight = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_touch_target_height,
defaultTouchTargetHeight);
barGravity = a.getInt(R.styleable.DefaultTimeBar_bar_gravity, BAR_GRAVITY_CENTER);
adMarkerWidth = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_ad_marker_width,
defaultAdMarkerWidth);
scrubberEnabledSize = a.getDimensionPixelSize(
......@@ -318,6 +329,7 @@ public class DefaultTimeBar extends View implements TimeBar {
} else {
barHeight = defaultBarHeight;
touchTargetHeight = defaultTouchTargetHeight;
barGravity = BAR_GRAVITY_CENTER;
adMarkerWidth = defaultAdMarkerWidth;
scrubberEnabledSize = defaultScrubberEnabledSize;
scrubberDisabledSize = defaultScrubberDisabledSize;
......@@ -659,7 +671,14 @@ public class DefaultTimeBar extends View implements TimeBar {
int barY = (height - touchTargetHeight) / 2;
int seekLeft = getPaddingLeft();
int seekRight = width - getPaddingRight();
int progressY = barY + (touchTargetHeight - barHeight) / 2;
int progressY;
if (barGravity == BAR_GRAVITY_BOTTOM) {
progressY = barY + touchTargetHeight - (getPaddingBottom() + scrubberPadding + barHeight / 2);
} else if (barGravity == BAR_GRAVITY_TOP) {
progressY = barY + getPaddingTop() + scrubberPadding - barHeight / 2;
} else {
progressY = barY + (touchTargetHeight - barHeight) / 2;
}
seekBounds.set(seekLeft, barY, seekRight, barY + touchTargetHeight);
progressBar.set(seekBounds.left + scrubberPadding, progressY,
seekBounds.right - scrubberPadding, progressY + barHeight);
......
......@@ -611,11 +611,15 @@ public class PlayerControlView extends FrameLayout {
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
* preparer.
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The view calls {@link
* ControlDispatcher#dispatchPrepare(Player)} instead of {@link
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the view
* uses by default, calls {@link Player#prepare()}. If you wish to customize this behaviour,
* you can provide a custom implementation of {@link
* ControlDispatcher#dispatchPrepare(Player)}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
this.playbackPreparer = playbackPreparer;
}
......@@ -1254,11 +1258,14 @@ public class PlayerControlView extends FrameLayout {
}
}
@SuppressWarnings("deprecation")
private void dispatchPlay(Player player) {
@State int state = player.getPlaybackState();
if (state == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
} else {
controlDispatcher.dispatchPrepare(player);
}
} else if (state == Player.STATE_ENDED) {
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
......
......@@ -983,11 +983,15 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
* preparer.
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The view calls {@link
* ControlDispatcher#dispatchPrepare(Player)} instead of {@link
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the view
* uses by default, calls {@link Player#prepare()}. If you wish to customize this behaviour,
* you can provide a custom implementation of {@link
* ControlDispatcher#dispatchPrepare(Player)}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
Assertions.checkStateNotNull(controller);
controller.setPlaybackPreparer(playbackPreparer);
......
......@@ -834,11 +834,15 @@ public class StyledPlayerControlView extends FrameLayout {
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
* preparer.
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The view calls {@link
* ControlDispatcher#dispatchPrepare(Player)} instead of {@link
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the view
* uses by default, calls {@link Player#prepare()}. If you wish to customize this behaviour,
* you can provide a custom implementation of {@link
* ControlDispatcher#dispatchPrepare(Player)}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
this.playbackPreparer = playbackPreparer;
}
......@@ -1698,11 +1702,14 @@ public class StyledPlayerControlView extends FrameLayout {
}
}
@SuppressWarnings("deprecation")
private void dispatchPlay(Player player) {
@State int state = player.getPlaybackState();
if (state == Player.STATE_IDLE) {
if (playbackPreparer != null) {
playbackPreparer.preparePlayback();
} else {
controlDispatcher.dispatchPrepare(player);
}
} else if (state == Player.STATE_ENDED) {
seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
......@@ -1920,7 +1927,7 @@ public class StyledPlayerControlView extends FrameLayout {
}
}
private class SettingViewHolder extends RecyclerView.ViewHolder {
private final class SettingViewHolder extends RecyclerView.ViewHolder {
private final TextView mainTextView;
private final TextView subTextView;
private final ImageView iconView;
......@@ -1930,8 +1937,7 @@ public class StyledPlayerControlView extends FrameLayout {
mainTextView = itemView.findViewById(R.id.exo_main_text);
subTextView = itemView.findViewById(R.id.exo_sub_text);
iconView = itemView.findViewById(R.id.exo_icon);
itemView.setOnClickListener(
v -> onSettingViewClicked(SettingViewHolder.this.getAdapterPosition()));
itemView.setOnClickListener(v -> onSettingViewClicked(getAdapterPosition()));
}
}
......@@ -1969,7 +1975,7 @@ public class StyledPlayerControlView extends FrameLayout {
}
}
private class SubSettingViewHolder extends RecyclerView.ViewHolder {
private final class SubSettingViewHolder extends RecyclerView.ViewHolder {
private final TextView textView;
private final View checkView;
......@@ -1977,8 +1983,7 @@ public class StyledPlayerControlView extends FrameLayout {
super(itemView);
textView = itemView.findViewById(R.id.exo_text);
checkView = itemView.findViewById(R.id.exo_check);
itemView.setOnClickListener(
v -> onSubSettingViewClicked(SubSettingViewHolder.this.getAdapterPosition()));
itemView.setOnClickListener(v -> onSubSettingViewClicked(getAdapterPosition()));
}
}
......
......@@ -45,6 +45,7 @@ import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
......@@ -978,11 +979,15 @@ public class StyledPlayerView extends FrameLayout implements AdsLoader.AdViewPro
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
* preparer.
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} instead. The view calls {@link
* ControlDispatcher#dispatchPrepare(Player)} instead of {@link
* PlaybackPreparer#preparePlayback()}. The {@link DefaultControlDispatcher} that the view
* uses by default, calls {@link Player#prepare()}. If you wish to customize this behaviour,
* you can provide a custom implementation of {@link
* ControlDispatcher#dispatchPrepare(Player)}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
Assertions.checkStateNotNull(controller);
controller.setPlaybackPreparer(playbackPreparer);
......
......@@ -289,10 +289,10 @@ public class TrackSelectionView extends LinearLayout {
(CheckedTextView) inflater.inflate(trackViewLayoutId, this, false);
trackView.setBackgroundResource(selectableItemBackgroundResourceId);
trackView.setText(trackNameProvider.getTrackName(trackInfos[trackIndex].format));
trackView.setTag(trackInfos[trackIndex]);
if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
== RendererCapabilities.FORMAT_HANDLED) {
trackView.setFocusable(true);
trackView.setTag(trackInfos[trackIndex]);
trackView.setOnClickListener(componentListener);
} else {
trackView.setFocusable(false);
......
......@@ -54,8 +54,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private @MonotonicNonNull SurfaceTexture surfaceTexture;
// Used by other threads only
private volatile @C.StreamType int defaultStereoMode;
private @C.StreamType int lastStereoMode;
@C.StereoMode private volatile int defaultStereoMode;
@C.StereoMode private int lastStereoMode;
@Nullable private byte[] lastProjectionData;
// Methods called on any thread.
......
......@@ -19,6 +19,7 @@
android:minWidth="@dimen/exo_setting_width"
android:minHeight="@dimen/exo_settings_height"
android:background="?android:attr/selectableItemBackground"
android:layoutDirection="locale"
android:orientation="horizontal">
<ImageView
......@@ -38,6 +39,7 @@
android:paddingEnd="4dp"
android:paddingRight="4dp"
android:gravity="center|start"
android:layoutDirection="locale"
android:orientation="vertical">
<TextView
......@@ -45,6 +47,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/exo_white"
android:textDirection="locale"
android:textSize="@dimen/exo_settings_main_text_size"/>
<TextView
......@@ -52,6 +55,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/exo_white_opacity_70"
android:textDirection="locale"
android:textSize="@dimen/exo_settings_sub_text_size"/>
</LinearLayout>
</LinearLayout>
......@@ -19,6 +19,7 @@
android:minWidth="@dimen/exo_setting_width"
android:minHeight="@dimen/exo_settings_height"
android:background="?android:attr/selectableItemBackground"
android:layoutDirection="locale"
android:orientation="horizontal">
<ImageView
......@@ -40,5 +41,6 @@
android:layout_marginRight="4dp"
android:gravity="center|start"
android:textColor="@color/exo_white"
android:textDirection="locale"
android:textSize="@dimen/exo_settings_main_text_size"/>
</LinearLayout>
......@@ -72,8 +72,16 @@
<attr name="controller_layout_id" format="reference"/>
<attr name="animation_enabled" format="boolean"/>
<!-- Needed for https://github.com/google/ExoPlayer/issues/7898 -->
<attr name="backgroundTint" format="color"/>
<!-- DefaultTimeBar attributes -->
<attr name="bar_height" format="dimension"/>
<attr name="bar_gravity" format="enum">
<enum name="center" value="0"/>
<enum name="bottom" value="1"/>
<enum name="top" value="2"/>
</attr>
<attr name="touch_target_height" format="dimension"/>
<attr name="ad_marker_width" format="dimension"/>
<attr name="scrubber_enabled_size" format="dimension"/>
......@@ -154,6 +162,7 @@
<attr name="animation_enabled"/>
<!-- DefaultTimeBar attributes -->
<attr name="bar_height"/>
<attr name="bar_gravity"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<attr name="scrubber_enabled_size"/>
......@@ -186,6 +195,7 @@
<attr name="controller_layout_id"/>
<!-- DefaultTimeBar attributes -->
<attr name="bar_height"/>
<attr name="bar_gravity"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<attr name="scrubber_enabled_size"/>
......@@ -217,6 +227,7 @@
<attr name="animation_enabled"/>
<!-- DefaultTimeBar attributes -->
<attr name="bar_height"/>
<attr name="bar_gravity"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<attr name="scrubber_enabled_size"/>
......@@ -233,6 +244,7 @@
<declare-styleable name="DefaultTimeBar">
<attr name="bar_height"/>
<attr name="bar_gravity"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<attr name="scrubber_enabled_size"/>
......
......@@ -38,8 +38,8 @@
<dimen name="exo_styled_progress_bar_height">2dp</dimen>
<dimen name="exo_styled_progress_enabled_thumb_size">10dp</dimen>
<dimen name="exo_styled_progress_dragged_thumb_size">14dp</dimen>
<dimen name="exo_styled_progress_layout_height">14dp</dimen>
<dimen name="exo_styled_progress_touch_target_height">14dp</dimen>
<dimen name="exo_styled_progress_layout_height">48dp</dimen>
<dimen name="exo_styled_progress_touch_target_height">48dp</dimen>
<dimen name="exo_styled_progress_margin_bottom">52dp</dimen>
<dimen name="exo_bottom_bar_height">60dp</dimen>
......
......@@ -93,12 +93,19 @@
<item name="android:gravity">center|bottom</item>
<item name="android:paddingBottom">@dimen/exo_icon_padding_bottom</item>
<item name="android:textAppearance">@style/ExoStyledControls.ButtonText</item>
<!-- Needed for https://github.com/google/ExoPlayer/issues/7898 -->
<item name="backgroundTint">@android:color/white</item>
<item name="android:insetBottom">0dp</item>
</style>
<style name="ExoStyledControls.Button.Center.RewWithAmount">
<item name="android:background">@drawable/exo_ripple_rew</item>
<item name="android:gravity">center|bottom</item>
<item name="android:paddingBottom">@dimen/exo_icon_padding_bottom</item>
<item name="android:textAppearance">@style/ExoStyledControls.ButtonText</item>
<!-- Needed for https://github.com/google/ExoPlayer/issues/7898 -->
<item name="backgroundTint">@android:color/white</item>
<item name="android:insetBottom">0dp</item>
</style>
<style name="ExoStyledControls.ButtonText">
......@@ -187,6 +194,7 @@
<style name="ExoStyledControls.TimeBar">
<item name="bar_height">@dimen/exo_styled_progress_bar_height</item>
<item name="bar_gravity">bottom</item>
<item name="touch_target_height">@dimen/exo_styled_progress_touch_target_height</item>
<item name="scrubber_enabled_size">@dimen/exo_styled_progress_enabled_thumb_size</item>
<item name="scrubber_dragged_size">@dimen/exo_styled_progress_dragged_thumb_size</item>
......
......@@ -17,7 +17,7 @@
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/midroll-5s.mp4
file:///android_asset/media/mp4/midroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>
......@@ -48,7 +48,7 @@ file:///android_asset/mp4/midroll-5s.mp4
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/midroll-5s.mp4
file:///android_asset/media/mp4/midroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>
......
......@@ -17,7 +17,7 @@
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/midroll-5s.mp4
file:///android_asset/media/mp4/midroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>
......@@ -48,7 +48,7 @@ file:///android_asset/mp4/midroll-5s.mp4
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/midroll-5s.mp4
file:///android_asset/media/mp4/midroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>
......
......@@ -14,7 +14,7 @@
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/preroll-5s.mp4
file:///android_asset/media/mp4/preroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>
......
......@@ -17,7 +17,7 @@
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/preroll-5s.mp4
file:///android_asset/media/mp4/preroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>
......@@ -48,7 +48,7 @@ file:///android_asset/mp4/preroll-5s.mp4
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/midroll-5s.mp4
file:///android_asset/media/mp4/midroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>
......@@ -79,7 +79,7 @@ file:///android_asset/mp4/midroll-5s.mp4
<MediaFiles>
<MediaFile id="GDFP" delivery="progressive" width="640" height="360" type="video/mp4" bitrate="450" scalable="true" maintainAspectRatio="true">
<![CDATA[
file:///android_asset/mp4/postroll-5s.mp4
file:///android_asset/media/mp4/postroll-5s.mp4
]]>
</MediaFile>
</MediaFiles>
......
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