Merge pull request #8102 from google/dev-v2-r2.12.1

r2.12.1
This commit is contained in:
Oliver Woodman 2020-10-22 20:14:02 +01:00 committed by GitHub
commit be13805ce4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
214 changed files with 6625 additions and 1457 deletions

View File

@ -1,5 +1,77 @@
# Release notes
### 2.12.1 (2020-10-23) ###
* Core library:
* Fix issue where `Player.setMediaItems` would ignore its `resetPosition`
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
* Add 403, 500 and 503 to the list of HTTP status codes that can trigger
failover to another quality variant during adaptive playbacks.
* Data sources:
* Add support for `android.resource` URI scheme in `RawResourceDataSource`
([#7866](https://github.com/google/ExoPlayer/issues/7866)).
* Text:
* Add support for `\h` SSA/ASS style override code (non-breaking space).
* Fix playback of WebVTT subtitles in MP4 containers in DASH streams
([#7985](https://github.com/google/ExoPlayer/issues/7985)).
* Fix `NullPointerException` in `TextRenderer` when playing content with a
single subtitle buffer
([#8017](https://github.com/google/ExoPlayer/issues/8017)).
* UI:
* Fix animation when `StyledPlayerView` first shows its playback controls.
* Improve touch targets in `StyledPlayerView` to make tapping easier.
* Allow `subtitleButton` to be omitted in custom `StyledPlayerView`
layouts ([#7962](https://github.com/google/ExoPlayer/issues/7962)).
* Add an option to sort tracks by `Format` in `TrackSelectionView` and
`TrackSelectionDialogBuilder`
([#7709](https://github.com/google/ExoPlayer/issues/7709)).
* Audio:
* Fix the default audio sink position not advancing correctly when using
`AudioTrack` based speed adjustment
([#7982](https://github.com/google/ExoPlayer/issues/7982)).
* Fix `NoClassDefFoundError` warning for `AudioTrack$StreamEventCallback`
([#8058](https://github.com/google/ExoPlayer/issues/8058)).
* Extractors:
* MP4:
* Add support for `_mp2` boxes
([#7967](https://github.com/google/ExoPlayer/issues/7967)).
* Fix playback of files containing `pcm_alaw` or `pcm_mulaw` audio
tracks, by enabling sample rechunking for such tracks.
* MPEG-TS:
* Add `TsExtractor` parameter to configure the number of bytes in
which to search for timestamps when seeking and determining stream
duration ([#7988](https://github.com/google/ExoPlayer/issues/7988)).
* Ignore negative payload size in PES packets
([#8005](https://github.com/google/ExoPlayer/issues/8005)).
* MP3: Use TLEN ID3 tag to compute the stream duration
([#7949](https://github.com/google/ExoPlayer/issues/7949)).
* Ogg: Fix regression playing files with packets that span multiple pages
([#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)).
* 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)).
* IMA extension:
* Fix position reporting after fetch errors
([#7956](https://github.com/google/ExoPlayer/issues/7956)).
* 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
([#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)).
* 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
companion ads rendering when targeting API 29
([#6432](https://github.com/google/ExoPlayer/issues/6432)).
### 2.12.0 (2020-09-11) ###
To learn more about what's new in 2.12, read the corresponding
@ -163,7 +235,7 @@ To learn more about what's new in 2.12, read the corresponding
* Redefine `Cue.lineType=LINE_TYPE_NUMBER` in terms of aligning the cue
text lines to grid of viewport lines. Only consider `Cue.lineAnchor`
when `Cue.lineType=LINE_TYPE_FRACTION`.
* WebVTT
* WebVTT:
* Add support for default
[text](https://www.w3.org/TR/webvtt1/#default-text-color) and
[background](https://www.w3.org/TR/webvtt1/#default-text-background)
@ -178,10 +250,10 @@ To learn more about what's new in 2.12, read the corresponding
* Parse the `ruby-position` CSS property.
* Parse the `text-combine-upright` CSS property (i.e., tate-chu-yoko).
* Parse the `<ruby>` and `<rt>` tags.
* TTML
* TTML:
* Parse the `tts:combineText` property (i.e., tate-chu-yoko).
* Parse t`tts:ruby` and `tts:rubyPosition` properties.
* CEA-608
* CEA-608:
* Implement timing-out of stuck captions, as permitted by
ANSI/CTA-608-E R-2014 Annex C.9. The default timeout is set to 16
seconds ([#7181](https://github.com/google/ExoPlayer/issues/7181)).

View File

@ -17,7 +17,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'com.novoda:bintray-release:0.9.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1'
}

View File

@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.12.0'
releaseVersionCode = 2012000
releaseVersion = '2.12.1'
releaseVersionCode = 2012001
minSdkVersion = 16
appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.

View File

@ -29,6 +29,7 @@ include modulePrefix + 'library-extractor'
include modulePrefix + 'library-hls'
include modulePrefix + 'library-smoothstreaming'
include modulePrefix + 'library-ui'
include modulePrefix + 'robolectricutils'
include modulePrefix + 'testutils'
include modulePrefix + 'testdata'
include modulePrefix + 'extension-av1'
@ -56,6 +57,7 @@ project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'libr
project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls')
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
project(modulePrefix + 'robolectricutils').projectDir = new File(rootDir, 'robolectricutils')
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
project(modulePrefix + 'testdata').projectDir = new File(rootDir, 'testdata')
project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1')

View File

@ -70,13 +70,6 @@ dependencies {
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:1.2.1'
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls')

View File

@ -527,6 +527,20 @@
{
"name": "MPEG-4 Timed Text (tx3g, mov_text)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4"
},
{
"name": "Japanese features (vertical + rubies) [TTML]",
"uri": "https://html5demos.com/assets/dizzy.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/japanese-ttml.xml",
"subtitle_mime_type": "application/ttml+xml",
"subtitle_language": "ja"
},
{
"name": "Japanese features (vertical + rubies) [WebVTT]",
"uri": "https://html5demos.com/assets/dizzy.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/japanese.vtt",
"subtitle_mime_type": "text/vtt",
"subtitle_language": "ja"
}
]
},

View File

@ -102,7 +102,7 @@ public class PlayerActivity extends AppCompatActivity
private int startWindow;
private long startPosition;
// Fields used only for ad playback. The ads loader is loaded via reflection.
// Fields used only for ad playback.
private AdsLoader adsLoader;
private Uri loadedAdTagUri;
@ -375,7 +375,7 @@ public class PlayerActivity extends AppCompatActivity
}
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
if (adsLoader == null) {
adsLoader = new ImaAdsLoader(/* context= */ PlayerActivity.this, adTagUri);
adsLoader = new ImaAdsLoader.Builder(/* context= */ this).build();
}
adsLoader.setPlayer(player);
return adsLoader;

View File

@ -354,7 +354,12 @@ public final class TrackSelectionDialog extends DialogFragment {
trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides);
trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);
trackSelectionView.init(
mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this);
mappedTrackInfo,
rendererIndex,
isDisabled,
overrides,
/* trackFormatComparator= */ null,
/* listener= */ this);
return rootView;
}

View File

@ -307,6 +307,13 @@ public final class CastPlayer extends BasePlayer {
}
}
@Override
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
int windowIndex = resetPosition ? 0 : getCurrentWindowIndex();
long startPositionMs = resetPosition ? C.TIME_UNSET : getContentPosition();
setMediaItems(mediaItems, windowIndex, startPositionMs);
}
@Override
public void setMediaItems(
List<MediaItem> mediaItems, int startWindowIndex, long startPositionMs) {

View File

@ -16,7 +16,6 @@
package com.google.android.exoplayer2.ext.cast;
import android.content.Context;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.OptionsProvider;
import com.google.android.gms.cast.framework.SessionProvider;
@ -29,24 +28,12 @@ import java.util.List;
public final class DefaultCastOptionsProvider implements OptionsProvider {
/**
* App id of the Default Media Receiver app. Apps that do not require DRM support may use this
* receiver receiver app ID.
* App id that points to the Default Media Receiver app with basic DRM support.
*
* <p>See https://developers.google.com/cast/docs/caf_receiver/#default_media_receiver.
* <p>Applications that require more complex DRM authentication should <a
* href="https://developers.google.com/cast/docs/web_receiver/streaming_protocols#drm">create a
* custom receiver application</a>.
*/
public static final String APP_ID_DEFAULT_RECEIVER =
CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
/**
* App id for receiver app with rudimentary support for DRM.
*
* <p>This app id is only suitable for ExoPlayer's Cast Demo app, and it is not intended for
* production use. In order to use DRM, custom receiver apps should be used. For environments that
* do not require DRM, the default receiver app should be used (see {@link
* #APP_ID_DEFAULT_RECEIVER}).
*/
// TODO: Add a documentation resource link for DRM support in the receiver app [Internal ref:
// b/128603245].
public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273";
@Override

View File

@ -17,13 +17,6 @@ dependencies {
api "com.google.android.gms:play-services-cronet:17.0.0"
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
testImplementation project(modulePrefix + 'library')

View File

@ -25,39 +25,18 @@ android {
}
dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.19.4'
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.20.1'
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
androidTestImplementation project(modulePrefix + 'testutils')
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
androidTestImplementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
androidTestCompileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils')
testImplementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}

View File

@ -1,56 +0,0 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.ima;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import java.util.Arrays;
import java.util.List;
/**
* Static utility class for constructing {@link AdPlaybackState} instances from IMA-specific data.
*/
/* package */ final class AdPlaybackStateFactory {
private AdPlaybackStateFactory() {}
/**
* Construct an {@link AdPlaybackState} from the provided {@code cuePoints}.
*
* @param cuePoints The cue points of the ads in seconds.
* @return The {@link AdPlaybackState}.
*/
public static AdPlaybackState fromCuePoints(List<Float> cuePoints) {
if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad.
return new AdPlaybackState(/* adGroupTimesUs...= */ 0);
}
int count = cuePoints.size();
long[] adGroupTimesUs = new long[count];
int adGroupIndex = 0;
for (int i = 0; i < count; i++) {
double cuePoint = cuePoints.get(i);
if (cuePoint == -1.0) {
adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE;
} else {
adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint);
}
}
// Cue points may be out of order, so sort them.
Arrays.sort(adGroupTimesUs, 0, adGroupIndex);
return new AdPlaybackState(adGroupTimesUs);
}
}

View File

@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.ima;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.max;
import android.content.Context;
@ -33,7 +32,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdError;
import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
import com.google.ads.interactivemedia.v3.api.AdEvent;
@ -61,7 +59,9 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSpec;
@ -93,12 +93,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
* {@link #release()}.
*
* <p>See https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
* information on compatible ad tag formats. Pass the ad tag URI when setting media item playback
* properties (if using the media item API) or as a {@link DataSpec} when constructing the {@link
* AdsMediaSource} (if using media sources directly). For the latter case, please note that this
* implementation delegates loading of the data spec to the IMA SDK, so range and headers
* specifications will be ignored in ad tag URIs. Literal ads responses can be encoded as data
* scheme data specs, for example, by constructing the data spec using a URI generated via {@link
* Util#getDataUriForString(String, String)}.
*
* <p>The IMA SDK can report obstructions to the ad view for accurate viewability measurement. This
* means that any overlay views that obstruct the ad overlay but are essential for playback need to
* be registered via the {@link AdViewProvider} passed to the {@link
* com.google.android.exoplayer2.source.ads.AdsMediaSource}. See the <a
* href="https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/omsdk">
* IMA SDK Open Measurement documentation</a> for more information.
* be registered via the {@link AdViewProvider} passed to the {@link AdsMediaSource}. See the <a
* href="https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/omsdk">IMA
* SDK Open Measurement documentation</a> for more information.
*/
public final class ImaAdsLoader
implements Player.EventListener, com.google.android.exoplayer2.source.ads.AdsLoader {
@ -126,6 +134,8 @@ public final class ImaAdsLoader
@Nullable private ImaSdkSettings imaSdkSettings;
@Nullable private AdErrorListener adErrorListener;
@Nullable private AdEventListener adEventListener;
@Nullable private VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback;
@Nullable private List<String> adMediaMimeTypes;
@Nullable private Set<UiElement> adUiElements;
@Nullable private Collection<CompanionAdSlot> companionAdSlots;
private long adPreloadTimeoutMs;
@ -134,7 +144,8 @@ public final class ImaAdsLoader
private int mediaBitrate;
private boolean focusSkipButtonWhenAvailable;
private boolean playAdBeforeStartPosition;
private ImaFactory imaFactory;
private boolean debugModeEnabled;
private ImaUtil.ImaFactory imaFactory;
/**
* Creates a new builder for {@link ImaAdsLoader}.
@ -142,7 +153,7 @@ public final class ImaAdsLoader
* @param context The context;
*/
public Builder(Context context) {
this.context = checkNotNull(context);
this.context = checkNotNull(context).getApplicationContext();
adPreloadTimeoutMs = DEFAULT_AD_PRELOAD_TIMEOUT_MS;
vastLoadTimeoutMs = TIMEOUT_UNSET;
mediaLoadTimeoutMs = TIMEOUT_UNSET;
@ -191,6 +202,22 @@ public final class ImaAdsLoader
return this;
}
/**
* Sets a callback to receive video ad player events. Note that these events are handled
* internally by the IMA SDK and this ads loader. For analytics and diagnostics, new
* implementations should generally use events from the top-level {@link Player} listeners
* instead of setting a callback via this method.
*
* @param videoAdPlayerCallback The callback to receive video ad player events.
* @return This builder, for convenience.
* @see com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer.VideoAdPlayerCallback
*/
public Builder setVideoAdPlayerCallback(
VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback) {
this.videoAdPlayerCallback = checkNotNull(videoAdPlayerCallback);
return this;
}
/**
* Sets the ad UI elements to be rendered by the IMA SDK.
*
@ -215,6 +242,23 @@ public final class ImaAdsLoader
return this;
}
/**
* Sets the MIME types to prioritize for linear ad media. If not specified, MIME types supported
* by the {@link MediaSourceFactory adMediaSourceFactory} used to construct the {@link
* AdsMediaSource} will be used.
*
* @param adMediaMimeTypes The MIME types to prioritize for linear ad media. May contain {@link
* MimeTypes#APPLICATION_MPD}, {@link MimeTypes#APPLICATION_M3U8}, {@link
* MimeTypes#VIDEO_MP4}, {@link MimeTypes#VIDEO_WEBM}, {@link MimeTypes#VIDEO_H263}, {@link
* MimeTypes#AUDIO_MP4} and {@link MimeTypes#AUDIO_MPEG}.
* @return This builder, for convenience.
* @see AdsRenderingSettings#setMimeTypes(List)
*/
public Builder setAdMediaMimeTypes(List<String> adMediaMimeTypes) {
this.adMediaMimeTypes = ImmutableList.copyOf(checkNotNull(adMediaMimeTypes));
return this;
}
/**
* Sets the duration in milliseconds for which the player must buffer while preloading an ad
* group before that ad group is skipped and marked as having failed to load. Pass {@link
@ -302,8 +346,23 @@ public final class ImaAdsLoader
return this;
}
/**
* Sets whether to enable outputting verbose logs for the IMA extension and IMA SDK. The default
* value is {@code false}. This setting is intended for debugging only, and should not be
* enabled in production applications.
*
* @param debugModeEnabled Whether to enable outputting verbose logs for the IMA extension and
* IMA SDK.
* @return This builder, for convenience.
* @see ImaSdkSettings#setDebugMode(boolean)
*/
public Builder setDebugModeEnabled(boolean debugModeEnabled) {
this.debugModeEnabled = debugModeEnabled;
return this;
}
@VisibleForTesting
/* package */ Builder setImaFactory(ImaFactory imaFactory) {
/* package */ Builder setImaFactory(ImaUtil.ImaFactory imaFactory) {
this.imaFactory = checkNotNull(imaFactory);
return this;
}
@ -315,24 +374,18 @@ public final class ImaAdsLoader
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
* information on compatible ad tags.
* @return The new {@link ImaAdsLoader}.
* @deprecated Pass the ad tag URI when setting media item playback properties (if using the
* media item API) or as a {@link DataSpec} when constructing the {@link AdsMediaSource} (if
* using media sources directly).
*/
@Deprecated
public ImaAdsLoader buildForAdTag(Uri adTagUri) {
return new ImaAdsLoader(
context,
adTagUri,
imaSdkSettings,
/* adsResponse= */ null,
adPreloadTimeoutMs,
vastLoadTimeoutMs,
mediaLoadTimeoutMs,
mediaBitrate,
focusSkipButtonWhenAvailable,
playAdBeforeStartPosition,
adUiElements,
companionAdSlots,
adErrorListener,
adEventListener,
imaFactory);
getConfiguration(),
imaFactory,
/* adTagUri= */ adTagUri,
/* adsResponse= */ null);
}
/**
@ -341,28 +394,44 @@ public final class ImaAdsLoader
* @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of
* making a request via an ad tag URL.
* @return The new {@link ImaAdsLoader}.
* @deprecated Pass the ads response as a data URI when setting media item playback properties
* (if using the media item API) or as a {@link DataSpec} when constructing the {@link
* AdsMediaSource} (if using media sources directly). {@link
* Util#getDataUriForString(String, String)} can be used to construct a data URI from
* literal string ads response (with MIME type text/xml).
*/
@Deprecated
public ImaAdsLoader buildForAdsResponse(String adsResponse) {
return new ImaAdsLoader(
context,
/* adTagUri= */ null,
imaSdkSettings,
adsResponse,
context, getConfiguration(), imaFactory, /* adTagUri= */ null, adsResponse);
}
/** Returns a new {@link ImaAdsLoader}. */
public ImaAdsLoader build() {
return new ImaAdsLoader(
context, getConfiguration(), imaFactory, /* adTagUri= */ null, /* adsResponse= */ null);
}
// TODO(internal: b/169646419): Remove/hide once the deprecated constructor has been removed.
/* package */ ImaUtil.Configuration getConfiguration() {
return new ImaUtil.Configuration(
adPreloadTimeoutMs,
vastLoadTimeoutMs,
mediaLoadTimeoutMs,
mediaBitrate,
focusSkipButtonWhenAvailable,
playAdBeforeStartPosition,
mediaBitrate,
adMediaMimeTypes,
adUiElements,
companionAdSlots,
adErrorListener,
adEventListener,
imaFactory);
videoAdPlayerCallback,
imaSdkSettings,
debugModeEnabled);
}
}
private static final boolean DEBUG = false;
private static final String TAG = "ImaAdsLoader";
private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima";
@ -413,20 +482,13 @@ public final class ImaAdsLoader
*/
private static final int IMA_AD_STATE_PAUSED = 2;
private static final DataSpec EMPTY_AD_TAG_DATA_SPEC = new DataSpec(Uri.EMPTY);
private final ImaUtil.Configuration configuration;
private final Context context;
private final ImaUtil.ImaFactory imaFactory;
@Nullable private final Uri adTagUri;
@Nullable private final String adsResponse;
private final long adPreloadTimeoutMs;
private final int vastLoadTimeoutMs;
private final int mediaLoadTimeoutMs;
private final boolean focusSkipButtonWhenAvailable;
private final boolean playAdBeforeStartPosition;
private final int mediaBitrate;
@Nullable private final Set<UiElement> adUiElements;
@Nullable private final Collection<CompanionAdSlot> companionAdSlots;
@Nullable private final AdErrorListener adErrorListener;
@Nullable private final AdEventListener adEventListener;
private final ImaFactory imaFactory;
private final ImaSdkSettings imaSdkSettings;
private final Timeline.Period period;
private final Handler handler;
@ -443,6 +505,7 @@ public final class ImaAdsLoader
private List<String> supportedMimeTypes;
@Nullable private EventListener eventListener;
@Nullable private Player player;
private DataSpec adTagDataSpec;
private VideoProgressUpdate lastContentProgress;
private VideoProgressUpdate lastAdProgress;
private int lastVolumePercent;
@ -518,61 +581,36 @@ public final class ImaAdsLoader
* @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
* more information.
* @deprecated Use {@link Builder} to create an instance. Pass the ad tag URI when setting media
* item playback properties (if using the media item API) or as a {@link DataSpec} when
* constructing the {@link AdsMediaSource} (if using media sources directly).
*/
@Deprecated
public ImaAdsLoader(Context context, Uri adTagUri) {
this(
context,
new Builder(context).getConfiguration(),
new DefaultImaFactory(),
adTagUri,
/* imaSdkSettings= */ null,
/* adsResponse= */ null,
/* adPreloadTimeoutMs= */ Builder.DEFAULT_AD_PRELOAD_TIMEOUT_MS,
/* vastLoadTimeoutMs= */ TIMEOUT_UNSET,
/* mediaLoadTimeoutMs= */ TIMEOUT_UNSET,
/* mediaBitrate= */ BITRATE_UNSET,
/* focusSkipButtonWhenAvailable= */ true,
/* playAdBeforeStartPosition= */ true,
/* adUiElements= */ null,
/* companionAdSlots= */ null,
/* adErrorListener= */ null,
/* adEventListener= */ null,
/* imaFactory= */ new DefaultImaFactory());
/* adsResponse= */ null);
}
@SuppressWarnings({"nullness:argument.type.incompatible", "methodref.receiver.bound.invalid"})
private ImaAdsLoader(
Context context,
ImaUtil.Configuration configuration,
ImaUtil.ImaFactory imaFactory,
@Nullable Uri adTagUri,
@Nullable ImaSdkSettings imaSdkSettings,
@Nullable String adsResponse,
long adPreloadTimeoutMs,
int vastLoadTimeoutMs,
int mediaLoadTimeoutMs,
int mediaBitrate,
boolean focusSkipButtonWhenAvailable,
boolean playAdBeforeStartPosition,
@Nullable Set<UiElement> adUiElements,
@Nullable Collection<CompanionAdSlot> companionAdSlots,
@Nullable AdErrorListener adErrorListener,
@Nullable AdEventListener adEventListener,
ImaFactory imaFactory) {
checkArgument(adTagUri != null || adsResponse != null);
@Nullable String adsResponse) {
this.context = context.getApplicationContext();
this.configuration = configuration;
this.imaFactory = imaFactory;
this.adTagUri = adTagUri;
this.adsResponse = adsResponse;
this.adPreloadTimeoutMs = adPreloadTimeoutMs;
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
this.mediaBitrate = mediaBitrate;
this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable;
this.playAdBeforeStartPosition = playAdBeforeStartPosition;
this.adUiElements = adUiElements;
this.companionAdSlots = companionAdSlots;
this.adErrorListener = adErrorListener;
this.adEventListener = adEventListener;
this.imaFactory = imaFactory;
@Nullable ImaSdkSettings imaSdkSettings = configuration.imaSdkSettings;
if (imaSdkSettings == null) {
imaSdkSettings = imaFactory.createImaSdkSettings();
if (DEBUG) {
if (configuration.debugModeEnabled) {
imaSdkSettings.setDebugMode(true);
}
}
@ -583,9 +621,13 @@ public final class ImaAdsLoader
handler = Util.createHandler(getImaLooper(), /* callback= */ null);
componentListener = new ComponentListener();
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
if (configuration.applicationVideoAdPlayerCallback != null) {
adCallbacks.add(configuration.applicationVideoAdPlayerCallback);
}
updateAdProgressRunnable = this::updateAdProgress;
adInfoByAdMediaInfo = HashBiMap.create();
supportedMimeTypes = Collections.emptyList();
adTagDataSpec = EMPTY_AD_TAG_DATA_SPEC;
lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
@ -631,12 +673,62 @@ public final class ImaAdsLoader
*
* @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI, or {@code
* null} if playing audio-only ads.
* @deprecated Use {@link #requestAds(DataSpec, ViewGroup)}, specifying the ad tag data spec to
* request, and migrate off deprecated builder methods/constructor that require an ad tag or
* ads response.
*/
@Deprecated
public void requestAds(@Nullable ViewGroup adViewGroup) {
requestAds(adTagDataSpec, adViewGroup);
}
/**
* Requests ads, if they have not already been requested. Must be called on the main thread.
*
* <p>Ads will be requested automatically when the player is prepared if this method has not been
* called, so it is only necessary to call this method if you want to request ads before preparing
* the player.
*
* @param adTagDataSpec The data specification of the ad tag to load. See class javadoc for
* information about compatible ad tag formats.
* @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI, or {@code
* null} if playing audio-only ads.
*/
public void requestAds(DataSpec adTagDataSpec, @Nullable ViewGroup adViewGroup) {
if (hasAdPlaybackState || adsManager != null || pendingAdRequestContext != null) {
// Ads have already been requested.
return;
}
if (EMPTY_AD_TAG_DATA_SPEC.equals(adTagDataSpec)) {
// Handle deprecated ways of specifying the ad tag.
if (adTagUri != null) {
adTagDataSpec = new DataSpec(adTagUri);
} else if (adsResponse != null) {
adTagDataSpec = new DataSpec(Util.getDataUriForString(adsResponse, "text/xml"));
} else {
throw new IllegalStateException();
}
}
AdsRequest request;
try {
request = ImaUtil.getAdsRequestForAdTagDataSpec(imaFactory, adTagDataSpec);
} catch (IOException e) {
hasAdPlaybackState = true;
updateAdPlaybackState();
pendingAdLoadError = AdLoadException.createForAllAds(e);
maybeNotifyPendingAdLoadError();
return;
}
this.adTagDataSpec = adTagDataSpec;
pendingAdRequestContext = new Object();
request.setUserRequestContext(pendingAdRequestContext);
if (configuration.vastLoadTimeoutMs != TIMEOUT_UNSET) {
request.setVastLoadTimeout(configuration.vastLoadTimeoutMs);
}
request.setContentProgressProvider(componentListener);
if (adViewGroup != null) {
adDisplayContainer =
imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener);
@ -644,27 +736,16 @@ public final class ImaAdsLoader
adDisplayContainer =
imaFactory.createAudioAdDisplayContainer(context, /* player= */ componentListener);
}
if (companionAdSlots != null) {
adDisplayContainer.setCompanionSlots(companionAdSlots);
if (configuration.companionAdSlots != null) {
adDisplayContainer.setCompanionSlots(configuration.companionAdSlots);
}
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
adsLoader.addAdErrorListener(componentListener);
if (adErrorListener != null) {
adsLoader.addAdErrorListener(adErrorListener);
if (configuration.applicationAdErrorListener != null) {
adsLoader.addAdErrorListener(configuration.applicationAdErrorListener);
}
adsLoader.addAdsLoadedListener(componentListener);
AdsRequest request = imaFactory.createAdsRequest();
if (adTagUri != null) {
request.setAdTagUrl(adTagUri.toString());
} else {
request.setAdsResponse(castNonNull(adsResponse));
}
if (vastLoadTimeoutMs != TIMEOUT_UNSET) {
request.setVastLoadTimeout(vastLoadTimeoutMs);
}
request.setContentProgressProvider(componentListener);
pendingAdRequestContext = new Object();
request.setUserRequestContext(pendingAdRequestContext);
adsLoader.requestAds(request);
}
@ -713,6 +794,11 @@ public final class ImaAdsLoader
this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes);
}
@Override
public void setAdTagDataSpec(DataSpec adTagDataSpec) {
this.adTagDataSpec = adTagDataSpec;
}
@Override
public void start(EventListener eventListener, AdViewProvider adViewProvider) {
checkState(
@ -735,18 +821,18 @@ public final class ImaAdsLoader
adsManager.resume();
}
} else if (adsManager != null) {
adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints());
adPlaybackState = ImaUtil.getInitialAdPlaybackStateForCuePoints(adsManager.getAdCuePoints());
updateAdPlaybackState();
} else {
// Ads haven't loaded yet, so request them.
requestAds(adViewProvider.getAdViewGroup());
requestAds(adTagDataSpec, adViewProvider.getAdViewGroup());
}
if (adDisplayContainer != null) {
for (OverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) {
adDisplayContainer.registerFriendlyObstruction(
imaFactory.createFriendlyObstruction(
overlayInfo.view,
getFriendlyObstructionPurpose(overlayInfo.purpose),
ImaUtil.getFriendlyObstructionPurpose(overlayInfo.purpose),
overlayInfo.reasonDetail));
}
}
@ -782,8 +868,8 @@ public final class ImaAdsLoader
if (adsLoader != null) {
adsLoader.removeAdsLoadedListener(componentListener);
adsLoader.removeAdErrorListener(componentListener);
if (adErrorListener != null) {
adsLoader.removeAdErrorListener(adErrorListener);
if (configuration.applicationAdErrorListener != null) {
adsLoader.removeAdErrorListener(configuration.applicationAdErrorListener);
}
}
imaPausedContent = false;
@ -800,7 +886,7 @@ public final class ImaAdsLoader
@Override
public void handlePrepareComplete(int adGroupIndex, int adIndexInAdGroup) {
AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(TAG, "Prepared ad " + adInfo);
}
@Nullable AdMediaInfo adMediaInfo = adInfoByAdMediaInfo.inverse().get(adInfo);
@ -850,7 +936,7 @@ public final class ImaAdsLoader
} else {
adsManager.init(adsRenderingSettings);
adsManager.start();
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings);
}
}
@ -887,7 +973,7 @@ public final class ImaAdsLoader
long adGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
long contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
long timeUntilAdMs = adGroupTimeMs - contentPositionMs;
if (timeUntilAdMs < adPreloadTimeoutMs) {
if (timeUntilAdMs < configuration.adPreloadTimeoutMs) {
waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime();
}
} else if (playbackState == Player.STATE_READY) {
@ -936,16 +1022,20 @@ public final class ImaAdsLoader
private AdsRenderingSettings setupAdsRendering() {
AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();
adsRenderingSettings.setEnablePreloading(true);
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {
adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs);
adsRenderingSettings.setMimeTypes(
configuration.adMediaMimeTypes != null
? configuration.adMediaMimeTypes
: supportedMimeTypes);
if (configuration.mediaLoadTimeoutMs != TIMEOUT_UNSET) {
adsRenderingSettings.setLoadVideoTimeout(configuration.mediaLoadTimeoutMs);
}
if (mediaBitrate != BITRATE_UNSET) {
adsRenderingSettings.setBitrateKbps(mediaBitrate / 1000);
if (configuration.mediaBitrate != BITRATE_UNSET) {
adsRenderingSettings.setBitrateKbps(configuration.mediaBitrate / 1000);
}
adsRenderingSettings.setFocusSkipButtonWhenAvailable(focusSkipButtonWhenAvailable);
if (adUiElements != null) {
adsRenderingSettings.setUiElements(adUiElements);
adsRenderingSettings.setFocusSkipButtonWhenAvailable(
configuration.focusSkipButtonWhenAvailable);
if (configuration.adUiElements != null) {
adsRenderingSettings.setUiElements(configuration.adUiElements);
}
// Skip ads based on the start position as required.
@ -956,7 +1046,7 @@ public final class ImaAdsLoader
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
if (adGroupForPositionIndex != C.INDEX_UNSET) {
boolean playAdWhenStartingPlayback =
playAdBeforeStartPosition
configuration.playAdBeforeStartPosition
|| adGroupTimesUs[adGroupForPositionIndex] == C.msToUs(contentPositionMs);
if (!playAdWhenStartingPlayback) {
adGroupForPositionIndex++;
@ -1069,7 +1159,7 @@ public final class ImaAdsLoader
switch (adEvent.getType()) {
case AD_BREAK_FETCH_ERROR:
String adGroupTimeSecondsString = checkNotNull(adEvent.getAdData().get("adBreakTime"));
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds");
}
double adGroupTimeSeconds = Double.parseDouble(adGroupTimeSecondsString);
@ -1077,7 +1167,7 @@ public final class ImaAdsLoader
adGroupTimeSeconds == -1.0
? adPlaybackState.adGroupCount - 1
: getAdGroupIndexForCuePointTimeSeconds(adGroupTimeSeconds);
handleAdGroupFetchError(adGroupIndex);
markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex);
break;
case CONTENT_PAUSE_REQUESTED:
// After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
@ -1156,7 +1246,7 @@ public final class ImaAdsLoader
adCallbacks.get(i).onEnded(adMediaInfo);
}
}
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlaybackStateChanged");
}
}
@ -1196,7 +1286,7 @@ public final class ImaAdsLoader
adCallbacks.get(i).onEnded(adMediaInfo);
}
}
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
}
}
@ -1218,7 +1308,7 @@ public final class ImaAdsLoader
private void loadAdInternal(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) {
if (adsManager == null) {
// Drop events after release.
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(
TAG,
"loadAd after release " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo);
@ -1230,7 +1320,7 @@ public final class ImaAdsLoader
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
adInfoByAdMediaInfo.put(adMediaInfo, adInfo);
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo));
}
if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) {
@ -1261,7 +1351,7 @@ public final class ImaAdsLoader
}
private void playAdInternal(AdMediaInfo adMediaInfo) {
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo));
}
if (adsManager == null) {
@ -1305,7 +1395,7 @@ public final class ImaAdsLoader
}
private void pauseAdInternal(AdMediaInfo adMediaInfo) {
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo));
}
if (adsManager == null) {
@ -1325,7 +1415,7 @@ public final class ImaAdsLoader
}
private void stopAdInternal(AdMediaInfo adMediaInfo) {
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo));
}
if (adsManager == null) {
@ -1364,35 +1454,20 @@ public final class ImaAdsLoader
}
}
private void handleAdGroupFetchError(int adGroupIndex) {
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
if (adGroup.count == C.LENGTH_UNSET) {
adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, max(1, adGroup.states.length));
adGroup = adPlaybackState.adGroups[adGroupIndex];
}
for (int i = 0; i < adGroup.count; i++) {
if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
if (DEBUG) {
Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex);
}
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i);
}
}
updateAdPlaybackState();
}
private void handleAdGroupLoadError(Exception error) {
if (player == null) {
return;
}
// TODO: Once IMA signals which ad group failed to load, remove this call.
int adGroupIndex = getLoadingAdGroupIndex();
if (adGroupIndex == C.INDEX_UNSET) {
Log.w(TAG, "Unable to determine ad group index for ad group load error", error);
return;
}
markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex);
if (pendingAdLoadError == null) {
pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
}
}
private void markAdGroupInErrorStateAndClearPendingContentPosition(int adGroupIndex) {
// Update the ad playback state so all ads in the ad group are in the error state.
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
if (adGroup.count == C.LENGTH_UNSET) {
adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, max(1, adGroup.states.length));
@ -1400,22 +1475,20 @@ public final class ImaAdsLoader
}
for (int i = 0; i < adGroup.count; i++) {
if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex);
}
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i);
}
}
updateAdPlaybackState();
if (pendingAdLoadError == null) {
pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
}
// Clear any pending content position that triggered attempting to load the ad group.
pendingContentPositionMs = C.TIME_UNSET;
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
}
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(
TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
}
@ -1467,7 +1540,7 @@ public final class ImaAdsLoader
adCallbacks.get(i).onContentComplete();
}
sentContentComplete = true;
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(TAG, "adsLoader.contentComplete");
}
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
@ -1487,7 +1560,7 @@ public final class ImaAdsLoader
private void maybeNotifyPendingAdLoadError() {
if (pendingAdLoadError != null && eventListener != null) {
eventListener.onAdLoadError(pendingAdLoadError, getAdsDataSpec(adTagUri));
eventListener.onAdLoadError(pendingAdLoadError, adTagDataSpec);
pendingAdLoadError = null;
}
}
@ -1502,8 +1575,7 @@ public final class ImaAdsLoader
updateAdPlaybackState();
if (eventListener != null) {
eventListener.onAdLoadError(
AdLoadException.createForUnexpected(new RuntimeException(message, cause)),
getAdsDataSpec(adTagUri));
AdLoadException.createForUnexpected(new RuntimeException(message, cause)), adTagDataSpec);
}
}
@ -1522,8 +1594,10 @@ public final class ImaAdsLoader
* no such ad group.
*/
private int getLoadingAdGroupIndex() {
long playerPositionUs =
C.msToUs(getContentPeriodPositionMs(checkNotNull(player), timeline, period));
if (player == null) {
return C.INDEX_UNSET;
}
long playerPositionUs = C.msToUs(getContentPeriodPositionMs(player, timeline, period));
int adGroupIndex =
adPlaybackState.getAdGroupIndexForPositionUs(playerPositionUs, C.msToUs(contentDurationMs));
if (adGroupIndex == C.INDEX_UNSET) {
@ -1538,7 +1612,8 @@ public final class ImaAdsLoader
// We receive initial cue points from IMA SDK as floats. This code replicates the same
// calculation used to populate adGroupTimesUs (having truncated input back to float, to avoid
// failures if the behavior of the IMA SDK changes to provide greater precision).
long adPodTimeUs = Math.round((float) cuePointTimeSeconds * C.MICROS_PER_SECOND);
float cuePointTimeSecondsFloat = (float) cuePointTimeSeconds;
long adPodTimeUs = Math.round((double) cuePointTimeSecondsFloat * C.MICROS_PER_SECOND);
for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) {
long adGroupTimeUs = adPlaybackState.adGroupTimesUs[adGroupIndex];
if (adGroupTimeUs != C.TIME_END_OF_SOURCE
@ -1554,25 +1629,6 @@ public final class ImaAdsLoader
return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]";
}
private static FriendlyObstructionPurpose getFriendlyObstructionPurpose(
@OverlayInfo.Purpose int purpose) {
switch (purpose) {
case OverlayInfo.PURPOSE_CONTROLS:
return FriendlyObstructionPurpose.VIDEO_CONTROLS;
case OverlayInfo.PURPOSE_CLOSE_AD:
return FriendlyObstructionPurpose.CLOSE_AD;
case OverlayInfo.PURPOSE_NOT_VISIBLE:
return FriendlyObstructionPurpose.NOT_VISIBLE;
case OverlayInfo.PURPOSE_OTHER:
default:
return FriendlyObstructionPurpose.OTHER;
}
}
private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) {
return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY);
}
private static long getContentPeriodPositionMs(
Player player, Timeline timeline, Timeline.Period period) {
long contentWindowPositionMs = player.getContentPosition();
@ -1582,13 +1638,6 @@ public final class ImaAdsLoader
: timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs());
}
private static boolean isAdGroupLoadError(AdError adError) {
// TODO: Find out what other errors need to be handled (if any), and whether each one relates to
// a single ad, ad group or the whole timeline.
return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH
|| adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR;
}
private static Looper getImaLooper() {
// IMA SDK callbacks occur on the main thread. This method can be used to check that the player
// is using the same looper, to ensure all interaction with this class is on the main thread.
@ -1610,50 +1659,18 @@ public final class ImaAdsLoader
private void destroyAdsManager() {
if (adsManager != null) {
adsManager.removeAdErrorListener(componentListener);
if (adErrorListener != null) {
adsManager.removeAdErrorListener(adErrorListener);
if (configuration.applicationAdErrorListener != null) {
adsManager.removeAdErrorListener(configuration.applicationAdErrorListener);
}
adsManager.removeAdEventListener(componentListener);
if (adEventListener != null) {
adsManager.removeAdEventListener(adEventListener);
if (configuration.applicationAdEventListener != null) {
adsManager.removeAdEventListener(configuration.applicationAdEventListener);
}
adsManager.destroy();
adsManager = null;
}
}
/** Factory for objects provided by the IMA SDK. */
@VisibleForTesting
/* package */ interface ImaFactory {
/** Creates {@link ImaSdkSettings} for configuring the IMA SDK. */
ImaSdkSettings createImaSdkSettings();
/**
* Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that
* control rendering of ads.
*/
AdsRenderingSettings createAdsRenderingSettings();
/**
* Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for
* non-linear ads, and slots for companion ads.
*/
AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player);
/** Creates an {@link AdDisplayContainer} to hold the player for audio ads. */
AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player);
/**
* Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for
* viewability measurement purposes.
*/
FriendlyObstruction createFriendlyObstruction(
View view,
FriendlyObstructionPurpose friendlyObstructionPurpose,
@Nullable String reasonDetail);
/** Creates an {@link AdsRequest} to contain the data used to request ads. */
AdsRequest createAdsRequest();
/** Creates an {@link AdsLoader} for requesting ads using the specified settings. */
AdsLoader createAdsLoader(
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);
}
private final class ComponentListener
implements AdsLoadedListener,
ContentProgressProvider,
@ -1673,17 +1690,18 @@ public final class ImaAdsLoader
pendingAdRequestContext = null;
ImaAdsLoader.this.adsManager = adsManager;
adsManager.addAdErrorListener(this);
if (adErrorListener != null) {
adsManager.addAdErrorListener(adErrorListener);
if (configuration.applicationAdErrorListener != null) {
adsManager.addAdErrorListener(configuration.applicationAdErrorListener);
}
adsManager.addAdEventListener(this);
if (adEventListener != null) {
adsManager.addAdEventListener(adEventListener);
if (configuration.applicationAdEventListener != null) {
adsManager.addAdEventListener(configuration.applicationAdEventListener);
}
if (player != null) {
// If a player is attached already, start playback immediately.
try {
adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints());
adPlaybackState =
ImaUtil.getInitialAdPlaybackStateForCuePoints(adsManager.getAdCuePoints());
hasAdPlaybackState = true;
updateAdPlaybackState();
} catch (RuntimeException e) {
@ -1697,7 +1715,7 @@ public final class ImaAdsLoader
@Override
public VideoProgressUpdate getContentProgress() {
VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate();
if (DEBUG) {
if (configuration.debugModeEnabled) {
if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) {
Log.d(TAG, "Content progress: not ready");
} else {
@ -1729,7 +1747,7 @@ public final class ImaAdsLoader
@Override
public void onAdEvent(AdEvent adEvent) {
AdEventType adEventType = adEvent.getType();
if (DEBUG && adEventType != AdEventType.AD_PROGRESS) {
if (configuration.debugModeEnabled && adEventType != AdEventType.AD_PROGRESS) {
Log.d(TAG, "onAdEvent: " + adEventType);
}
try {
@ -1744,7 +1762,7 @@ public final class ImaAdsLoader
@Override
public void onAdError(AdErrorEvent adErrorEvent) {
AdError error = adErrorEvent.getError();
if (DEBUG) {
if (configuration.debugModeEnabled) {
Log.d(TAG, "onAdError", error);
}
if (adsManager == null) {
@ -1753,7 +1771,7 @@ public final class ImaAdsLoader
adPlaybackState = AdPlaybackState.NONE;
hasAdPlaybackState = true;
updateAdPlaybackState();
} else if (isAdGroupLoadError(error)) {
} else if (ImaUtil.isAdGroupLoadError(error)) {
try {
handleAdGroupLoadError(error);
} catch (RuntimeException e) {
@ -1868,8 +1886,11 @@ public final class ImaAdsLoader
}
}
/** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */
private static final class DefaultImaFactory implements ImaFactory {
/**
* Default {@link ImaUtil.ImaFactory} for non-test usage, which delegates to {@link
* ImaSdkFactory}.
*/
private static final class DefaultImaFactory implements ImaUtil.ImaFactory {
@Override
public ImaSdkSettings createImaSdkSettings() {
return ImaSdkFactory.getInstance().createImaSdkSettings();

View File

@ -0,0 +1,206 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.ext.ima;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdError;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
import com.google.ads.interactivemedia.v3.api.AdEvent;
import com.google.ads.interactivemedia.v3.api.AdsLoader;
import com.google.ads.interactivemedia.v3.api.AdsManager;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.CompanionAdSlot;
import com.google.ads.interactivemedia.v3.api.FriendlyObstruction;
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.android.exoplayer2.C;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo;
import com.google.android.exoplayer2.upstream.DataSchemeDataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/** Utilities for working with IMA SDK and IMA extension data types. */
/* package */ final class ImaUtil {
/** Factory for objects provided by the IMA SDK. */
public interface ImaFactory {
/** Creates {@link ImaSdkSettings} for configuring the IMA SDK. */
ImaSdkSettings createImaSdkSettings();
/**
* Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that
* control rendering of ads.
*/
AdsRenderingSettings createAdsRenderingSettings();
/**
* Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for
* non-linear ads, and slots for companion ads.
*/
AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player);
/** Creates an {@link AdDisplayContainer} to hold the player for audio ads. */
AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player);
/**
* Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for
* viewability measurement purposes.
*/
FriendlyObstruction createFriendlyObstruction(
View view,
FriendlyObstructionPurpose friendlyObstructionPurpose,
@Nullable String reasonDetail);
/** Creates an {@link AdsRequest} to contain the data used to request ads. */
AdsRequest createAdsRequest();
/** Creates an {@link AdsLoader} for requesting ads using the specified settings. */
AdsLoader createAdsLoader(
Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer);
}
/** Stores configuration for ad loading and playback. */
public static final class Configuration {
public final long adPreloadTimeoutMs;
public final int vastLoadTimeoutMs;
public final int mediaLoadTimeoutMs;
public final boolean focusSkipButtonWhenAvailable;
public final boolean playAdBeforeStartPosition;
public final int mediaBitrate;
@Nullable public final List<String> adMediaMimeTypes;
@Nullable public final Set<UiElement> adUiElements;
@Nullable public final Collection<CompanionAdSlot> companionAdSlots;
@Nullable public final AdErrorEvent.AdErrorListener applicationAdErrorListener;
@Nullable public final AdEvent.AdEventListener applicationAdEventListener;
@Nullable public final VideoAdPlayer.VideoAdPlayerCallback applicationVideoAdPlayerCallback;
@Nullable public final ImaSdkSettings imaSdkSettings;
public final boolean debugModeEnabled;
public Configuration(
long adPreloadTimeoutMs,
int vastLoadTimeoutMs,
int mediaLoadTimeoutMs,
boolean focusSkipButtonWhenAvailable,
boolean playAdBeforeStartPosition,
int mediaBitrate,
@Nullable List<String> adMediaMimeTypes,
@Nullable Set<UiElement> adUiElements,
@Nullable Collection<CompanionAdSlot> companionAdSlots,
@Nullable AdErrorEvent.AdErrorListener applicationAdErrorListener,
@Nullable AdEvent.AdEventListener applicationAdEventListener,
@Nullable VideoAdPlayer.VideoAdPlayerCallback applicationVideoAdPlayerCallback,
@Nullable ImaSdkSettings imaSdkSettings,
boolean debugModeEnabled) {
this.adPreloadTimeoutMs = adPreloadTimeoutMs;
this.vastLoadTimeoutMs = vastLoadTimeoutMs;
this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable;
this.playAdBeforeStartPosition = playAdBeforeStartPosition;
this.mediaBitrate = mediaBitrate;
this.adMediaMimeTypes = adMediaMimeTypes;
this.adUiElements = adUiElements;
this.companionAdSlots = companionAdSlots;
this.applicationAdErrorListener = applicationAdErrorListener;
this.applicationAdEventListener = applicationAdEventListener;
this.applicationVideoAdPlayerCallback = applicationVideoAdPlayerCallback;
this.imaSdkSettings = imaSdkSettings;
this.debugModeEnabled = debugModeEnabled;
}
}
/**
* Returns the IMA {@link FriendlyObstructionPurpose} corresponding to the given {@link
* OverlayInfo#purpose}.
*/
public static FriendlyObstructionPurpose getFriendlyObstructionPurpose(
@OverlayInfo.Purpose int purpose) {
switch (purpose) {
case OverlayInfo.PURPOSE_CONTROLS:
return FriendlyObstructionPurpose.VIDEO_CONTROLS;
case OverlayInfo.PURPOSE_CLOSE_AD:
return FriendlyObstructionPurpose.CLOSE_AD;
case OverlayInfo.PURPOSE_NOT_VISIBLE:
return FriendlyObstructionPurpose.NOT_VISIBLE;
case OverlayInfo.PURPOSE_OTHER:
default:
return FriendlyObstructionPurpose.OTHER;
}
}
/**
* Returns an initial {@link AdPlaybackState} with ad groups at the provided {@code cuePoints}.
*
* @param cuePoints The cue points of the ads in seconds.
* @return The {@link AdPlaybackState}.
*/
public static AdPlaybackState getInitialAdPlaybackStateForCuePoints(List<Float> cuePoints) {
if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad.
return new AdPlaybackState(/* adGroupTimesUs...= */ 0);
}
int count = cuePoints.size();
long[] adGroupTimesUs = new long[count];
int adGroupIndex = 0;
for (int i = 0; i < count; i++) {
double cuePoint = cuePoints.get(i);
if (cuePoint == -1.0) {
adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE;
} else {
adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint);
}
}
// Cue points may be out of order, so sort them.
Arrays.sort(adGroupTimesUs, 0, adGroupIndex);
return new AdPlaybackState(adGroupTimesUs);
}
/** Returns an {@link AdsRequest} based on the specified ad tag {@link DataSpec}. */
public static AdsRequest getAdsRequestForAdTagDataSpec(
ImaFactory imaFactory, DataSpec adTagDataSpec) throws IOException {
AdsRequest request = imaFactory.createAdsRequest();
if (DataSchemeDataSource.SCHEME_DATA.equals(adTagDataSpec.uri.getScheme())) {
DataSchemeDataSource dataSchemeDataSource = new DataSchemeDataSource();
try {
dataSchemeDataSource.open(adTagDataSpec);
request.setAdsResponse(Util.fromUtf8Bytes(Util.readToEnd(dataSchemeDataSource)));
} finally {
dataSchemeDataSource.close();
}
} else {
request.setAdTagUrl(adTagDataSpec.uri.toString());
}
return request;
}
/** Returns whether the ad error indicates that an entire ad group failed to load. */
public static boolean isAdGroupLoadError(AdError adError) {
// TODO: Find out what other errors need to be handled (if any), and whether each one relates to
// a single ad, ad group or the whole timeline.
return adError.getErrorCode() == AdError.AdErrorCode.VAST_LINEAR_ASSET_MISMATCH
|| adError.getErrorCode() == AdError.AdErrorCode.UNKNOWN_ERROR;
}
private ImaUtil() {}
}

View File

@ -48,13 +48,14 @@ import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo;
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
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.ExoPlaybackException;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader.ImaFactory;
import com.google.android.exoplayer2.ext.ima.ImaUtil.ImaFactory;
import com.google.android.exoplayer2.source.MaskingMediaSource.PlaceholderTimeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.source.ads.AdsLoader;
@ -63,6 +64,8 @@ import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
@ -96,8 +99,9 @@ public final class ImaAdsLoaderTest {
/* isSeekable= */ true, /* isDynamic= */ false, CONTENT_DURATION_US));
private static final long CONTENT_PERIOD_DURATION_US =
CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs;
private static final Uri TEST_URI = Uri.EMPTY;
private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo(TEST_URI.toString());
private static final Uri TEST_URI = Uri.parse("https://www.google.com");
private static final DataSpec TEST_DATA_SPEC = new DataSpec(TEST_URI);
private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo("https://www.google.com");
private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND;
private static final ImmutableList<Float> PREROLL_CUE_POINTS_SECONDS = ImmutableList.of(0f);
@ -284,7 +288,7 @@ public final class ImaAdsLoaderTest {
new AdPlaybackState(/* adGroupTimesUs...= */ 0)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI)
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withAdResumePositionUs(/* adResumePositionUs= */ 0));
@ -311,6 +315,31 @@ public final class ImaAdsLoaderTest {
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
}
@Test
public void playback_withMidrollFetchError_updatesContentProgress() {
AdEvent mockMidrollFetchErrorAdEvent = mock(AdEvent.class);
when(mockMidrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR);
when(mockMidrollFetchErrorAdEvent.getAdData())
.thenReturn(ImmutableMap.of("adBreakTime", "5.5"));
setupPlayback(CONTENT_TIMELINE, ImmutableList.of(5.5f));
// Simulate loading an empty midroll ad and advancing the player position.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
adEventListener.onAdEvent(mockMidrollFetchErrorAdEvent);
long playerPositionUs = CONTENT_DURATION_US - C.MICROS_PER_SECOND;
long playerPositionInPeriodUs =
playerPositionUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
long periodDurationUs =
CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs;
fakeExoPlayer.setPlayingContentPosition(C.usToMs(playerPositionUs));
// Verify the content progress is updated to reflect the new player position.
assertThat(contentProgressProvider.getContentProgress())
.isEqualTo(
new VideoProgressUpdate(
C.usToMs(playerPositionInPeriodUs), C.usToMs(periodDurationUs)));
}
@Test
public void playback_withPostrollFetchError_marksAdAsInErrorState() {
AdEvent mockPostrollFetchErrorAdEvent = mock(AdEvent.class);
@ -352,7 +381,7 @@ public final class ImaAdsLoaderTest {
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@ -376,7 +405,7 @@ public final class ImaAdsLoaderTest {
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
@ -398,7 +427,7 @@ public final class ImaAdsLoaderTest {
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@ -422,7 +451,7 @@ public final class ImaAdsLoaderTest {
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@ -447,7 +476,7 @@ public final class ImaAdsLoaderTest {
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@ -474,7 +503,7 @@ public final class ImaAdsLoaderTest {
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@ -505,7 +534,7 @@ public final class ImaAdsLoaderTest {
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@ -524,7 +553,8 @@ public final class ImaAdsLoaderTest {
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
.build(),
TEST_DATA_SPEC);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@ -537,7 +567,7 @@ public final class ImaAdsLoaderTest {
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withSkippedAdGroup(/* adGroupIndex= */ 0)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@ -556,7 +586,8 @@ public final class ImaAdsLoaderTest {
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
.build(),
TEST_DATA_SPEC);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@ -569,7 +600,7 @@ public final class ImaAdsLoaderTest {
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@ -588,7 +619,8 @@ public final class ImaAdsLoaderTest {
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
.build(),
TEST_DATA_SPEC);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@ -596,7 +628,7 @@ public final class ImaAdsLoaderTest {
verify(mockAdsManager).destroy();
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0)
.withSkippedAdGroup(/* adGroupIndex= */ 1));
@ -624,7 +656,8 @@ public final class ImaAdsLoaderTest {
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
.build(),
TEST_DATA_SPEC);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@ -637,7 +670,7 @@ public final class ImaAdsLoaderTest {
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withSkippedAdGroup(/* adGroupIndex= */ 0)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@ -663,7 +696,8 @@ public final class ImaAdsLoaderTest {
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
.build(),
TEST_DATA_SPEC);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@ -676,16 +710,90 @@ public final class ImaAdsLoaderTest {
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@Test
public void requestAdTagWithDataScheme_requestsWithAdsResponse() throws Exception {
String adsResponse =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<VAST xmlns:xsi=\"https://www.w3.org/2001/XMLSchema-instance\""
+ " xsi:noNamespaceSchemaLocation=\"vast.xsd\" version=\"2.0\">\n"
+ " <Ad id=\"17180293\">\n"
+ " <InLine></InLine>\n"
+ " </Ad>\n"
+ "</VAST>";
DataSpec adDataSpec = new DataSpec(Util.getDataUriForString("text/xml", adsResponse));
setupPlayback(
CONTENT_TIMELINE,
ImmutableList.of(0f),
new ImaAdsLoader.Builder(getApplicationContext())
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.build(),
adDataSpec);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdsRequest).setAdsResponse(adsResponse);
}
@Test
public void requestAdTagWithUri_requestsWithAdTagUrl() throws Exception {
setupPlayback(
CONTENT_TIMELINE,
ImmutableList.of(0f),
new ImaAdsLoader.Builder(getApplicationContext())
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.build(),
TEST_DATA_SPEC);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdsRequest).setAdTagUrl(TEST_DATA_SPEC.uri.toString());
}
@Test
public void setsDefaultMimeTypes() throws Exception {
setupPlayback(CONTENT_TIMELINE, ImmutableList.of(0f));
imaAdsLoader.setSupportedContentTypes(C.TYPE_DASH, C.TYPE_OTHER);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdsRenderingSettings)
.setMimeTypes(
ImmutableList.of(
MimeTypes.APPLICATION_MPD,
MimeTypes.VIDEO_MP4,
MimeTypes.VIDEO_WEBM,
MimeTypes.VIDEO_H263,
MimeTypes.AUDIO_MP4,
MimeTypes.AUDIO_MPEG));
}
@Test
public void buildWithAdMediaMimeTypes_setsMimeTypes() throws Exception {
setupPlayback(
CONTENT_TIMELINE,
ImmutableList.of(0f),
new ImaAdsLoader.Builder(getApplicationContext())
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.setAdMediaMimeTypes(ImmutableList.of(MimeTypes.AUDIO_MPEG))
.build(),
TEST_DATA_SPEC);
imaAdsLoader.setSupportedContentTypes(C.TYPE_OTHER);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdsRenderingSettings).setMimeTypes(ImmutableList.of(MimeTypes.AUDIO_MPEG));
}
@Test
public void stop_unregistersAllVideoControlOverlays() {
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.requestAds(TEST_DATA_SPEC, adViewGroup);
imaAdsLoader.stop();
InOrder inOrder = inOrder(mockAdDisplayContainer);
@ -695,7 +803,8 @@ public final class ImaAdsLoaderTest {
@Test
public void loadAd_withLargeAdCuePoint_updatesAdPlaybackStateWithLoadedAd() {
float midrollTimeSecs = 1_765f;
// Use a large enough value to test correct truncating of large cue points.
float midrollTimeSecs = Float.MAX_VALUE;
ImmutableList<Float> cuePoints = ImmutableList.of(midrollTimeSecs);
setupPlayback(CONTENT_TIMELINE, cuePoints);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@ -735,7 +844,7 @@ public final class ImaAdsLoaderTest {
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI)
@ -749,16 +858,21 @@ public final class ImaAdsLoaderTest {
new ImaAdsLoader.Builder(getApplicationContext())
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
.build(),
TEST_DATA_SPEC);
}
private void setupPlayback(
Timeline contentTimeline, List<Float> cuePoints, ImaAdsLoader imaAdsLoader) {
Timeline contentTimeline,
List<Float> cuePoints,
ImaAdsLoader imaAdsLoader,
DataSpec adTagDataSpec) {
fakeExoPlayer = new FakePlayer();
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline);
when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints);
this.imaAdsLoader = imaAdsLoader;
imaAdsLoader.setPlayer(fakeExoPlayer);
imaAdsLoader.setAdTagDataSpec(adTagDataSpec);
}
private void setupMocks() {

View File

@ -19,13 +19,6 @@ dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.collection:collection:' + androidxCollectionVersion
implementation 'androidx.concurrent:concurrent-futures:1.1.0'
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
api 'androidx.media2:media2-session:1.0.3'
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion

View File

@ -23,13 +23,13 @@ import com.google.android.exoplayer2.MediaItem;
*/
public interface MediaItemConverter {
/**
* Converts an {@link androidx.media2.common.MediaItem Media2 MediaItem} to an {@link MediaItem
* Converts a {@link androidx.media2.common.MediaItem Media2 MediaItem} to an {@link MediaItem
* ExoPlayer MediaItem}.
*/
MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem media2MediaItem);
/**
* Converts an {@link MediaItem ExoPlayer MediaItem} to an {@link androidx.media2.common.MediaItem
* Converts an {@link MediaItem ExoPlayer MediaItem} to a {@link androidx.media2.common.MediaItem
* Media2 MediaItem}.
*/
androidx.media2.common.MediaItem convertToMedia2MediaItem(MediaItem exoPlayerMediaItem);

View File

@ -16,13 +16,6 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
testImplementation project(modulePrefix + 'testutils')

View File

@ -18,16 +18,6 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
implementation project(modulePrefix + 'library-core')
implementation 'androidx.work:work-runtime:2.4.0'
// Guava & Gradle interact badly, and this prevents
// "cannot access ListenableFuture" errors [internal b/157225611].
// More info: https://blog.gradle.org/guava
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
}

View File

@ -16,14 +16,16 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
android.buildTypes.debug.testCoverageEnabled true
dependencies {
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation ('com.google.guava:guava:' + guavaVersion) {
api ('com.google.guava:guava:' + guavaVersion) {
// Exclude dependencies that are only used by Guava at compile time
// (but declared as runtime deps) [internal b/168188131].
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion

View File

@ -4,3 +4,6 @@
-dontwarn org.checkerframework.**
-dontwarn kotlin.annotations.jvm.**
-dontwarn javax.annotation.**
# From https://github.com/google/guava/wiki/UsingProGuardWithGuava
-dontwarn java.lang.ClassValue

View File

@ -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.0";
public static final String VERSION = "2.12.1";
/** 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.0";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.12.1";
/**
* 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 = 2012000;
public static final int VERSION_INT = 2012001;
/** The default user agent for requests made by the library. */
public static final String DEFAULT_USER_AGENT =

View File

@ -669,6 +669,10 @@ public final class MediaItem {
@Nullable public final String language;
/** The selection flags. */
@C.SelectionFlags public final int selectionFlags;
/** The role flags. */
@C.RoleFlags public final int roleFlags;
/** The label. */
@Nullable public final String label;
/**
* Creates an instance.
@ -682,7 +686,7 @@ public final class MediaItem {
}
/**
* Creates an instance with the given selection flags.
* Creates an instance.
*
* @param uri The {@link Uri URI} to the subtitle file.
* @param mimeType The MIME type.
@ -691,10 +695,32 @@ public final class MediaItem {
*/
public Subtitle(
Uri uri, String mimeType, @Nullable String language, @C.SelectionFlags int selectionFlags) {
this(uri, mimeType, language, selectionFlags, /* roleFlags= */ 0, /* label= */ null);
}
/**
* Creates an instance.
*
* @param uri The {@link Uri URI} to the subtitle file.
* @param mimeType The MIME type.
* @param language The optional language.
* @param selectionFlags The selection flags.
* @param roleFlags The role flags.
* @param label The optional label.
*/
public Subtitle(
Uri uri,
String mimeType,
@Nullable String language,
@C.SelectionFlags int selectionFlags,
@C.RoleFlags int roleFlags,
@Nullable String label) {
this.uri = uri;
this.mimeType = mimeType;
this.language = language;
this.selectionFlags = selectionFlags;
this.roleFlags = roleFlags;
this.label = label;
}
@Override
@ -711,7 +737,9 @@ public final class MediaItem {
return uri.equals(other.uri)
&& mimeType.equals(other.mimeType)
&& Util.areEqual(language, other.language)
&& selectionFlags == other.selectionFlags;
&& selectionFlags == other.selectionFlags
&& roleFlags == other.roleFlags
&& Util.areEqual(label, other.label);
}
@Override
@ -720,6 +748,8 @@ public final class MediaItem {
result = 31 * result + mimeType.hashCode();
result = 31 * result + (language == null ? 0 : language.hashCode());
result = 31 * result + selectionFlags;
result = 31 * result + roleFlags;
result = 31 * result + (label == null ? 0 : label.hashCode());
return result;
}
}

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util;
import static android.content.Context.UI_MODE_SERVICE;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static java.lang.Math.abs;
import static java.lang.Math.max;
import static java.lang.Math.min;
@ -47,6 +48,7 @@ import android.os.SystemClock;
import android.security.NetworkSecurityPolicy;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Base64;
import android.view.Display;
import android.view.SurfaceView;
import android.view.WindowManager;
@ -530,6 +532,54 @@ public final class Util {
return Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, threadName));
}
/**
* Reads data from the specified opened {@link DataSource} until it ends, and returns a byte array
* containing the read data.
*
* @param dataSource The source from which to read.
* @return The concatenation of all read data.
* @throws IOException If an error occurs reading from the source.
*/
public static byte[] readToEnd(DataSource dataSource) throws IOException {
byte[] data = new byte[1024];
int position = 0;
int bytesRead = 0;
while (bytesRead != C.RESULT_END_OF_INPUT) {
if (position == data.length) {
data = Arrays.copyOf(data, data.length * 2);
}
bytesRead = dataSource.read(data, position, data.length - position);
if (bytesRead != C.RESULT_END_OF_INPUT) {
position += bytesRead;
}
}
return Arrays.copyOf(data, position);
}
/**
* Reads {@code length} bytes from the specified opened {@link DataSource}, and returns a byte
* array containing the read data.
*
* @param dataSource The source from which to read.
* @return The read data.
* @throws IOException If an error occurs reading from the source.
* @throws IllegalStateException If the end of the source was reached before {@code length} bytes
* could be read.
*/
public static byte[] readExactly(DataSource dataSource, int length) throws IOException {
byte[] data = new byte[length];
int position = 0;
while (position < length) {
int bytesRead = dataSource.read(data, position, data.length - position);
if (bytesRead == C.RESULT_END_OF_INPUT) {
throw new IllegalStateException(
"Not enough data could be read: " + position + " < " + length);
}
position += bytesRead;
}
return data;
}
/**
* Closes a {@link DataSource}, suppressing any {@link IOException} that may occur.
*
@ -1844,13 +1894,16 @@ public final class Util {
if (timeMs == C.TIME_UNSET) {
timeMs = 0;
}
String prefix = timeMs < 0 ? "-" : "";
timeMs = abs(timeMs);
long totalSeconds = (timeMs + 500) / 1000;
long seconds = totalSeconds % 60;
long minutes = (totalSeconds / 60) % 60;
long hours = totalSeconds / 3600;
builder.setLength(0);
return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
: formatter.format("%02d:%02d", minutes, seconds).toString();
return hours > 0
? formatter.format("%s%d:%02d:%02d", prefix, hours, minutes, seconds).toString()
: formatter.format("%s%02d:%02d", prefix, minutes, seconds).toString();
}
/**
@ -1952,6 +2005,14 @@ public final class Util {
return builder.toString();
}
/** Returns a data URI with the specified MIME type and data. */
public static Uri getDataUriForString(String mimeType, String data) {
// TODO(internal: b/169937045): For now we don't pass the URL_SAFE flag as DataSchemeDataSource
// doesn't decode using it.
return Uri.parse(
"data:" + mimeType + ";base64," + Base64.encodeToString(data.getBytes(), Base64.NO_WRAP));
}
/**
* A hacky method that always throws {@code t} even if {@code t} is a checked exception,
* and is not declared to be thrown.

View File

@ -173,7 +173,14 @@ public class MediaItemTest {
Uri.parse(URI_STRING + "/de"),
MimeTypes.APPLICATION_TTML,
/* language= */ null,
C.SELECTION_FLAG_DEFAULT));
C.SELECTION_FLAG_DEFAULT),
new MediaItem.Subtitle(
Uri.parse(URI_STRING + "/fr"),
MimeTypes.APPLICATION_SUBRIP,
/* language= */ "fr",
C.SELECTION_FLAG_DEFAULT,
C.ROLE_FLAG_ALTERNATE,
"label"));
MediaItem mediaItem =
new MediaItem.Builder().setUri(URI_STRING).setSubtitles(subtitles).build();
@ -317,7 +324,10 @@ public class MediaItemTest {
new MediaItem.Subtitle(
Uri.parse(URI_STRING + "/en"),
MimeTypes.APPLICATION_TTML,
/* language= */ "en")))
/* language= */ "en",
C.SELECTION_FLAG_FORCED,
C.ROLE_FLAG_ALTERNATE,
"label")))
.setTag(new Object())
.build();

View File

@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.util.Util.binarySearchCeil;
import static com.google.android.exoplayer2.util.Util.binarySearchFloor;
import static com.google.android.exoplayer2.util.Util.escapeFileName;
import static com.google.android.exoplayer2.util.Util.getCodecsOfType;
import static com.google.android.exoplayer2.util.Util.getStringForTime;
import static com.google.android.exoplayer2.util.Util.parseXsDateTime;
import static com.google.android.exoplayer2.util.Util.parseXsDuration;
import static com.google.android.exoplayer2.util.Util.unescapeFileName;
@ -37,6 +38,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Formatter;
import java.util.Random;
import java.util.zip.Deflater;
import org.junit.Test;
@ -874,6 +876,14 @@ public class UtilTest {
}
}
@Test
public void getDataUriForString_returnsCorrectDataUri() {
assertThat(
Util.getDataUriForString(/* mimeType= */ "text/plain", "Some Data!<>:\"/\\|?*%")
.toString())
.isEqualTo("data:text/plain;base64,U29tZSBEYXRhITw+OiIvXHw/KiU=");
}
@Test
public void crc32_returnsUpdatedCrc32() {
byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F};
@ -1082,6 +1092,12 @@ public class UtilTest {
assertThat(Util.tableExists(database, "table")).isFalse();
}
@Test
public void getStringForTime_withNegativeTime_setsNegativePrefix() {
assertThat(getStringForTime(new StringBuilder(), new Formatter(), /* timeMs= */ -35000))
.isEqualTo("-00:35");
}
private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) {
assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName);
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);

View File

@ -37,40 +37,20 @@ dependencies {
api project(modulePrefix + 'library-common')
api project(modulePrefix + 'library-extractor')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
androidTestImplementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestImplementation(project(modulePrefix + 'testutils')) {
exclude module: modulePrefix.substring(1) + 'library-core'
}
testImplementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils')
testImplementation project(modulePrefix + 'robolectricutils')
}
ext {

View File

@ -71,8 +71,3 @@
-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
<init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
}
# Don't warn about checkerframework and Kotlin annotations
-dontwarn org.checkerframework.**
-dontwarn kotlin.annotations.jvm.**
-dontwarn javax.annotation.**

View File

@ -44,12 +44,6 @@ public abstract class BasePlayer implements Player {
setMediaItems(Collections.singletonList(mediaItem), resetPosition);
}
@Override
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
setMediaItems(
mediaItems, /* startWindowIndex= */ C.INDEX_UNSET, /* startPositionMs= */ C.TIME_UNSET);
}
@Override
public void setMediaItems(List<MediaItem> mediaItems) {
setMediaItems(mediaItems, /* resetPosition= */ true);

View File

@ -16,11 +16,13 @@
package com.google.android.exoplayer2;
import android.content.Context;
import android.media.AudioTrack;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioSink;
import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
@ -622,14 +624,13 @@ public interface ExoPlayer extends Player {
* the following:
*
* <ul>
* <li>audio offload rendering is enabled in {@link
* <li>Audio offload rendering is enabled in {@link
* DefaultRenderersFactory#setEnableAudioOffload} or the equivalent option passed to {@link
* com.google.android.exoplayer2.audio.DefaultAudioSink#DefaultAudioSink(AudioCapabilities,
* DefaultAudioSink#DefaultAudioSink(AudioCapabilities,
* DefaultAudioSink.AudioProcessorChain, boolean, boolean, boolean)}.
* <li>an audio track is playing in a format which the device supports offloading (for example,
* <li>An audio track is playing in a format that the device supports offloading (for example,
* MP3 or AAC).
* <li>The {@link com.google.android.exoplayer2.audio.AudioSink} is playing with an offload
* {@link android.media.AudioTrack}.
* <li>The {@link AudioSink} is playing with an offload {@link AudioTrack}.
* </ul>
*
* <p>This method is experimental, and will be renamed or removed in a future release.

View File

@ -153,7 +153,7 @@ import java.util.concurrent.TimeoutException;
new TrackSelectorResult(
new RendererConfiguration[renderers.length],
new TrackSelection[renderers.length],
null);
/* info= */ null);
period = new Timeline.Period();
maskingWindowIndex = C.INDEX_UNSET;
playbackInfoUpdateHandler = new Handler(applicationLooper);
@ -347,6 +347,11 @@ import java.util.concurrent.TimeoutException;
prepare();
}
@Override
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
setMediaSources(createMediaSources(mediaItems), resetPosition);
}
@Override
public void setMediaItems(
List<MediaItem> mediaItems, int startWindowIndex, long startPositionMs) {

View File

@ -897,10 +897,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
// tracks in the current period have uneven durations and are still being read by another
// renderer. See: https://github.com/google/ExoPlayer/issues/1874.
boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream();
boolean isWaitingForNextStream =
!isReadingAhead
&& playingPeriodHolder.getNext() != null
&& renderer.hasReadStreamToEnd();
boolean isWaitingForNextStream = !isReadingAhead && renderer.hasReadStreamToEnd();
boolean allowsPlayback =
isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded();
renderersAllowPlayback = renderersAllowPlayback && allowsPlayback;

View File

@ -315,8 +315,8 @@ import com.google.common.collect.ImmutableList;
public boolean updateQueuedPeriods(
Timeline timeline, long rendererPositionUs, long maxRendererReadPositionUs) {
// TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline
// is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be
// handled here.
// is set, once all cases handled by ExoPlayerImplInternal.handleMediaSourceListInfoRefreshed
// can be handled here.
MediaPeriodHolder previousPeriodHolder = null;
MediaPeriodHolder periodHolder = playing;
while (periodHolder != null) {
@ -326,8 +326,8 @@ import com.google.common.collect.ImmutableList;
MediaPeriodInfo newPeriodInfo;
if (previousPeriodHolder == null) {
// The id and start position of the first period have already been verified by
// ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline
// and isLastInPeriod flags.
// ExoPlayerImplInternal.handleMediaSourceListInfoRefreshed. Just update duration,
// isLastInTimeline and isLastInPeriod flags.
newPeriodInfo = getUpdatedMediaPeriodInfo(timeline, oldPeriodInfo);
} else {
newPeriodInfo =

View File

@ -289,7 +289,10 @@ import java.lang.reflect.Method;
if (elapsedSincePreviousModeUs < MODE_SWITCH_SMOOTHING_DURATION_US) {
// Use a ramp to smooth between the old mode and the new one to avoid introducing a sudden
// jump if the two modes disagree.
long previousModeProjectedPositionUs = previousModePositionUs + elapsedSincePreviousModeUs;
long previousModeProjectedPositionUs =
previousModePositionUs
+ Util.getMediaDurationForPlayoutDuration(
elapsedSincePreviousModeUs, audioTrackPlaybackSpeed);
// A ramp consisting of 1000 points distributed over MODE_SWITCH_SMOOTHING_DURATION_US.
long rampPoint = (elapsedSincePreviousModeUs * 1000) / MODE_SWITCH_SMOOTHING_DURATION_US;
positionUs *= rampPoint;

View File

@ -28,6 +28,7 @@ import android.os.Handler;
import android.os.SystemClock;
import android.util.Pair;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
@ -335,6 +336,7 @@ public final class DefaultAudioSink implements AudioSink {
private boolean tunneling;
private long lastFeedElapsedRealtimeMs;
private boolean offloadDisabledUntilNextConfiguration;
private boolean isWaitingForOffloadEndOfStreamHandled;
/**
* Creates a new default audio sink.
@ -711,6 +713,7 @@ public final class DefaultAudioSink implements AudioSink {
audioTrack.setOffloadEndOfStream();
audioTrack.setOffloadDelayPadding(
configuration.inputFormat.encoderDelay, configuration.inputFormat.encoderPadding);
isWaitingForOffloadEndOfStreamHandled = true;
}
}
// Re-apply playback parameters.
@ -931,14 +934,27 @@ public final class DefaultAudioSink implements AudioSink {
throw new WriteException(bytesWritten);
}
if (isOffloadedPlayback(audioTrack)) {
// After calling AudioTrack.setOffloadEndOfStream, the AudioTrack internally stops and
// restarts during which AudioTrack.write will return 0. This situation must be detected to
// prevent reporting the buffer as full even though it is not which could lead ExoPlayer to
// sleep forever waiting for a onDataRequest that will never come.
if (writtenEncodedFrames > 0) {
isWaitingForOffloadEndOfStreamHandled = false;
}
// Consider the offload buffer as full if the AudioTrack is playing and AudioTrack.write could
// not write all the data provided to it. This relies on the assumption that AudioTrack.write
// always writes as much as possible.
if (playing
&& listener != null
&& bytesWritten < bytesRemaining
&& isOffloadedPlayback(audioTrack)) {
&& !isWaitingForOffloadEndOfStreamHandled) {
long pendingDurationMs =
audioTrackPositionTracker.getPendingBufferDurationMs(writtenEncodedFrames);
listener.onOffloadBufferFull(pendingDurationMs);
}
}
if (configuration.outputMode == OUTPUT_MODE_PCM) {
writtenPcmBytes += bytesWritten;
@ -1220,6 +1236,7 @@ public final class DefaultAudioSink implements AudioSink {
submittedEncodedFrames = 0;
writtenPcmBytes = 0;
writtenEncodedFrames = 0;
isWaitingForOffloadEndOfStreamHandled = false;
framesPerEncodedSample = 0;
mediaPositionParameters =
new MediaPositionParameters(
@ -1679,13 +1696,17 @@ public final class DefaultAudioSink implements AudioSink {
}
@RequiresApi(29)
private final class StreamEventCallbackV29 extends AudioTrack.StreamEventCallback {
private final class StreamEventCallbackV29 {
private final Handler handler;
private final AudioTrack.StreamEventCallback callback;
public StreamEventCallbackV29() {
handler = new Handler();
}
// Avoid StreamEventCallbackV29 inheriting directly from AudioTrack.StreamEventCallback as it
// would cause a NoClassDefFoundError warning on load of DefaultAudioSink for SDK < 29.
// See: https://github.com/google/ExoPlayer/issues/8058
callback =
new AudioTrack.StreamEventCallback() {
@Override
public void onDataRequest(AudioTrack track, int size) {
Assertions.checkState(track == DefaultAudioSink.this.audioTrack);
@ -1694,12 +1715,24 @@ public final class DefaultAudioSink implements AudioSink {
}
}
@Override
public void onTearDown(@NonNull AudioTrack track) {
if (listener != null && playing) {
// A new Audio Track needs to be created and it's buffer filled, which will be done
// on the next handleBuffer call. Request this call explicitly in case ExoPlayer is
// sleeping waiting for a data request.
listener.onOffloadBufferEmptying();
}
}
};
}
public void register(AudioTrack audioTrack) {
audioTrack.registerStreamEventCallback(handler::post, this);
audioTrack.registerStreamEventCallback(handler::post, callback);
}
public void unregister(AudioTrack audioTrack) {
audioTrack.unregisterStreamEventCallback(this);
audioTrack.unregisterStreamEventCallback(callback);
handler.removeCallbacksAndMessages(/* token= */ null);
}
}

View File

@ -363,6 +363,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@Nullable private DrmSession sourceDrmSession;
@Nullable private MediaCrypto mediaCrypto;
private boolean mediaCryptoRequiresSecureDecoder;
private long renderTimeLimitMs;
private float operatingRate;
@Nullable private MediaCodec codec;
@Nullable private MediaCodecAdapter codecAdapter;
@ -442,6 +443,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
outputBufferInfo = new MediaCodec.BufferInfo();
operatingRate = 1f;
mediaCodecOperationMode = OPERATION_MODE_SYNCHRONOUS;
renderTimeLimitMs = C.TIME_UNSET;
pendingOutputStreamStartPositionsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
@ -451,6 +453,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
resetCodecStateForRelease();
}
/**
* Set a limit on the time a single {@link #render(long, long)} call can spend draining and
* filling the decoder.
*
* <p>This method should be called right after creating an instance of this class.
*
* @param renderTimeLimitMs The render time limit in milliseconds, or {@link C#TIME_UNSET} for no
* limit.
*/
public void setRenderTimeLimitMs(long renderTimeLimitMs) {
this.renderTimeLimitMs = renderTimeLimitMs;
}
/**
* Set the mode of operation of the underlying {@link MediaCodec}.
*
@ -837,9 +852,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
while (bypassRender(positionUs, elapsedRealtimeUs)) {}
TraceUtil.endSection();
} else if (codec != null) {
long renderStartTimeMs = SystemClock.elapsedRealtime();
TraceUtil.beginSection("drainAndFeed");
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
while (feedInputBuffer()) {}
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)
&& shouldContinueRendering(renderStartTimeMs)) {}
while (feedInputBuffer() && shouldContinueRendering(renderStartTimeMs)) {}
TraceUtil.endSection();
} else {
decoderCounters.skippedInputBufferCount += skipSource(positionUs);
@ -1171,6 +1188,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
onCodecInitialized(codecName, codecInitializedTimestamp, elapsed);
}
private boolean shouldContinueRendering(long renderStartTimeMs) {
return renderTimeLimitMs == C.TIME_UNSET
|| SystemClock.elapsedRealtime() - renderStartTimeMs < renderTimeLimitMs;
}
private void getCodecBuffers(MediaCodec codec) {
if (Util.SDK_INT < 21) {
inputBuffers = codec.getInputBuffers();

View File

@ -29,6 +29,7 @@ import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
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.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
@ -280,7 +281,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
private MediaSource maybeWrapWithAdsMediaSource(MediaItem mediaItem, MediaSource mediaSource) {
Assertions.checkNotNull(mediaItem.playbackProperties);
if (mediaItem.playbackProperties.adTagUri == null) {
@Nullable Uri adTagUri = mediaItem.playbackProperties.adTagUri;
if (adTagUri == null) {
return mediaSource;
}
AdsLoaderProvider adsLoaderProvider = this.adsLoaderProvider;
@ -292,14 +294,17 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
+ " setAdViewProvider.");
return mediaSource;
}
@Nullable
AdsLoader adsLoader = adsLoaderProvider.getAdsLoader(mediaItem.playbackProperties.adTagUri);
@Nullable AdsLoader adsLoader = adsLoaderProvider.getAdsLoader(adTagUri);
if (adsLoader == null) {
Log.w(TAG, "Playing media without ads. No AdsLoader for provided adTagUri");
return mediaSource;
}
return new AdsMediaSource(
mediaSource, /* adMediaSourceFactory= */ this, adsLoader, adViewProvider);
mediaSource,
new DataSpec(adTagUri),
/* adMediaSourceFactory= */ this,
adsLoader,
adViewProvider);
}
private static SparseArray<MediaSourceFactory> loadDelegates(

View File

@ -340,7 +340,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
/* manifest= */ null,
mediaItem);
if (timelineIsPlaceholder) {
// TODO: Actually prepare the extractors during prepatation so that we don't need a
// TODO: Actually prepare the extractors during preparation so that we don't need a
// placeholder. See https://github.com/google/ExoPlayer/issues/4727.
timeline =
new ForwardingTimeline(timeline) {

View File

@ -281,6 +281,8 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
.setSampleMimeType(subtitle.mimeType)
.setLanguage(subtitle.language)
.setSelectionFlags(subtitle.selectionFlags)
.setRoleFlags(subtitle.roleFlags)
.setLabel(subtitle.label)
.build();
dataSpec =
new DataSpec.Builder().setUri(subtitle.uri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build();

View File

@ -198,6 +198,14 @@ public interface AdsLoader {
*/
void setSupportedContentTypes(@C.ContentType int... contentTypes);
/**
* Sets the data spec of the ad tag to load.
*
* @param adTagDataSpec The data spec of the ad tag to load. See the implementation's
* documentation for information about compatible ad tag formats.
*/
void setAdTagDataSpec(DataSpec adTagDataSpec);
/**
* Starts using the ads loader for playback. Called on the main thread by {@link AdsMediaSource}.
*

View File

@ -128,6 +128,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
private final MediaSourceFactory adMediaSourceFactory;
private final AdsLoader adsLoader;
private final AdsLoader.AdViewProvider adViewProvider;
@Nullable private final DataSpec adTagDataSpec;
private final Handler mainHandler;
private final Timeline.Period period;
@ -145,7 +146,10 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
* @param dataSourceFactory Factory for data sources used to load ad media.
* @param adsLoader The loader for ads.
* @param adViewProvider Provider of views for the ad UI.
* @deprecated Use {@link AdsMediaSource#AdsMediaSource(MediaSource, DataSpec, MediaSourceFactory,
* AdsLoader, AdsLoader.AdViewProvider)} instead.
*/
@Deprecated
public AdsMediaSource(
MediaSource contentMediaSource,
DataSource.Factory dataSourceFactory,
@ -155,7 +159,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
contentMediaSource,
new ProgressiveMediaSource.Factory(dataSourceFactory),
adsLoader,
adViewProvider);
adViewProvider,
/* adTagDataSpec= */ null);
}
/**
@ -166,16 +171,53 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
* @param adMediaSourceFactory Factory for media sources used to load ad media.
* @param adsLoader The loader for ads.
* @param adViewProvider Provider of views for the ad UI.
* @deprecated Use {@link AdsMediaSource#AdsMediaSource(MediaSource, DataSpec, MediaSourceFactory,
* AdsLoader, AdsLoader.AdViewProvider)} instead.
*/
@Deprecated
public AdsMediaSource(
MediaSource contentMediaSource,
MediaSourceFactory adMediaSourceFactory,
AdsLoader adsLoader,
AdsLoader.AdViewProvider adViewProvider) {
this(
contentMediaSource,
adMediaSourceFactory,
adsLoader,
adViewProvider,
/* adTagDataSpec= */ null);
}
/**
* Constructs a new source that inserts ads linearly with the content specified by {@code
* contentMediaSource}.
*
* @param contentMediaSource The {@link MediaSource} providing the content to play.
* @param adTagDataSpec The data specification of the ad tag to load.
* @param adMediaSourceFactory Factory for media sources used to load ad media.
* @param adsLoader The loader for ads.
* @param adViewProvider Provider of views for the ad UI.
*/
public AdsMediaSource(
MediaSource contentMediaSource,
DataSpec adTagDataSpec,
MediaSourceFactory adMediaSourceFactory,
AdsLoader adsLoader,
AdsLoader.AdViewProvider adViewProvider) {
this(contentMediaSource, adMediaSourceFactory, adsLoader, adViewProvider, adTagDataSpec);
}
private AdsMediaSource(
MediaSource contentMediaSource,
MediaSourceFactory adMediaSourceFactory,
AdsLoader adsLoader,
AdsLoader.AdViewProvider adViewProvider,
@Nullable DataSpec adTagDataSpec) {
this.contentMediaSource = contentMediaSource;
this.adMediaSourceFactory = adMediaSourceFactory;
this.adsLoader = adsLoader;
this.adViewProvider = adViewProvider;
this.adTagDataSpec = adTagDataSpec;
mainHandler = new Handler(Looper.getMainLooper());
period = new Timeline.Period();
adMediaSourceHolders = new AdMediaSourceHolder[0][];
@ -204,7 +246,13 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
ComponentListener componentListener = new ComponentListener();
this.componentListener = componentListener;
prepareChildSource(CHILD_SOURCE_MEDIA_PERIOD_ID, contentMediaSource);
mainHandler.post(() -> adsLoader.start(componentListener, adViewProvider));
mainHandler.post(
() -> {
if (adTagDataSpec != null) {
adsLoader.setAdTagDataSpec(adTagDataSpec);
}
adsLoader.start(componentListener, adViewProvider);
});
}
@Override

View File

@ -325,10 +325,13 @@ public final class TextRenderer extends BaseRenderer implements Callback {
}
private long getNextEventTime() {
if (nextSubtitleEventIndex == C.INDEX_UNSET) {
return Long.MAX_VALUE;
}
checkNotNull(subtitle);
return nextSubtitleEventIndex == C.INDEX_UNSET
|| nextSubtitleEventIndex >= subtitle.getEventTimeCount()
? Long.MAX_VALUE : subtitle.getEventTime(nextSubtitleEventIndex);
return nextSubtitleEventIndex >= subtitle.getEventTimeCount()
? Long.MAX_VALUE
: subtitle.getEventTime(nextSubtitleEventIndex);
}
private void updateOutput(List<Cue> cues) {

View File

@ -263,8 +263,9 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
SsaStyle.Overrides styleOverrides = SsaStyle.Overrides.parseFromDialogue(rawText);
String text =
SsaStyle.Overrides.stripStyleOverrides(rawText)
.replaceAll("\\\\N", "\n")
.replaceAll("\\\\n", "\n");
.replace("\\N", "\n")
.replace("\\n", "\n")
.replace("\\h", "\u00A0");
Cue cue = createCue(text, style, styleOverrides, screenWidth, screenHeight);
int startTimeIndex = addCuePlacerholderByTime(startTimeUs, cueTimesUs, cues);

View File

@ -339,14 +339,15 @@ public abstract class MappingTrackSelector extends TrackSelector {
* Returns the mapping information for the currently active track selection, or null if no
* selection is currently active.
*/
public final @Nullable MappedTrackInfo getCurrentMappedTrackInfo() {
@Nullable
public final MappedTrackInfo getCurrentMappedTrackInfo() {
return currentMappedTrackInfo;
}
// TrackSelector implementation.
@Override
public final void onSelectionActivated(Object info) {
public final void onSelectionActivated(@Nullable Object info) {
currentMappedTrackInfo = (MappedTrackInfo) info;
}

View File

@ -137,7 +137,7 @@ public abstract class TrackSelector {
*
* @param info The value of {@link TrackSelectorResult#info} in the activated selection.
*/
public abstract void onSelectionActivated(Object info);
public abstract void onSelectionActivated(@Nullable Object info);
/**
* Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously

View File

@ -40,19 +40,20 @@ public final class TrackSelectorResult {
* An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)}
* should the selections be activated.
*/
public final Object info;
@Nullable public final Object info;
/**
* @param rendererConfigurations A {@link RendererConfiguration} for each renderer. A null entry
* indicates the corresponding renderer should be disabled.
* @param selections A {@link TrackSelectionArray} containing the selection for each renderer.
* @param info An opaque object that will be returned to {@link
* TrackSelector#onSelectionActivated(Object)} should the selection be activated.
* TrackSelector#onSelectionActivated(Object)} should the selection be activated. May be
* {@code null}.
*/
public TrackSelectorResult(
@NullableType RendererConfiguration[] rendererConfigurations,
@NullableType TrackSelection[] selections,
Object info) {
@Nullable Object info) {
this.rendererConfigurations = rendererConfigurations;
this.selections = new TrackSelectionArray(selections);
this.info = info;

View File

@ -59,7 +59,8 @@ public final class DataSchemeDataSource extends BaseDataSource {
String dataString = uriParts[1];
if (uriParts[0].contains(";base64")) {
try {
data = Base64.decode(dataString, 0);
// TODO(internal: b/169937045): Consider passing Base64.URL_SAFE flag.
data = Base64.decode(dataString, /* flags= */ Base64.DEFAULT);
} catch (IllegalArgumentException e) {
throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e);
}

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.upstream;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.Nullable;
@ -39,6 +40,9 @@ import java.util.Map;
* <li>rawresource: For fetching data from a raw resource in the application's apk (e.g.
* rawresource:///resourceId, where rawResourceId is the integer identifier of the raw
* resource).
* <li>android.resource: For fetching data in the application's apk (e.g.
* android.resource:///resourceId or android.resource://resourceType/resourceName). See {@link
* RawResourceDataSource} for more information about the URI form.
* <li>content: For fetching data from a content URI (e.g. content://authority/path/123).
* <li>rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an
* explicit dependency on ExoPlayer's RTMP extension.
@ -58,7 +62,9 @@ public final class DefaultDataSource implements DataSource {
private static final String SCHEME_CONTENT = "content";
private static final String SCHEME_RTMP = "rtmp";
private static final String SCHEME_UDP = "udp";
private static final String SCHEME_DATA = DataSchemeDataSource.SCHEME_DATA;
private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME;
private static final String SCHEME_ANDROID_RESOURCE = ContentResolver.SCHEME_ANDROID_RESOURCE;
private final Context context;
private final List<TransferListener> transferListeners;
@ -182,9 +188,9 @@ public final class DefaultDataSource implements DataSource {
dataSource = getRtmpDataSource();
} else if (SCHEME_UDP.equals(scheme)) {
dataSource = getUdpDataSource();
} else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) {
} else if (SCHEME_DATA.equals(scheme)) {
dataSource = getDataSchemeDataSource();
} else if (SCHEME_RAW.equals(scheme)) {
} else if (SCHEME_RAW.equals(scheme) || SCHEME_ANDROID_RESOURCE.equals(scheme)) {
dataSource = getRawResourceDataSource();
} else {
dataSource = baseDataSource;

View File

@ -72,9 +72,12 @@ public class DefaultLoadErrorHandlingPolicy implements LoadErrorHandlingPolicy {
IOException exception = loadErrorInfo.exception;
if (exception instanceof InvalidResponseCodeException) {
int responseCode = ((InvalidResponseCodeException) exception).responseCode;
return responseCode == 404 // HTTP 404 Not Found.
return responseCode == 403 // HTTP 403 Forbidden.
|| responseCode == 404 // HTTP 404 Not Found.
|| responseCode == 410 // HTTP 410 Gone.
|| responseCode == 416 // HTTP 416 Range Not Satisfiable.
|| responseCode == 500 // HTTP 500 Internal Server Error.
|| responseCode == 503 // HTTP 503 Service Unavailable.
? DEFAULT_TRACK_BLACKLIST_MS
: C.TIME_UNSET;
}

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.min;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
@ -34,9 +35,20 @@ import java.io.InputStream;
/**
* A {@link DataSource} for reading a raw resource inside the APK.
*
* <p>URIs supported by this source are of the form {@code rawresource:///rawResourceId}, where
* rawResourceId is the integer identifier of a raw resource. {@link #buildRawResourceUri(int)} can
* be used to build {@link Uri}s in this format.
* <p>URIs supported by this source are of one of the forms:
*
* <ul>
* <li>{@code rawresource:///id}, where {@code id} is the integer identifier of a raw resource.
* <li>{@code android.resource:///id}, where {@code id} is the integer identifier of a raw
* resource.
* <li>{@code android.resource://[package]/[type/]name}, where {@code package} is the name of the
* package in which the resource is located, {@code type} is the resource type and {@code
* name} is the resource name. The package and the type are optional. Their default value is
* the package of this application and "raw", respectively. Using the two other forms is more
* efficient.
* </ul>
*
* <p>{@link #buildRawResourceUri(int)} can be used to build supported {@link Uri}s.
*/
public final class RawResourceDataSource extends BaseDataSource {
@ -67,6 +79,7 @@ public final class RawResourceDataSource extends BaseDataSource {
public static final String RAW_RESOURCE_SCHEME = "rawresource";
private final Resources resources;
private final String packageName;
@Nullable private Uri uri;
@Nullable private AssetFileDescriptor assetFileDescriptor;
@ -80,23 +93,44 @@ public final class RawResourceDataSource extends BaseDataSource {
public RawResourceDataSource(Context context) {
super(/* isNetwork= */ false);
this.resources = context.getResources();
this.packageName = context.getPackageName();
}
@Override
public long open(DataSpec dataSpec) throws RawResourceDataSourceException {
try {
Uri uri = dataSpec.uri;
this.uri = uri;
if (!TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())) {
throw new RawResourceDataSourceException("URI must use scheme " + RAW_RESOURCE_SCHEME);
}
int resourceId;
if (TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())
|| (TextUtils.equals(ContentResolver.SCHEME_ANDROID_RESOURCE, uri.getScheme())
&& uri.getPathSegments().size() == 1
&& Assertions.checkNotNull(uri.getLastPathSegment()).matches("\\d+"))) {
try {
resourceId = Integer.parseInt(Assertions.checkNotNull(uri.getLastPathSegment()));
} catch (NumberFormatException e) {
throw new RawResourceDataSourceException("Resource identifier must be an integer.");
}
} else if (TextUtils.equals(ContentResolver.SCHEME_ANDROID_RESOURCE, uri.getScheme())) {
String path = Assertions.checkNotNull(uri.getPath());
if (path.startsWith("/")) {
path = path.substring(1);
}
@Nullable String host = uri.getHost();
String resourceName = (TextUtils.isEmpty(host) ? "" : (host + ":")) + path;
resourceId =
resources.getIdentifier(
resourceName, /* defType= */ "raw", /* defPackage= */ packageName);
if (resourceId == 0) {
throw new RawResourceDataSourceException("Resource not found.");
}
} else {
throw new RawResourceDataSourceException(
"URI must either use scheme "
+ RAW_RESOURCE_SCHEME
+ " or "
+ ContentResolver.SCHEME_ANDROID_RESOURCE);
}
transferInitializing(dataSpec);
AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId);
@ -104,9 +138,10 @@ public final class RawResourceDataSource extends BaseDataSource {
if (assetFileDescriptor == null) {
throw new RawResourceDataSourceException("Resource is compressed: " + uri);
}
FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
this.inputStream = inputStream;
try {
inputStream.skip(assetFileDescriptor.getStartOffset());
long skipped = inputStream.skip(dataSpec.position);
if (skipped < dataSpec.position) {
@ -114,16 +149,19 @@ public final class RawResourceDataSource extends BaseDataSource {
// skip beyond the end of the data.
throw new EOFException();
}
} catch (IOException e) {
throw new RawResourceDataSourceException(e);
}
if (dataSpec.length != C.LENGTH_UNSET) {
bytesRemaining = dataSpec.length;
} else {
long assetFileDescriptorLength = assetFileDescriptor.getLength();
// If the length is UNKNOWN_LENGTH then the asset extends to the end of the file.
bytesRemaining = assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH
? C.LENGTH_UNSET : (assetFileDescriptorLength - dataSpec.position);
}
} catch (IOException e) {
throw new RawResourceDataSourceException(e);
bytesRemaining =
assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH
? C.LENGTH_UNSET
: (assetFileDescriptorLength - dataSpec.position);
}
opened = true;

View File

@ -27,6 +27,7 @@ import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
/**
* Static utility to retrieve the device time offset using SNTP.
@ -37,6 +38,9 @@ import java.util.Arrays;
*/
public final class SntpClient {
/** The default NTP host address used to retrieve {@link #getElapsedRealtimeOffsetMs()}. */
public static final String DEFAULT_NTP_HOST = "time.android.com";
/** Callback for calls to {@link #initialize(Loader, InitializationCallback)}. */
public interface InitializationCallback {
@ -51,7 +55,6 @@ public final class SntpClient {
void onInitializationFailed(IOException error);
}
private static final String NTP_HOST = "pool.ntp.org";
private static final int TIMEOUT_MS = 10_000;
private static final int ORIGINATE_TIME_OFFSET = 24;
@ -80,8 +83,37 @@ public final class SntpClient {
@GuardedBy("valueLock")
private static long elapsedRealtimeOffsetMs;
@GuardedBy("valueLock")
private static String ntpHost = DEFAULT_NTP_HOST;
private SntpClient() {}
/** Returns the NTP host address used to retrieve {@link #getElapsedRealtimeOffsetMs()}. */
public static String getNtpHost() {
synchronized (valueLock) {
return ntpHost;
}
}
/**
* Sets the NTP host address used to retrieve {@link #getElapsedRealtimeOffsetMs()}.
*
* <p>The default is {@link #DEFAULT_NTP_HOST}.
*
* <p>If the new host address is different from the previous one, the NTP client will be {@link
* #isInitialized()} uninitialized} again.
*
* @param ntpHost The NTP host address.
*/
public static void setNtpHost(String ntpHost) {
synchronized (valueLock) {
if (!SntpClient.ntpHost.equals(ntpHost)) {
SntpClient.ntpHost = ntpHost;
isInitialized = false;
}
}
}
/**
* Returns whether the device time offset has already been loaded.
*
@ -129,7 +161,7 @@ public final class SntpClient {
}
private static long loadNtpTimeOffsetMs() throws IOException {
InetAddress address = InetAddress.getByName(NTP_HOST);
InetAddress address = InetAddress.getByName(getNtpHost());
try (DatagramSocket socket = new DatagramSocket()) {
socket.setSoTimeout(TIMEOUT_MS);
byte[] buffer = new byte[NTP_PACKET_SIZE];
@ -282,11 +314,16 @@ public final class SntpClient {
@Override
public void onLoadCompleted(Loadable loadable, long elapsedRealtimeMs, long loadDurationMs) {
Assertions.checkState(SntpClient.isInitialized());
if (callback != null) {
if (!SntpClient.isInitialized()) {
// This may happen in the unlikely edge case of someone calling setNtpHost between the end
// of the load method and this callback.
callback.onInitializationFailed(new IOException(new ConcurrentModificationException()));
} else {
callback.onInitialized();
}
}
}
@Override
public void onLoadCanceled(

View File

@ -1551,19 +1551,55 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
synchronized (MediaCodecVideoRenderer.class) {
if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) {
if ("dangal".equals(Util.DEVICE)) {
// Workaround for MiTV devices:
deviceNeedsSetOutputSurfaceWorkaround = evaluateDeviceNeedsSetOutputSurfaceWorkaround();
evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true;
}
}
return deviceNeedsSetOutputSurfaceWorkaround;
}
protected Surface getSurface() {
return surface;
}
protected static final class CodecMaxValues {
public final int width;
public final int height;
public final int inputSize;
public CodecMaxValues(int width, int height, int inputSize) {
this.width = width;
this.height = height;
this.inputSize = inputSize;
}
}
private static boolean evaluateDeviceNeedsSetOutputSurfaceWorkaround() {
if (Util.SDK_INT <= 28) {
// Workaround for MiTV devices which have been observed broken up to API 28.
// https://github.com/google/ExoPlayer/issues/5169,
// https://github.com/google/ExoPlayer/issues/6899.
deviceNeedsSetOutputSurfaceWorkaround = true;
} else if (Util.SDK_INT <= 27 && "HWEML".equals(Util.DEVICE)) {
// https://github.com/google/ExoPlayer/issues/8014.
switch (Util.DEVICE) {
case "dangal":
case "dangalUHD":
case "dangalFHD":
case "magnolia":
case "machuca":
return true;
default:
break; // Do nothing.
}
}
if (Util.SDK_INT <= 27 && "HWEML".equals(Util.DEVICE)) {
// Workaround for Huawei P20:
// https://github.com/google/ExoPlayer/issues/4468#issuecomment-459291645.
deviceNeedsSetOutputSurfaceWorkaround = true;
} else if (Util.SDK_INT >= 27) {
// In general, devices running API level 27 or later should be unaffected. Do nothing.
} else {
// Enable the workaround on a per-device basis. Works around:
return true;
}
if (Util.SDK_INT <= 26) {
// In general, devices running API level 27 or later should be unaffected unless observed
// otherwise. Enable the workaround on a per-device basis. Works around:
// https://github.com/google/ExoPlayer/issues/3236,
// https://github.com/google/ExoPlayer/issues/3355,
// https://github.com/google/ExoPlayer/issues/3439,
@ -1579,10 +1615,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
// https://github.com/google/ExoPlayer/issues/4468,
// https://github.com/google/ExoPlayer/issues/5312,
// https://github.com/google/ExoPlayer/issues/6503.
// https://github.com/google/ExoPlayer/issues/8014,
// https://github.com/google/ExoPlayer/pull/8030.
switch (Util.DEVICE) {
case "1601":
case "1713":
case "1714":
case "601LV":
case "602LV":
case "A10-70F":
case "A10-70L":
case "A1601":
@ -1594,6 +1634,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "AquaPowerM":
case "ASUS_X00AD_2":
case "Aura_Note_2":
case "b5":
case "BLACK-1X":
case "BRAVIA_ATV2":
case "BRAVIA_ATV3_4K":
@ -1601,18 +1642,24 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "ComioS1":
case "CP8676_I02":
case "CPH1609":
case "CPH1715":
case "CPY83_I00":
case "cv1":
case "cv3":
case "deb":
case "DM-01K":
case "E5643":
case "ELUGA_A3_Pro":
case "ELUGA_Note":
case "ELUGA_Prim":
case "ELUGA_Ray_X":
case "EverStar_S":
case "F01H":
case "F01J":
case "F02H":
case "F03H":
case "F04H":
case "F04J":
case "F3111":
case "F3113":
case "F3116":
@ -1650,6 +1697,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "l5460":
case "le_x6":
case "LS-5017":
case "M04":
case "M5c":
case "manning":
case "marino_f":
@ -1665,6 +1713,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "p212":
case "P681":
case "P85":
case "pacificrim":
case "panell_d":
case "panell_dl":
case "panell_ds":
@ -1685,6 +1734,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "Q5":
case "QM16XE_U":
case "QX1":
case "RAIJIN":
case "santoni":
case "Slate_Pro":
case "SVP-DTV15":
@ -1708,45 +1758,20 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "XT1663":
case "Z12_PRO":
case "Z80":
deviceNeedsSetOutputSurfaceWorkaround = true;
break;
return true;
default:
// Do nothing.
break;
break; // Do nothing.
}
switch (Util.MODEL) {
case "AFTA":
case "AFTN":
case "JSN-L21":
deviceNeedsSetOutputSurfaceWorkaround = true;
break;
return true;
default:
// Do nothing.
break;
break; // Do nothing.
}
}
evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true;
}
}
return deviceNeedsSetOutputSurfaceWorkaround;
}
protected Surface getSurface() {
return surface;
}
protected static final class CodecMaxValues {
public final int width;
public final int height;
public final int inputSize;
public CodecMaxValues(int width, int height, int inputSize) {
this.width = width;
this.height = height;
this.inputSize = inputSize;
}
return false;
}
@RequiresApi(23)

View File

@ -58,6 +58,7 @@ import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.source.ClippingMediaSource;
import com.google.android.exoplayer2.source.CompositeMediaSource;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MaskingMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
@ -98,12 +99,13 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocation;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
@ -5553,7 +5555,8 @@ public final class ExoPlayerTest {
AdsMediaSource adsMediaSource =
new AdsMediaSource(
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)),
new DefaultDataSourceFactory(context),
/* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
new DefaultMediaSourceFactory(context),
new FakeAdsLoader(),
new FakeAdViewProvider());
Exception[] exception = {null};
@ -5590,7 +5593,8 @@ public final class ExoPlayerTest {
AdsMediaSource adsMediaSource =
new AdsMediaSource(
mediaSource,
new DefaultDataSourceFactory(context),
/* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
new DefaultMediaSourceFactory(context),
new FakeAdsLoader(),
new FakeAdViewProvider());
final Exception[] exception = {null};
@ -5629,7 +5633,8 @@ public final class ExoPlayerTest {
AdsMediaSource adsMediaSource =
new AdsMediaSource(
mediaSource,
new DefaultDataSourceFactory(context),
/* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
new DefaultMediaSourceFactory(context),
new FakeAdsLoader(),
new FakeAdViewProvider());
final Exception[] exception = {null};
@ -5699,6 +5704,42 @@ public final class ExoPlayerTest {
assertArrayEquals(new int[] {0, 0, 0}, currentWindowIndices);
}
@Test
public void setMediaItems_resetPosition_resetsPosition() throws Exception {
final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] currentPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000);
currentWindowIndices[0] = player.getCurrentWindowIndex();
currentPositions[0] = player.getCurrentPosition();
List<MediaItem> listOfTwo =
Lists.newArrayList(
MediaItem.fromUri(Uri.EMPTY), MediaItem.fromUri(Uri.EMPTY));
player.setMediaItems(listOfTwo, /* resetPosition= */ true);
currentWindowIndices[1] = player.getCurrentWindowIndex();
currentPositions[1] = player.getCurrentPosition();
}
})
.prepare()
.waitForTimelineChanged()
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {1, 0}, currentWindowIndices);
assertArrayEquals(new long[] {1000, 0}, currentPositions);
}
@Test
public void setMediaSources_empty_whenEmpty_validInitialSeek_correctMaskingWindowIndex()
throws Exception {
@ -7334,6 +7375,8 @@ public final class ExoPlayerTest {
new DefaultLoadControl.Builder()
.setTargetBufferBytes(10 * C.DEFAULT_BUFFER_SEGMENT_SIZE)
.build();
// Return no end of stream signal to prevent playback from ending.
FakeMediaPeriod.TrackDataFactory trackDataWithoutEos = (format, periodId) -> ImmutableList.of();
MediaSource continuouslyAllocatingMediaSource =
new FakeMediaSource(
new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT) {
@ -7348,8 +7391,11 @@ public final class ExoPlayerTest {
@Nullable TransferListener transferListener) {
return new FakeMediaPeriod(
trackGroupArray,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
mediaSourceEventDispatcher) {
trackDataWithoutEos,
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
/* deferOnPrepared= */ false) {
private final List<Allocation> allocations = new ArrayList<>();
@ -7382,14 +7428,8 @@ public final class ExoPlayerTest {
};
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
// Prevent player from ever assuming it finished playing.
.setRepeatMode(Player.REPEAT_MODE_ALL)
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.setMediaSources(continuouslyAllocatingMediaSource)
.setLoadControl(loadControl)
.build();
@ -8512,6 +8552,9 @@ public final class ExoPlayerTest {
@Override
public void setSupportedContentTypes(int... contentTypes) {}
@Override
public void setAdTagDataSpec(DataSpec adTagDataSpec) {}
@Override
public void start(AdsLoader.EventListener eventListener, AdViewProvider adViewProvider) {}

View File

@ -23,8 +23,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.e2etest.util.PlaybackOutput;
import com.google.android.exoplayer2.e2etest.util.ShadowMediaCodecConfig;
import com.google.android.exoplayer2.robolectric.PlaybackOutput;
import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
import com.google.android.exoplayer2.testutil.TestExoPlayer;

View File

@ -22,8 +22,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.e2etest.util.PlaybackOutput;
import com.google.android.exoplayer2.e2etest.util.ShadowMediaCodecConfig;
import com.google.android.exoplayer2.robolectric.PlaybackOutput;
import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
import com.google.android.exoplayer2.testutil.TestExoPlayer;

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.trackselection;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.RendererCapabilities;
@ -52,7 +53,7 @@ public class TrackSelectorTest {
}
@Override
public void onSelectionActivated(Object info) {}
public void onSelectionActivated(@Nullable Object info) {}
};
}

View File

@ -22,7 +22,6 @@ import static org.junit.Assert.fail;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import org.junit.Before;
@ -146,6 +145,14 @@ public final class DataSchemeDataSourceTest {
}
}
@Test
public void readSourceToEnd_readsEncodedString() throws Exception {
String data = "Some Data!<>:\"/\\|?*%";
schemeDataDataSource.open(new DataSpec(Util.getDataUriForString("text/plain", data)));
assertThat(Util.fromUtf8Bytes(Util.readToEnd(schemeDataDataSource))).isEqualTo(data);
}
private static DataSpec buildDataSpec(String uriString) {
return buildDataSpec(uriString, /* position= */ 0, /* length= */ C.LENGTH_UNSET);
}
@ -167,7 +174,7 @@ public final class DataSchemeDataSourceTest {
try {
long length = dataSource.open(dataSpec);
assertThat(length).isEqualTo(expectedData.length);
byte[] readData = TestUtil.readToEnd(dataSource);
byte[] readData = Util.readToEnd(dataSource);
assertThat(readData).isEqualTo(expectedData);
} finally {
dataSource.close();

View File

@ -47,41 +47,46 @@ public final class DefaultLoadErrorHandlingPolicyTest {
private static final MediaLoadData PLACEHOLDER_MEDIA_LOAD_DATA =
new MediaLoadData(/* dataType= */ C.DATA_TYPE_UNKNOWN);
@Test
public void getExclusionDurationMsFor_responseCode403() {
InvalidResponseCodeException exception = buildInvalidResponseCodeException(403, "Forbidden");
assertThat(getDefaultPolicyExclusionDurationMsFor(exception))
.isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS);
}
@Test
public void getExclusionDurationMsFor_responseCode404() {
InvalidResponseCodeException exception =
new InvalidResponseCodeException(
404,
"Not Found",
Collections.emptyMap(),
new DataSpec(Uri.EMPTY),
/* responseBody= */ Util.EMPTY_BYTE_ARRAY);
InvalidResponseCodeException exception = buildInvalidResponseCodeException(404, "Not found");
assertThat(getDefaultPolicyExclusionDurationMsFor(exception))
.isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS);
}
@Test
public void getExclusionDurationMsFor_responseCode410() {
InvalidResponseCodeException exception = buildInvalidResponseCodeException(410, "Gone");
assertThat(getDefaultPolicyExclusionDurationMsFor(exception))
.isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS);
}
@Test
public void getExclusionDurationMsFor_responseCode500() {
InvalidResponseCodeException exception =
new InvalidResponseCodeException(
410,
"Gone",
Collections.emptyMap(),
new DataSpec(Uri.EMPTY),
/* responseBody= */ Util.EMPTY_BYTE_ARRAY);
buildInvalidResponseCodeException(500, "Internal server error");
assertThat(getDefaultPolicyExclusionDurationMsFor(exception))
.isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS);
}
@Test
public void getExclusionDurationMsFor_responseCode503() {
InvalidResponseCodeException exception =
buildInvalidResponseCodeException(503, "Service unavailable");
assertThat(getDefaultPolicyExclusionDurationMsFor(exception))
.isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS);
}
@Test
public void getExclusionDurationMsFor_dontExcludeUnexpectedHttpCodes() {
InvalidResponseCodeException exception =
new InvalidResponseCodeException(
500,
"Internal Server Error",
Collections.emptyMap(),
new DataSpec(Uri.EMPTY),
/* responseBody= */ Util.EMPTY_BYTE_ARRAY);
InvalidResponseCodeException exception = buildInvalidResponseCodeException(418, "I'm a teapot");
assertThat(getDefaultPolicyExclusionDurationMsFor(exception)).isEqualTo(C.TIME_UNSET);
}
@ -120,4 +125,14 @@ public final class DefaultLoadErrorHandlingPolicyTest {
PLACEHOLDER_LOAD_EVENT_INFO, PLACEHOLDER_MEDIA_LOAD_DATA, exception, errorCount);
return new DefaultLoadErrorHandlingPolicy().getRetryDelayMsFor(loadErrorInfo);
}
private static InvalidResponseCodeException buildInvalidResponseCodeException(
int statusCode, String message) {
return new InvalidResponseCodeException(
statusCode,
message,
Collections.emptyMap(),
new DataSpec(Uri.EMPTY),
/* responseBody= */ Util.EMPTY_BYTE_ARRAY);
}
}

View File

@ -302,7 +302,7 @@ public final class CacheDataSourceTest {
CacheDataSource cacheDataSource = new CacheDataSource(cache, upstream, 0);
cacheDataSource.open(unboundedDataSpec);
TestUtil.readToEnd(cacheDataSource);
Util.readToEnd(cacheDataSource);
cacheDataSource.close();
assertThat(upstream.getAndClearOpenedDataSpecs()).hasLength(1);
@ -319,7 +319,7 @@ public final class CacheDataSourceTest {
cache, upstream, CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);
cacheDataSource.open(unboundedDataSpec);
TestUtil.readToEnd(cacheDataSource);
Util.readToEnd(cacheDataSource);
cacheDataSource.close();
assertThat(cache.getKeys()).isEmpty();
@ -369,7 +369,7 @@ public final class CacheDataSourceTest {
cacheWriter.cache();
// Read the rest of the data.
TestUtil.readToEnd(cacheDataSource);
Util.readToEnd(cacheDataSource);
cacheDataSource.close();
}
@ -419,7 +419,7 @@ public final class CacheDataSourceTest {
cacheWriter.cache();
// Read the rest of the data.
TestUtil.readToEnd(cacheDataSource);
Util.readToEnd(cacheDataSource);
cacheDataSource.close();
}
@ -449,14 +449,14 @@ public final class CacheDataSourceTest {
// Open source and read some data from upstream as the data hasn't cached yet.
cacheDataSource.open(unboundedDataSpec);
TestUtil.readExactly(cacheDataSource, 100);
Util.readExactly(cacheDataSource, 100);
// Delete cached data.
cache.removeResource(cacheDataSource.getCacheKeyFactory().buildCacheKey(unboundedDataSpec));
assertCacheEmpty(cache);
// Read the rest of the data.
TestUtil.readToEnd(cacheDataSource);
Util.readToEnd(cacheDataSource);
cacheDataSource.close();
}
@ -487,7 +487,7 @@ public final class CacheDataSourceTest {
cacheDataSource.open(unboundedDataSpec);
// Read the first half from upstream as it hasn't cached yet.
TestUtil.readExactly(cacheDataSource, halfDataLength);
Util.readExactly(cacheDataSource, halfDataLength);
// Delete the cached latter half.
NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(defaultCacheKey);
@ -498,7 +498,7 @@ public final class CacheDataSourceTest {
}
// Read the rest of the data.
TestUtil.readToEnd(cacheDataSource);
Util.readToEnd(cacheDataSource);
cacheDataSource.close();
}

View File

@ -46,7 +46,6 @@ import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.testutil.FakeSampleStream;
import com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem;
import com.google.android.exoplayer2.util.MimeTypes;
@ -107,8 +106,7 @@ public class MediaCodecVideoRendererTest {
/* maxDroppedFramesToNotify= */ 1) {
@Override
@Capabilities
protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format)
throws DecoderQueryException {
protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) {
return RendererCapabilities.create(FORMAT_HANDLED);
}

View File

@ -25,17 +25,11 @@ android {
dependencies {
implementation project(modulePrefix + 'library-core')
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
testImplementation project(modulePrefix + 'robolectricutils')
testImplementation project(modulePrefix + 'testutils')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
}

View File

@ -70,6 +70,16 @@ public class DashManifestParser extends DefaultHandler
private static final Pattern CEA_708_ACCESSIBILITY_PATTERN =
Pattern.compile("([1-9]|[1-5][0-9]|6[0-3])=.*");
/**
* Maps the value attribute of an AudioElementConfiguration with schemeIdUri
* "urn:mpeg:mpegB:cicp:ChannelConfiguration", as defined by ISO 23001-8 clause 8.1, to a channel
* count.
*/
private static final int[] MPEG_CHANNEL_CONFIGURATION_MAPPING =
new int[] {
Format.NO_VALUE, 1, 2, 3, 4, 5, 6, 8, 2, 3, 4, 7, 8, 24, 8, 12, 10, 12, 14, 12, 14
};
private final XmlPullParserFactory xmlParserFactory;
public DashManifestParser() {
@ -1156,13 +1166,22 @@ public class DashManifestParser extends DefaultHandler
protected int parseAudioChannelConfiguration(XmlPullParser xpp)
throws XmlPullParserException, IOException {
String schemeIdUri = parseString(xpp, "schemeIdUri", null);
int audioChannels =
"urn:mpeg:dash:23003:3:audio_channel_configuration:2011".equals(schemeIdUri)
? parseInt(xpp, "value", Format.NO_VALUE)
: ("tag:dolby.com,2014:dash:audio_channel_configuration:2011".equals(schemeIdUri)
|| "urn:dolby:dash:audio_channel_configuration:2011".equals(schemeIdUri)
? parseDolbyChannelConfiguration(xpp)
: Format.NO_VALUE);
int audioChannels;
switch (schemeIdUri) {
case "urn:mpeg:dash:23003:3:audio_channel_configuration:2011":
audioChannels = parseInt(xpp, "value", Format.NO_VALUE);
break;
case "urn:mpeg:mpegB:cicp:ChannelConfiguration":
audioChannels = parseMpegChannelConfiguration(xpp);
break;
case "tag:dolby.com,2014:dash:audio_channel_configuration:2011":
case "urn:dolby:dash:audio_channel_configuration:2011":
audioChannels = parseDolbyChannelConfiguration(xpp);
break;
default:
audioChannels = Format.NO_VALUE;
break;
}
do {
xpp.next();
} while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration"));
@ -1344,7 +1363,8 @@ public class DashManifestParser extends DefaultHandler
// All other text types are raw formats.
return containerMimeType;
} else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) {
return MimeTypes.getMediaMimeType(codecs);
@Nullable String mimeType = MimeTypes.getMediaMimeType(codecs);
return MimeTypes.TEXT_VTT.equals(mimeType) ? MimeTypes.APPLICATION_MP4VTT : mimeType;
}
return null;
}
@ -1528,6 +1548,21 @@ public class DashManifestParser extends DefaultHandler
return value == null ? defaultValue : value;
}
/**
* Parses the number of channels from the value attribute of an AudioElementConfiguration with
* schemeIdUri "urn:mpeg:mpegB:cicp:ChannelConfiguration", as defined by ISO 23001-8 clause 8.1.
*
* @param xpp The parser from which to read.
* @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could
* not be parsed.
*/
protected static int parseMpegChannelConfiguration(XmlPullParser xpp) {
int index = parseInt(xpp, "value", C.INDEX_UNSET);
return 0 <= index && index < MPEG_CHANNEL_CONFIGURATION_MAPPING.length
? MPEG_CHANNEL_CONFIGURATION_MAPPING[index]
: Format.NO_VALUE;
}
/**
* Parses the number of channels from the value attribute of an AudioElementConfiguration with
* schemeIdUri "tag:dolby.com,2014:dash:audio_channel_configuration:2011", as defined by table E.5

View File

@ -0,0 +1,73 @@
/*
* 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.e2etest;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.robolectric.PlaybackOutput;
import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
import com.google.android.exoplayer2.testutil.TestExoPlayer;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
/** End-to-end tests using DASH samples. */
// TODO(b/143232359): Remove once https://issuetracker.google.com/143232359 is resolved.
@Config(sdk = 29)
@RunWith(AndroidJUnit4.class)
public final class DashPlaybackTest {
@Rule
public ShadowMediaCodecConfig mediaCodecConfig =
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
// https://github.com/google/ExoPlayer/issues/7985
@Test
public void webvttInMp4() throws Exception {
SimpleExoPlayer player =
new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext())
.setClock(new AutoAdvancingFakeClock())
.build();
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
PlaybackOutput playbackOutput = PlaybackOutput.register(player, mediaCodecConfig);
// Ensure the subtitle track is selected.
DefaultTrackSelector trackSelector =
checkNotNull((DefaultTrackSelector) player.getTrackSelector());
trackSelector.setParameters(trackSelector.buildUponParameters().setPreferredTextLanguage("en"));
player.setMediaItem(MediaItem.fromUri("asset:///media/dash/webvtt-in-mp4/sample.mpd"));
player.prepare();
player.play();
TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
DumpFileAsserts.assertOutput(
ApplicationProvider.getApplicationContext(),
playbackOutput,
"playbackdumps/dash/webvtt-in-mp4.dump");
}
}

View File

@ -26,13 +26,6 @@ android {
dependencies {
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'library-common')
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion

View File

@ -131,9 +131,11 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
@Mp3Extractor.Flags private int mp3Flags;
@TsExtractor.Mode private int tsMode;
@DefaultTsPayloadReaderFactory.Flags private int tsFlags;
private int tsTimestampSearchBytes;
public DefaultExtractorsFactory() {
tsMode = TsExtractor.MODE_SINGLE_PMT;
tsTimestampSearchBytes = TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES;
}
/**
@ -246,7 +248,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
/**
* Sets the mode for {@link TsExtractor} instances created by the factory.
*
* @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory)
* @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory, int)
* @param mode The mode to use.
* @return The factory, for convenience.
*/
@ -269,6 +271,20 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
return this;
}
/**
* Sets the number of bytes searched to find a timestamp for {@link TsExtractor} instances created
* by the factory.
*
* @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory, int)
* @param timestampSearchBytes The number of search bytes to use.
* @return The factory, for convenience.
*/
public synchronized DefaultExtractorsFactory setTsExtractorTimestampSearchBytes(
int timestampSearchBytes) {
tsTimestampSearchBytes = timestampSearchBytes;
return this;
}
@Override
public synchronized Extractor[] createExtractors() {
return createExtractors(Uri.EMPTY, new HashMap<>());
@ -361,7 +377,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
extractors.add(new PsExtractor());
break;
case FileTypes.TS:
extractors.add(new TsExtractor(tsMode, tsFlags));
extractors.add(new TsExtractor(tsMode, tsFlags, tsTimestampSearchBytes));
break;
case FileTypes.WAV:
extractors.add(new WavExtractor());

View File

@ -0,0 +1,85 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
/**
* A {@link SeekMap} implementation based on a mapping between times and positions in the input
* stream.
*/
public final class IndexSeekMap implements SeekMap {
private final long[] positions;
private final long[] timesUs;
private final long durationUs;
private final boolean isSeekable;
/**
* Creates an instance.
*
* @param positions The positions in the stream corresponding to {@code timesUs}, in bytes.
* @param timesUs The times corresponding to {@code positions}, in microseconds.
* @param durationUs The duration of the input stream, or {@link C#TIME_UNSET} if it is unknown.
*/
public IndexSeekMap(long[] positions, long[] timesUs, long durationUs) {
checkArgument(positions.length == timesUs.length);
int length = timesUs.length;
isSeekable = length > 0;
if (isSeekable && timesUs[0] > 0) {
// Add (position = 0, timeUs = 0) as first entry.
this.positions = new long[length + 1];
this.timesUs = new long[length + 1];
System.arraycopy(positions, 0, this.positions, 1, length);
System.arraycopy(timesUs, 0, this.timesUs, 1, length);
} else {
this.positions = positions;
this.timesUs = timesUs;
}
this.durationUs = durationUs;
}
@Override
public boolean isSeekable() {
return isSeekable;
}
@Override
public long getDurationUs() {
return durationUs;
}
@Override
public SeekMap.SeekPoints getSeekPoints(long timeUs) {
if (!isSeekable) {
return new SeekMap.SeekPoints(SeekPoint.START);
}
int targetIndex =
Util.binarySearchFloor(timesUs, timeUs, /* inclusive= */ true, /* stayInBounds= */ true);
SeekPoint leftSeekPoint = new SeekPoint(timesUs[targetIndex], positions[targetIndex]);
if (leftSeekPoint.timeUs == timeUs || targetIndex == timesUs.length - 1) {
return new SeekMap.SeekPoints(leftSeekPoint);
} else {
SeekPoint rightSeekPoint =
new SeekPoint(timesUs[targetIndex + 1], positions[targetIndex + 1]);
return new SeekMap.SeekPoints(leftSeekPoint, rightSeekPoint);
}
}
}

View File

@ -23,6 +23,7 @@ import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.IndexSeekMap;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.util.Assertions;
@ -135,8 +136,12 @@ public final class FlvExtractor implements Extractor {
@Override
public void seek(long position, long timeUs) {
if (position == 0) {
state = STATE_READING_FLV_HEADER;
outputFirstSample = false;
} else {
state = STATE_READING_TAG_HEADER;
}
bytesToNextTagHeader = 0;
}
@ -267,7 +272,11 @@ public final class FlvExtractor implements Extractor {
wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs);
long durationUs = metadataReader.getDurationUs();
if (durationUs != C.TIME_UNSET) {
extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
extractorOutput.seekMap(
new IndexSeekMap(
metadataReader.getKeyFrameTagPositions(),
metadataReader.getKeyFrameTimesUs(),
durationUs));
outputSeekMap = true;
}
} else {

View File

@ -22,6 +22,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -31,6 +32,9 @@ import java.util.Map;
private static final String NAME_METADATA = "onMetaData";
private static final String KEY_DURATION = "duration";
private static final String KEY_KEY_FRAMES = "keyframes";
private static final String KEY_FILE_POSITIONS = "filepositions";
private static final String KEY_TIMES = "times";
// AMF object types
private static final int AMF_TYPE_NUMBER = 0;
@ -43,16 +47,28 @@ import java.util.Map;
private static final int AMF_TYPE_DATE = 11;
private long durationUs;
private long[] keyFrameTimesUs;
private long[] keyFrameTagPositions;
public ScriptTagPayloadReader() {
super(new DummyTrackOutput());
durationUs = C.TIME_UNSET;
keyFrameTimesUs = new long[0];
keyFrameTagPositions = new long[0];
}
public long getDurationUs() {
return durationUs;
}
public long[] getKeyFrameTimesUs() {
return keyFrameTimesUs;
}
public long[] getKeyFrameTagPositions() {
return keyFrameTagPositions;
}
@Override
public void seek() {
// Do nothing.
@ -80,14 +96,41 @@ import java.util.Map;
// We're not interested in this metadata.
return false;
}
// Set the duration to the value contained in the metadata, if present.
Map<String, Object> metadata = readAmfEcmaArray(data);
if (metadata.containsKey(KEY_DURATION)) {
double durationSeconds = (double) metadata.get(KEY_DURATION);
// Set the duration to the value contained in the metadata, if present.
@Nullable Object durationSecondsObj = metadata.get(KEY_DURATION);
if (durationSecondsObj instanceof Double) {
double durationSeconds = (double) durationSecondsObj;
if (durationSeconds > 0.0) {
durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND);
}
}
// Set the key frame times and positions to the value contained in the metadata, if present.
@Nullable Object keyFramesObj = metadata.get(KEY_KEY_FRAMES);
if (keyFramesObj instanceof Map) {
Map<?, ?> keyFrames = (Map<?, ?>) keyFramesObj;
@Nullable Object positionsObj = keyFrames.get(KEY_FILE_POSITIONS);
@Nullable Object timesSecondsObj = keyFrames.get(KEY_TIMES);
if (positionsObj instanceof List && timesSecondsObj instanceof List) {
List<?> positions = (List<?>) positionsObj;
List<?> timesSeconds = (List<?>) timesSecondsObj;
int keyFrameCount = timesSeconds.size();
keyFrameTimesUs = new long[keyFrameCount];
keyFrameTagPositions = new long[keyFrameCount];
for (int i = 0; i < keyFrameCount; i++) {
Object positionObj = positions.get(i);
Object timeSecondsObj = timesSeconds.get(i);
if (timeSecondsObj instanceof Double && positionObj instanceof Double) {
keyFrameTimesUs[i] = (long) (((Double) timeSecondsObj) * C.MICROS_PER_SECOND);
keyFrameTagPositions[i] = ((Double) positionObj).longValue();
} else {
keyFrameTimesUs = new long[0];
keyFrameTagPositions = new long[0];
break;
}
}
}
}
return false;
}

View File

@ -70,7 +70,7 @@ import com.google.android.exoplayer2.util.Util;
int targetIndex =
Util.binarySearchFloor(timesUs, timeUs, /* inclusive= */ true, /* stayInBounds= */ true);
SeekPoint seekPoint = new SeekPoint(timesUs.get(targetIndex), positions.get(targetIndex));
if (seekPoint.timeUs >= timeUs || targetIndex == timesUs.size() - 1) {
if (seekPoint.timeUs == timeUs || targetIndex == timesUs.size() - 1) {
return new SeekPoints(seekPoint);
} else {
SeekPoint nextSeekPoint =

View File

@ -29,9 +29,11 @@ import com.google.android.exoplayer2.util.Util;
*
* @param firstFramePosition The position of the start of the first frame in the stream.
* @param mlltFrame The MLLT frame with seeking metadata.
* @param durationUs The stream duration in microseconds, or {@link C#TIME_UNSET} if it is
* unknown.
* @return An {@link MlltSeeker} for seeking in the stream.
*/
public static MlltSeeker create(long firstFramePosition, MlltFrame mlltFrame) {
public static MlltSeeker create(long firstFramePosition, MlltFrame mlltFrame, long durationUs) {
int referenceCount = mlltFrame.bytesDeviations.length;
long[] referencePositions = new long[1 + referenceCount];
long[] referenceTimesMs = new long[1 + referenceCount];
@ -45,19 +47,22 @@ import com.google.android.exoplayer2.util.Util;
referencePositions[i] = position;
referenceTimesMs[i] = timeMs;
}
return new MlltSeeker(referencePositions, referenceTimesMs);
return new MlltSeeker(referencePositions, referenceTimesMs, durationUs);
}
private final long[] referencePositions;
private final long[] referenceTimesMs;
private final long durationUs;
private MlltSeeker(long[] referencePositions, long[] referenceTimesMs) {
private MlltSeeker(long[] referencePositions, long[] referenceTimesMs, long durationUs) {
this.referencePositions = referencePositions;
this.referenceTimesMs = referenceTimesMs;
// Use the last reference point as the duration, as extrapolating variable bitrate at the end of
// the stream may give a large error.
durationUs = C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]);
// Use the last reference point as the duration if it is unknown, as extrapolating variable
// bitrate at the end of the stream may give a large error.
this.durationUs =
durationUs != C.TIME_UNSET
? durationUs
: C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]);
}
@Override

View File

@ -35,6 +35,7 @@ import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
import com.google.android.exoplayer2.metadata.id3.MlltFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
@ -432,7 +433,7 @@ public final class Mp3Extractor implements Extractor {
@Nullable Seeker resultSeeker = null;
if ((flags & FLAG_ENABLE_INDEX_SEEKING) != 0) {
long durationUs = C.TIME_UNSET;
long durationUs;
long dataEndPosition = C.POSITION_UNSET;
if (metadataSeeker != null) {
durationUs = metadataSeeker.getDurationUs();
@ -440,6 +441,8 @@ public final class Mp3Extractor implements Extractor {
} else if (seekFrameSeeker != null) {
durationUs = seekFrameSeeker.getDurationUs();
dataEndPosition = seekFrameSeeker.getDataEndPosition();
} else {
durationUs = getId3TlenUs(metadata);
}
resultSeeker =
new IndexSeeker(
@ -554,10 +557,24 @@ public final class Mp3Extractor implements Extractor {
for (int i = 0; i < length; i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof MlltFrame) {
return MlltSeeker.create(firstFramePosition, (MlltFrame) entry);
return MlltSeeker.create(firstFramePosition, (MlltFrame) entry, getId3TlenUs(metadata));
}
}
}
return null;
}
private static long getId3TlenUs(@Nullable Metadata metadata) {
if (metadata != null) {
int length = metadata.length();
for (int i = 0; i < length; i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof TextInformationFrame
&& ((TextInformationFrame) entry).id.equals("TLEN")) {
return C.msToUs(Long.parseLong(((TextInformationFrame) entry).value));
}
}
}
return C.TIME_UNSET;
}
}

View File

@ -115,6 +115,9 @@ import java.util.List;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_mp4a = 0x6d703461;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE__mp2 = 0x2e6d7032;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE__mp3 = 0x2e6d7033;
@ -274,9 +277,6 @@ import java.util.List;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_TTML = 0x54544d4c;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_vmhd = 0x766d6864;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_mp4v = 0x6d703476;
@ -358,6 +358,9 @@ import java.util.List;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_camm = 0x63616d6d;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_mett = 0x6d657474;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_alac = 0x616c6163;

View File

@ -384,9 +384,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
// Fixed sample size raw audio may need to be rechunked.
boolean isFixedSampleSizeRawAudio =
sampleSizeBox.isFixedSampleSize()
&& MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType)
int fixedSampleSize = sampleSizeBox.getFixedSampleSize();
@Nullable String sampleMimeType = track.format.sampleMimeType;
boolean rechunkFixedSizeSamples =
fixedSampleSize != C.LENGTH_UNSET
&& (MimeTypes.AUDIO_RAW.equals(sampleMimeType)
|| MimeTypes.AUDIO_MLAW.equals(sampleMimeType)
|| MimeTypes.AUDIO_ALAW.equals(sampleMimeType))
&& remainingTimestampDeltaChanges == 0
&& remainingTimestampOffsetChanges == 0
&& remainingSynchronizationSamples == 0;
@ -399,15 +403,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
long timestampTimeUnits = 0;
long duration;
if (isFixedSampleSizeRawAudio) {
if (rechunkFixedSizeSamples) {
long[] chunkOffsetsBytes = new long[chunkIterator.length];
int[] chunkSampleCounts = new int[chunkIterator.length];
while (chunkIterator.moveNext()) {
chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset;
chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples;
}
int fixedSampleSize =
Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount);
FixedSampleSizeRechunker.Results rechunkedResults =
FixedSampleSizeRechunker.rechunk(
fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
@ -878,6 +880,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|| childAtomType == Atom.TYPE_lpcm
|| childAtomType == Atom.TYPE_sowt
|| childAtomType == Atom.TYPE_twos
|| childAtomType == Atom.TYPE__mp2
|| childAtomType == Atom.TYPE__mp3
|| childAtomType == Atom.TYPE_alac
|| childAtomType == Atom.TYPE_alaw
@ -891,6 +894,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|| childAtomType == Atom.TYPE_c608) {
parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
language, out);
} else if (childAtomType == Atom.TYPE_mett) {
parseMetaDataSampleEntry(stsd, childAtomType, childStartPosition, trackId, out);
} else if (childAtomType == Atom.TYPE_camm) {
out.format =
new Format.Builder()
@ -1097,6 +1102,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
.build();
}
private static void parseMetaDataSampleEntry(
ParsableByteArray parent, int atomType, int position, int trackId, StsdData out) {
parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
if (atomType == Atom.TYPE_mett) {
parent.readNullTerminatedString(); // Skip optional content_encoding
@Nullable String mimeType = parent.readNullTerminatedString();
if (mimeType != null) {
out.format = new Format.Builder().setId(trackId).setSampleMimeType(mimeType).build();
}
}
}
/**
* Parses the edts atom (defined in ISO/IEC 14496-12 subsection 8.6.5).
*
@ -1229,7 +1246,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} else if (atomType == Atom.TYPE_twos) {
mimeType = MimeTypes.AUDIO_RAW;
pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN;
} else if (atomType == Atom.TYPE__mp3) {
} else if (atomType == Atom.TYPE__mp2 || atomType == Atom.TYPE__mp3) {
mimeType = MimeTypes.AUDIO_MPEG;
} else if (atomType == Atom.TYPE_alac) {
mimeType = MimeTypes.AUDIO_ALAC;
@ -1646,16 +1663,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
*/
int getSampleCount();
/**
* Returns the size for the next sample.
*/
/** Returns the size of each sample if fixed, or {@link C#LENGTH_UNSET} otherwise. */
int getFixedSampleSize();
/** Returns the size for the next sample. */
int readNextSampleSize();
/**
* Returns whether samples have a fixed size.
*/
boolean isFixedSampleSize();
}
/**
@ -1670,7 +1682,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
public StszSampleSizeBox(Atom.LeafAtom stszAtom) {
data = stszAtom.data;
data.setPosition(Atom.FULL_HEADER_SIZE);
fixedSampleSize = data.readUnsignedIntToInt();
int fixedSampleSize = data.readUnsignedIntToInt();
this.fixedSampleSize = fixedSampleSize == 0 ? C.LENGTH_UNSET : fixedSampleSize;
sampleCount = data.readUnsignedIntToInt();
}
@ -1680,15 +1693,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
@Override
public int readNextSampleSize() {
return fixedSampleSize == 0 ? data.readUnsignedIntToInt() : fixedSampleSize;
public int getFixedSampleSize() {
return fixedSampleSize;
}
@Override
public boolean isFixedSampleSize() {
return fixedSampleSize != 0;
public int readNextSampleSize() {
return fixedSampleSize == C.LENGTH_UNSET ? data.readUnsignedIntToInt() : fixedSampleSize;
}
}
/**
@ -1716,6 +1728,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return sampleCount;
}
@Override
public int getFixedSampleSize() {
return C.LENGTH_UNSET;
}
@Override
public int readNextSampleSize() {
if (fieldSize == 8) {
@ -1735,12 +1752,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
}
}
@Override
public boolean isFixedSampleSize() {
return false;
}
}
}

View File

@ -88,7 +88,9 @@ import java.util.Arrays;
int segmentIndex = currentSegmentIndex + segmentCount;
if (size > 0) {
if (packetArray.capacity() < packetArray.limit() + size) {
packetArray.reset(Arrays.copyOf(packetArray.getData(), packetArray.limit() + size));
packetArray.reset(
Arrays.copyOf(packetArray.getData(), packetArray.limit() + size),
/* limit= */ packetArray.limit());
}
input.readFully(packetArray.getData(), packetArray.limit(), size);
packetArray.setLimit(packetArray.limit() + size);
@ -131,7 +133,8 @@ import java.util.Arrays;
}
packetArray.reset(
Arrays.copyOf(
packetArray.getData(), max(OggPageHeader.MAX_PAGE_PAYLOAD, packetArray.limit())));
packetArray.getData(), max(OggPageHeader.MAX_PAGE_PAYLOAD, packetArray.limit())),
/* limit= */ packetArray.limit());
}
/**

View File

@ -25,6 +25,7 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
/**
* {@link StreamReader} to extract Vorbis data out of Ogg byte stream.
@ -160,8 +161,11 @@ import java.util.ArrayList;
@VisibleForTesting
/* package */ static void appendNumberOfSamples(
ParsableByteArray buffer, long packetSampleCount) {
if (buffer.capacity() < buffer.limit() + 4) {
buffer.reset(Arrays.copyOf(buffer.getData(), buffer.limit() + 4));
} else {
buffer.setLimit(buffer.limit() + 4);
}
// The vorbis decoder expects the number of samples in the packet
// to be appended to the audio data as an int32
byte[] data = buffer.getData();

View File

@ -97,11 +97,11 @@ public final class PesReader implements TsPayloadReader {
Log.w(TAG, "Unexpected start indicator reading extended header");
break;
case STATE_READING_BODY:
// If payloadSize == -1 then the length of the previous packet was unspecified, and so
// we only know that it's finished now that we've seen the start of the next one. This
// is expected. If payloadSize != -1, then the length of the previous packet was known,
// but we didn't receive that amount of data. This is not expected.
if (payloadSize != -1) {
// If payloadSize is unset then the length of the previous packet was unspecified, and so
// we only know that it's finished now that we've seen the start of the next one. This is
// expected. If payloadSize is set, then the length of the previous packet was known, but
// we didn't receive that amount of data. This is not expected.
if (payloadSize != C.LENGTH_UNSET) {
Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes");
}
// Either way, notify the reader that it has now finished.
@ -136,13 +136,13 @@ public final class PesReader implements TsPayloadReader {
break;
case STATE_READING_BODY:
readLength = data.bytesLeft();
int padding = payloadSize == -1 ? 0 : readLength - payloadSize;
int padding = payloadSize == C.LENGTH_UNSET ? 0 : readLength - payloadSize;
if (padding > 0) {
readLength -= padding;
data.setLimit(data.getPosition() + readLength);
}
reader.consume(data);
if (payloadSize != -1) {
if (payloadSize != C.LENGTH_UNSET) {
payloadSize -= readLength;
if (payloadSize == 0) {
reader.packetFinished();
@ -191,7 +191,7 @@ public final class PesReader implements TsPayloadReader {
int startCodePrefix = pesScratch.readBits(24);
if (startCodePrefix != 0x000001) {
Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix);
payloadSize = -1;
payloadSize = C.LENGTH_UNSET;
return false;
}
@ -208,10 +208,14 @@ public final class PesReader implements TsPayloadReader {
extendedHeaderLength = pesScratch.readBits(8);
if (packetLength == 0) {
payloadSize = -1;
payloadSize = C.LENGTH_UNSET;
} else {
payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */
- HEADER_SIZE - extendedHeaderLength;
if (payloadSize < 0) {
Log.w(TAG, "Found negative packet payload size: " + payloadSize);
payloadSize = C.LENGTH_UNSET;
}
}
return true;
}

View File

@ -37,13 +37,16 @@ import java.io.IOException;
private static final long SEEK_TOLERANCE_US = 100_000;
private static final int MINIMUM_SEARCH_RANGE_BYTES = 5 * TsExtractor.TS_PACKET_SIZE;
private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE;
public TsBinarySearchSeeker(
TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) {
TimestampAdjuster pcrTimestampAdjuster,
long streamDurationUs,
long inputLength,
int pcrPid,
int timestampSearchBytes) {
super(
new DefaultSeekTimestampConverter(),
new TsPcrSeeker(pcrPid, pcrTimestampAdjuster),
new TsPcrSeeker(pcrPid, pcrTimestampAdjuster, timestampSearchBytes),
streamDurationUs,
/* floorTimePosition= */ 0,
/* ceilingTimePosition= */ streamDurationUs + 1,
@ -58,7 +61,7 @@ import java.io.IOException;
* position in a TS stream.
*
* <p>Given a PCR timestamp, and a position within a TS stream, this seeker will peek up to {@link
* #TIMESTAMP_SEARCH_BYTES} from that stream position, look for all packets with PID equal to
* #timestampSearchBytes} from that stream position, look for all packets with PID equal to
* PCR_PID, and then compare the PCR timestamps (if available) of these packets to the target
* timestamp.
*/
@ -67,10 +70,13 @@ import java.io.IOException;
private final TimestampAdjuster pcrTimestampAdjuster;
private final ParsableByteArray packetBuffer;
private final int pcrPid;
private final int timestampSearchBytes;
public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) {
public TsPcrSeeker(
int pcrPid, TimestampAdjuster pcrTimestampAdjuster, int timestampSearchBytes) {
this.pcrPid = pcrPid;
this.pcrTimestampAdjuster = pcrTimestampAdjuster;
this.timestampSearchBytes = timestampSearchBytes;
packetBuffer = new ParsableByteArray();
}
@ -78,7 +84,7 @@ import java.io.IOException;
public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
throws IOException {
long inputPosition = input.getPosition();
int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition);
int bytesToSearch = (int) min(timestampSearchBytes, input.getLength() - inputPosition);
packetBuffer.reset(bytesToSearch);
input.peekFully(packetBuffer.getData(), /* offset= */ 0, bytesToSearch);

View File

@ -38,8 +38,7 @@ import java.io.IOException;
*/
/* package */ final class TsDurationReader {
private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE;
private final int timestampSearchBytes;
private final TimestampAdjuster pcrTimestampAdjuster;
private final ParsableByteArray packetBuffer;
@ -51,7 +50,8 @@ import java.io.IOException;
private long lastPcrValue;
private long durationUs;
/* package */ TsDurationReader() {
/* package */ TsDurationReader(int timestampSearchBytes) {
this.timestampSearchBytes = timestampSearchBytes;
pcrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);
firstPcrValue = C.TIME_UNSET;
lastPcrValue = C.TIME_UNSET;
@ -125,7 +125,7 @@ import java.io.IOException;
private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
throws IOException {
int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, input.getLength());
int bytesToSearch = (int) min(timestampSearchBytes, input.getLength());
int searchStartPosition = 0;
if (input.getPosition() != searchStartPosition) {
seekPositionHolder.position = searchStartPosition;
@ -161,7 +161,7 @@ import java.io.IOException;
private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
throws IOException {
long inputLength = input.getLength();
int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, inputLength);
int bytesToSearch = (int) min(timestampSearchBytes, inputLength);
long searchStartPosition = inputLength - bytesToSearch;
if (input.getPosition() != searchStartPosition) {
seekPositionHolder.position = searchStartPosition;

View File

@ -80,6 +80,9 @@ public final class TsExtractor implements Extractor {
*/
public static final int MODE_HLS = 2;
public static final int TS_PACKET_SIZE = 188;
public static final int DEFAULT_TIMESTAMP_SEARCH_BYTES = 600 * TS_PACKET_SIZE;
public static final int TS_STREAM_TYPE_MPA = 0x03;
public static final int TS_STREAM_TYPE_MPA_LSF = 0x04;
public static final int TS_STREAM_TYPE_AAC_ADTS = 0x0F;
@ -100,7 +103,6 @@ public final class TsExtractor implements Extractor {
// Stream types that aren't defined by the MPEG-2 TS specification.
public static final int TS_STREAM_TYPE_AIT = 0x101;
public static final int TS_PACKET_SIZE = 188;
public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
private static final int TS_PAT_PID = 0;
@ -115,6 +117,7 @@ public final class TsExtractor implements Extractor {
private static final int SNIFF_TS_PACKET_COUNT = 5;
private final @Mode int mode;
private final int timestampSearchBytes;
private final List<TimestampAdjuster> timestampAdjusters;
private final ParsableByteArray tsPacketBuffer;
private final SparseIntArray continuityCounters;
@ -136,7 +139,7 @@ public final class TsExtractor implements Extractor {
private int pcrPid;
public TsExtractor() {
this(0);
this(/* defaultTsPayloadReaderFlags= */ 0);
}
/**
@ -144,7 +147,7 @@ public final class TsExtractor implements Extractor {
* {@code FLAG_*} values that control the behavior of the payload readers.
*/
public TsExtractor(@Flags int defaultTsPayloadReaderFlags) {
this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags);
this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags, DEFAULT_TIMESTAMP_SEARCH_BYTES);
}
/**
@ -152,12 +155,22 @@ public final class TsExtractor implements Extractor {
* and {@link #MODE_HLS}.
* @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory}
* {@code FLAG_*} values that control the behavior of the payload readers.
* @param timestampSearchBytes The number of bytes searched from a given position in the stream to
* find a PCR timestamp. If this value is too small, the duration might be unknown and seeking
* might not be supported for high bitrate progressive streams. Setting a large value for this
* field might be inefficient though because the extractor stores a buffer of {@code
* timestampSearchBytes} bytes when determining the duration or when performing a seek
* operation. The default value is {@link #DEFAULT_TIMESTAMP_SEARCH_BYTES}. If the number of
* bytes left in the stream from the current position is less than {@code
* timestampSearchBytes}, the search is performed on the bytes left.
*/
public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) {
public TsExtractor(
@Mode int mode, @Flags int defaultTsPayloadReaderFlags, int timestampSearchBytes) {
this(
mode,
new TimestampAdjuster(0),
new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags));
new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags),
timestampSearchBytes);
}
/**
@ -170,7 +183,30 @@ public final class TsExtractor implements Extractor {
@Mode int mode,
TimestampAdjuster timestampAdjuster,
TsPayloadReader.Factory payloadReaderFactory) {
this(mode, timestampAdjuster, payloadReaderFactory, DEFAULT_TIMESTAMP_SEARCH_BYTES);
}
/**
* @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT}
* and {@link #MODE_HLS}.
* @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
* @param payloadReaderFactory Factory for injecting a custom set of payload readers.
* @param timestampSearchBytes The number of bytes searched from a given position in the stream to
* find a PCR timestamp. If this value is too small, the duration might be unknown and seeking
* might not be supported for high bitrate progressive streams. Setting a large value for this
* field might be inefficient though because the extractor stores a buffer of {@code
* timestampSearchBytes} bytes when determining the duration or when performing a seek
* operation. The default value is {@link #DEFAULT_TIMESTAMP_SEARCH_BYTES}. If the number of
* bytes left in the stream from the current position is less than {@code
* timestampSearchBytes}, the search is performed on the bytes left.
*/
public TsExtractor(
@Mode int mode,
TimestampAdjuster timestampAdjuster,
TsPayloadReader.Factory payloadReaderFactory,
int timestampSearchBytes) {
this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory);
this.timestampSearchBytes = timestampSearchBytes;
this.mode = mode;
if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) {
timestampAdjusters = Collections.singletonList(timestampAdjuster);
@ -183,7 +219,7 @@ public final class TsExtractor implements Extractor {
trackPids = new SparseBooleanArray();
tsPayloadReaders = new SparseArray<>();
continuityCounters = new SparseIntArray();
durationReader = new TsDurationReader();
durationReader = new TsDurationReader(timestampSearchBytes);
pcrPid = -1;
resetPayloadReaders();
}
@ -365,7 +401,8 @@ public final class TsExtractor implements Extractor {
durationReader.getPcrTimestampAdjuster(),
durationReader.getDurationUs(),
inputLength,
pcrPid);
pcrPid,
timestampSearchBytes);
output.seekMap(tsBinarySearchSeeker.getSeekMap());
} else {
output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));

View File

@ -0,0 +1,179 @@
/*
* 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.extractor.flv;
import static com.google.android.exoplayer2.testutil.TestUtil.extractAllSamplesFromFile;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Seeking tests for {@link FlvExtractor}. */
@RunWith(AndroidJUnit4.class)
public class FlvExtractorSeekTest {
private static final String TEST_FILE_KEY_FRAME_INDEX =
"media/flv/sample-with-key-frame-index.flv";
private static final long DURATION_US = 3_042_000;
private static final long KEY_FRAMES_INTERVAL_US = C.MICROS_PER_SECOND;
private FlvExtractor extractor;
private FakeExtractorOutput extractorOutput;
private DefaultDataSource dataSource;
@Before
public void setUp() throws Exception {
extractor = new FlvExtractor();
extractorOutput = new FakeExtractorOutput();
dataSource =
new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext())
.createDataSource();
}
@Test
public void flvExtractorReads_returnsSeekableSeekMap() throws Exception {
Uri fileUri = TestUtil.buildAssetUri(TEST_FILE_KEY_FRAME_INDEX);
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
assertThat(seekMap.getDurationUs()).isEqualTo(DURATION_US);
assertThat(seekMap.isSeekable()).isTrue();
}
@Test
public void seeking_handlesSeekToZero() throws Exception {
String fileName = TEST_FILE_KEY_FRAME_INDEX;
Uri fileUri = TestUtil.buildAssetUri(fileName);
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
int trackId = extractorOutput.trackOutputs.keyAt(0);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(trackId);
long targetSeekTimeUs = 0;
int extractedFrameIndex =
TestUtil.seekToTimeUs(
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
assertFirstFrameAfterSeekIsWithinKeyFrameInterval(
fileName, trackId, trackOutput, extractedFrameIndex, targetSeekTimeUs);
}
@Test
public void seeking_handlesSeekToEof() throws Exception {
String fileName = TEST_FILE_KEY_FRAME_INDEX;
Uri fileUri = TestUtil.buildAssetUri(fileName);
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
int trackId = extractorOutput.trackOutputs.keyAt(0);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(trackId);
long targetSeekTimeUs = seekMap.getDurationUs();
int extractedFrameIndex =
TestUtil.seekToTimeUs(
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
assertFirstFrameAfterSeekIsWithinKeyFrameInterval(
fileName, trackId, trackOutput, extractedFrameIndex, targetSeekTimeUs);
}
@Test
public void seeking_handlesSeekingBackward() throws Exception {
String fileName = TEST_FILE_KEY_FRAME_INDEX;
Uri fileUri = TestUtil.buildAssetUri(fileName);
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
int trackId = extractorOutput.trackOutputs.keyAt(0);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(trackId);
long firstSeekTimeUs = seekMap.getDurationUs() * 2 / 3;
TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);
long targetSeekTimeUs = seekMap.getDurationUs() / 3;
int extractedFrameIndex =
TestUtil.seekToTimeUs(
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
assertFirstFrameAfterSeekIsWithinKeyFrameInterval(
fileName, trackId, trackOutput, extractedFrameIndex, targetSeekTimeUs);
}
@Test
public void seeking_handlesSeekingForward() throws Exception {
String fileName = TEST_FILE_KEY_FRAME_INDEX;
Uri fileUri = TestUtil.buildAssetUri(fileName);
SeekMap seekMap = TestUtil.extractSeekMap(extractor, extractorOutput, dataSource, fileUri);
int trackId = extractorOutput.trackOutputs.keyAt(0);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(trackId);
long firstSeekTimeUs = seekMap.getDurationUs() / 3;
TestUtil.seekToTimeUs(extractor, seekMap, firstSeekTimeUs, dataSource, trackOutput, fileUri);
long targetSeekTimeUs = seekMap.getDurationUs() * 2 / 3;
int extractedFrameIndex =
TestUtil.seekToTimeUs(
extractor, seekMap, targetSeekTimeUs, dataSource, trackOutput, fileUri);
assertThat(extractedFrameIndex).isNotEqualTo(C.INDEX_UNSET);
assertFirstFrameAfterSeekIsWithinKeyFrameInterval(
fileName, trackId, trackOutput, extractedFrameIndex, targetSeekTimeUs);
}
private static void assertFirstFrameAfterSeekIsWithinKeyFrameInterval(
String fileName,
int trackId,
FakeTrackOutput trackOutput,
int firstFrameIndexAfterSeek,
long targetSeekTimeUs)
throws IOException {
long foundFrameTimeUs = trackOutput.getSampleTimeUs(firstFrameIndexAfterSeek);
assertThat(targetSeekTimeUs - foundFrameTimeUs).isAtMost(KEY_FRAMES_INTERVAL_US);
FakeTrackOutput expectedTrackOutput = getTrackOutput(fileName, trackId);
int foundFrameIndex = getFrameIndex(expectedTrackOutput, foundFrameTimeUs);
trackOutput.assertSample(
firstFrameIndexAfterSeek,
expectedTrackOutput.getSampleData(foundFrameIndex),
expectedTrackOutput.getSampleTimeUs(foundFrameIndex),
expectedTrackOutput.getSampleFlags(foundFrameIndex),
expectedTrackOutput.getSampleCryptoData(foundFrameIndex));
}
private static FakeTrackOutput getTrackOutput(String fileName, int trackId) throws IOException {
return extractAllSamplesFromFile(
new FlvExtractor(), ApplicationProvider.getApplicationContext(), fileName)
.trackOutputs
.get(trackId);
}
private static int getFrameIndex(FakeTrackOutput trackOutput, long targetSeekTimeUs) {
List<Long> frameTimes = trackOutput.getSampleTimesUs();
return Util.binarySearchFloor(
frameTimes, targetSeekTimeUs, /* inclusive= */ true, /* stayInBounds= */ false);
}
}

View File

@ -38,4 +38,10 @@ public final class FlvExtractorTest {
public void sample() throws Exception {
ExtractorAsserts.assertBehavior(FlvExtractor::new, "media/flv/sample.flv", simulationConfig);
}
@Test
public void sampleSeekable() throws Exception {
ExtractorAsserts.assertBehavior(
FlvExtractor::new, "media/flv/sample-with-key-frame-index.flv", simulationConfig);
}
}

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp4;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import org.junit.Test;
@ -64,7 +65,7 @@ public final class AtomParsersTest {
private static void verifyStz2Parsing(Atom.LeafAtom stz2Atom) {
AtomParsers.Stz2SampleSizeBox box = new AtomParsers.Stz2SampleSizeBox(stz2Atom);
assertThat(box.getSampleCount()).isEqualTo(4);
assertThat(box.isFixedSampleSize()).isFalse();
assertThat(box.getFixedSampleSize()).isEqualTo(C.LENGTH_UNSET);
for (int i = 0; i < box.getSampleCount(); i++) {
assertThat(box.readNextSampleSize()).isEqualTo(i + 1);
}

View File

@ -60,11 +60,27 @@ public final class OggExtractorParameterizedTest {
OggExtractor::new, "media/ogg/bear_vorbis.ogg", simulationConfig);
}
// Ensure the extractor can handle non-contiguous pages by using a file with 10 bytes of garbage
// data before the start of the second page.
/**
* Ensure the extractor can handle non-contiguous pages by using a file with 10 bytes of garbage
* data before the start of the second page.
*
* <p>https://github.com/google/ExoPlayer/issues/7230
*/
@Test
public void vorbisWithGapBeforeSecondPage() throws Exception {
ExtractorAsserts.assertBehavior(
OggExtractor::new, "media/ogg/bear_vorbis_gap.ogg", simulationConfig);
}
/**
* Use some very large Vorbis Comment metadata to create a packet that is larger than a single Ogg
* page.
*
* <p>https://github.com/google/ExoPlayer/issues/7992
*/
@Test
public void vorbisWithPacketSpanningBetweenPages() throws Exception {
ExtractorAsserts.assertBehavior(
OggExtractor::new, "media/ogg/bear_vorbis_with_large_metadata.ogg", simulationConfig);
}
}

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.ExtractorAsserts.AssertionConfig;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -49,7 +50,10 @@ public final class AdtsExtractorTest {
public void sample_withSeeking() throws Exception {
ExtractorAsserts.assertBehavior(
() -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING),
"media/ts/sample_cbs.adts",
"media/ts/sample.adts",
new AssertionConfig.Builder()
.setDumpFilesPrefix("extractordumps/ts/sample_cbs.adts")
.build(),
simulationConfig);
}

View File

@ -37,7 +37,7 @@ public final class TsDurationReaderTest {
@Before
public void setUp() {
tsDurationReader = new TsDurationReader();
tsDurationReader = new TsDurationReader(TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES);
seekPositionHolder = new PositionHolder();
}

View File

@ -25,13 +25,6 @@ android {
dependencies {
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion

View File

@ -592,7 +592,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public InitializationTrackSelection(TrackGroup group, int[] tracks) {
super(group, tracks);
selectedIndex = indexOf(group.getFormat(0));
// The initially selected index corresponds to the first EXT-X-STREAMINF tag in the master
// playlist.
selectedIndex = indexOf(group.getFormat(tracks[0]));
}
@Override

View File

@ -20,13 +20,6 @@ dependencies {
api 'androidx.media:media:' + androidxMediaVersion
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion
implementation ('com.google.guava:guava:' + guavaVersion) {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
}
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
testImplementation project(modulePrefix + 'testutils')

View File

@ -227,13 +227,22 @@ public class DefaultTimeBar extends View implements TimeBar {
this(context, attrs, defStyleAttr, attrs);
}
public DefaultTimeBar(
Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
@Nullable AttributeSet timebarAttrs) {
this(context, attrs, defStyleAttr, timebarAttrs, 0);
}
// Suppress warnings due to usage of View methods in the constructor.
@SuppressWarnings("nullness:method.invocation.invalid")
public DefaultTimeBar(
Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
@Nullable AttributeSet timebarAttrs) {
@Nullable AttributeSet timebarAttrs,
int defStyleRes) {
super(context, attrs, defStyleAttr);
seekBounds = new Rect();
progressBar = new Rect();
@ -262,7 +271,10 @@ public class DefaultTimeBar extends View implements TimeBar {
int defaultScrubberDraggedSize = dpToPx(density, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP);
if (timebarAttrs != null) {
TypedArray a =
context.getTheme().obtainStyledAttributes(timebarAttrs, R.styleable.DefaultTimeBar, 0, 0);
context
.getTheme()
.obtainStyledAttributes(
timebarAttrs, R.styleable.DefaultTimeBar, defStyleAttr, defStyleRes);
try {
scrubberDrawable = a.getDrawable(R.styleable.DefaultTimeBar_scrubber_drawable);
if (scrubberDrawable != null) {

View File

@ -989,7 +989,6 @@ public class PlayerNotificationManager {
Notification notification = builder.build();
notificationManager.notify(notificationId, notification);
if (!isNotificationStarted) {
isNotificationStarted = true;
context.registerReceiver(notificationBroadcastReceiver, intentFilter);
if (notificationListener != null) {
notificationListener.onNotificationStarted(notificationId, notification);
@ -997,8 +996,12 @@ public class PlayerNotificationManager {
}
@Nullable NotificationListener listener = notificationListener;
if (listener != null) {
listener.onNotificationPosted(notificationId, notification, ongoing);
// Always pass true for ongoing with the first notification to tell a service to go into
// foreground even when paused.
listener.onNotificationPosted(
notificationId, notification, ongoing || !isNotificationStarted);
}
isNotificationStarted = true;
}
// We're calling a deprecated listener method that we still want to notify.

View File

@ -459,6 +459,7 @@ public class StyledPlayerControlView extends FrameLayout {
@SuppressWarnings({
"nullness:argument.type.incompatible",
"nullness:assignment.type.incompatible",
"nullness:method.invocation.invalid",
"nullness:methodref.receiver.bound.invalid"
})
@ -526,8 +527,11 @@ public class StyledPlayerControlView extends FrameLayout {
a.recycle();
}
}
controlViewLayoutManager = new StyledPlayerControlViewLayoutManager();
controlViewLayoutManager.setAnimationEnabled(animationEnabled);
LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
componentListener = new ComponentListener();
visibilityListeners = new CopyOnWriteArrayList<>();
period = new Timeline.Period();
window = new Timeline.Window();
@ -537,13 +541,9 @@ public class StyledPlayerControlView extends FrameLayout {
playedAdGroups = new boolean[0];
extraAdGroupTimesMs = new long[0];
extraPlayedAdGroups = new boolean[0];
componentListener = new ComponentListener();
controlDispatcher = new DefaultControlDispatcher(fastForwardMs, rewindMs);
updateProgressAction = this::updateProgress;
LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
// Relating to Bottom Bar Left View
durationView = findViewById(R.id.exo_duration);
positionView = findViewById(R.id.exo_position);
@ -570,7 +570,8 @@ public class StyledPlayerControlView extends FrameLayout {
} else if (timeBarPlaceholder != null) {
// Propagate attrs as timebarAttrs so that DefaultTimeBar's custom attributes are transferred,
// but standard attributes (e.g. background) are not.
DefaultTimeBar defaultTimeBar = new DefaultTimeBar(context, null, 0, playbackAttrs);
DefaultTimeBar defaultTimeBar =
new DefaultTimeBar(context, null, 0, playbackAttrs, R.style.ExoStyledControls_TimeBar);
defaultTimeBar.setId(R.id.exo_progress);
defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams());
ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent());
@ -581,10 +582,10 @@ public class StyledPlayerControlView extends FrameLayout {
} else {
timeBar = null;
}
if (timeBar != null) {
timeBar.addListener(componentListener);
}
playPauseButton = findViewById(R.id.exo_play_pause);
if (playPauseButton != null) {
playPauseButton.setOnClickListener(componentListener);
@ -626,7 +627,6 @@ public class StyledPlayerControlView extends FrameLayout {
}
resources = context.getResources();
buttonAlphaEnabled =
(float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_enabled) / 100;
buttonAlphaDisabled =
@ -634,10 +634,12 @@ public class StyledPlayerControlView extends FrameLayout {
vrButton = findViewById(R.id.exo_vr);
if (vrButton != null) {
setShowVrButton(showVrButton);
updateButton(/* enabled= */ false, vrButton);
}
controlViewLayoutManager = new StyledPlayerControlViewLayoutManager(this);
controlViewLayoutManager.setAnimationEnabled(animationEnabled);
// Related to Settings List View
String[] settingTexts = new String[2];
Drawable[] settingIcons = new Drawable[2];
@ -1071,6 +1073,11 @@ public class StyledPlayerControlView extends FrameLayout {
controlViewLayoutManager.hide();
}
/** Hides the controller without any animation. */
public void hideImmediately() {
controlViewLayoutManager.hideImmediately();
}
/** Returns whether the controller is fully visible, which means all UI controls are visible. */
public boolean isFullyVisible() {
return controlViewLayoutManager.isFullyVisible();
@ -1159,13 +1166,14 @@ public class StyledPlayerControlView extends FrameLayout {
if (controlDispatcher instanceof DefaultControlDispatcher) {
rewindMs = ((DefaultControlDispatcher) controlDispatcher).getRewindIncrementMs();
}
long rewindSec = rewindMs / 1_000;
int rewindSec = (int) (rewindMs / 1_000);
if (rewindButtonTextView != null) {
rewindButtonTextView.setText(String.valueOf(rewindSec));
}
if (rewindButton != null) {
rewindButton.setContentDescription(
resources.getString(R.string.exo_controls_rewind_by_amount_description, rewindSec));
resources.getQuantityString(
R.plurals.exo_controls_rewind_by_amount_description, rewindSec, rewindSec));
}
}
@ -1173,14 +1181,16 @@ public class StyledPlayerControlView extends FrameLayout {
if (controlDispatcher instanceof DefaultControlDispatcher) {
fastForwardMs = ((DefaultControlDispatcher) controlDispatcher).getFastForwardIncrementMs();
}
long fastForwardSec = fastForwardMs / 1_000;
int fastForwardSec = (int) (fastForwardMs / 1_000);
if (fastForwardButtonTextView != null) {
fastForwardButtonTextView.setText(String.valueOf(fastForwardSec));
}
if (fastForwardButton != null) {
fastForwardButton.setContentDescription(
resources.getString(
R.string.exo_controls_fastforward_by_amount_description, fastForwardSec));
resources.getQuantityString(
R.plurals.exo_controls_fastforward_by_amount_description,
fastForwardSec,
fastForwardSec));
}
}
@ -1604,7 +1614,7 @@ public class StyledPlayerControlView extends FrameLayout {
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
controlViewLayoutManager.onViewAttached(this);
controlViewLayoutManager.onAttachedToWindow();
isAttachedToWindow = true;
if (isFullyVisible()) {
controlViewLayoutManager.resetHideCallbacks();
@ -1615,7 +1625,7 @@ public class StyledPlayerControlView extends FrameLayout {
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
controlViewLayoutManager.onViewDetached(this);
controlViewLayoutManager.onDetachedFromWindow();
isAttachedToWindow = false;
removeCallbacks(updateProgressAction);
controlViewLayoutManager.removeHideCallbacks();
@ -2002,11 +2012,13 @@ public class StyledPlayerControlView extends FrameLayout {
break;
}
}
checkNotNull(subtitleButton)
.setImageDrawable(subtitleIsOn ? subtitleOnButtonDrawable : subtitleOffButtonDrawable);
checkNotNull(subtitleButton)
.setContentDescription(
if (subtitleButton != null) {
subtitleButton.setImageDrawable(
subtitleIsOn ? subtitleOnButtonDrawable : subtitleOffButtonDrawable);
subtitleButton.setContentDescription(
subtitleIsOn ? subtitleOnContentDescription : subtitleOffContentDescription);
}
this.rendererIndices = rendererIndices;
this.tracks = trackInfos;
this.mappedTrackInfo = mappedTrackInfo;

View File

@ -47,6 +47,26 @@ import java.util.List;
// Int for defining the UX state where the views are being animated to be shown.
private static final int UX_STATE_ANIMATING_SHOW = 4;
private final StyledPlayerControlView styledPlayerControlView;
@Nullable private final ViewGroup embeddedTransportControls;
@Nullable private final ViewGroup bottomBar;
@Nullable private final ViewGroup minimalControls;
@Nullable private final ViewGroup basicControls;
@Nullable private final ViewGroup extraControls;
@Nullable private final ViewGroup extraControlsScrollView;
@Nullable private final ViewGroup timeView;
@Nullable private final View timeBar;
@Nullable private final View overflowShowButton;
private final AnimatorSet hideMainBarsAnimator;
private final AnimatorSet hideProgressBarAnimator;
private final AnimatorSet hideAllBarsAnimator;
private final AnimatorSet showMainBarsAnimator;
private final AnimatorSet showAllBarsAnimator;
private final ValueAnimator overflowShowAnimator;
private final ValueAnimator overflowHideAnimator;
private final Runnable showAllBarsRunnable;
private final Runnable hideAllBarsRunnable;
private final Runnable hideProgressBarRunnable;
@ -57,32 +77,16 @@ import java.util.List;
private final List<View> shownButtons;
private int uxState;
private boolean initiallyHidden;
private boolean isMinimalMode;
private boolean needToShowBars;
private boolean animationEnabled;
@Nullable private StyledPlayerControlView styledPlayerControlView;
@Nullable private ViewGroup embeddedTransportControls;
@Nullable private ViewGroup bottomBar;
@Nullable private ViewGroup minimalControls;
@Nullable private ViewGroup basicControls;
@Nullable private ViewGroup extraControls;
@Nullable private ViewGroup extraControlsScrollView;
@Nullable private ViewGroup timeView;
@Nullable private View timeBar;
@Nullable private View overflowShowButton;
@Nullable private AnimatorSet hideMainBarsAnimator;
@Nullable private AnimatorSet hideProgressBarAnimator;
@Nullable private AnimatorSet hideAllBarsAnimator;
@Nullable private AnimatorSet showMainBarsAnimator;
@Nullable private AnimatorSet showAllBarsAnimator;
@Nullable private ValueAnimator overflowShowAnimator;
@Nullable private ValueAnimator overflowHideAnimator;
public StyledPlayerControlViewLayoutManager() {
@SuppressWarnings({
"nullness:method.invocation.invalid",
"nullness:methodref.receiver.bound.invalid"
})
public StyledPlayerControlViewLayoutManager(StyledPlayerControlView styledPlayerControlView) {
this.styledPlayerControlView = styledPlayerControlView;
showAllBarsRunnable = this::showAllBars;
hideAllBarsRunnable = this::hideAllBars;
hideProgressBarRunnable = this::hideProgressBar;
@ -92,121 +96,48 @@ import java.util.List;
animationEnabled = true;
uxState = UX_STATE_ALL_VISIBLE;
shownButtons = new ArrayList<>();
}
public void show() {
initiallyHidden = false;
if (this.styledPlayerControlView == null) {
return;
}
StyledPlayerControlView styledPlayerControlView = this.styledPlayerControlView;
if (!styledPlayerControlView.isVisible()) {
styledPlayerControlView.setVisibility(View.VISIBLE);
styledPlayerControlView.updateAll();
styledPlayerControlView.requestPlayPauseFocus();
}
styledPlayerControlView.post(showAllBarsRunnable);
}
public void hide() {
initiallyHidden = true;
if (styledPlayerControlView == null
|| uxState == UX_STATE_ANIMATING_HIDE
|| uxState == UX_STATE_NONE_VISIBLE) {
return;
}
removeHideCallbacks();
if (!animationEnabled) {
postDelayedRunnable(hideControllerRunnable, 0);
} else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) {
postDelayedRunnable(hideProgressBarRunnable, 0);
} else {
postDelayedRunnable(hideAllBarsRunnable, 0);
}
}
public void setAnimationEnabled(boolean animationEnabled) {
this.animationEnabled = animationEnabled;
}
public boolean isAnimationEnabled() {
return animationEnabled;
}
public void resetHideCallbacks() {
if (uxState == UX_STATE_ANIMATING_HIDE) {
return;
}
removeHideCallbacks();
int showTimeoutMs =
styledPlayerControlView != null ? styledPlayerControlView.getShowTimeoutMs() : 0;
if (showTimeoutMs > 0) {
if (!animationEnabled) {
postDelayedRunnable(hideControllerRunnable, showTimeoutMs);
} else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) {
postDelayedRunnable(hideProgressBarRunnable, ANIMATION_INTERVAL_MS);
} else {
postDelayedRunnable(hideMainBarsRunnable, showTimeoutMs);
}
}
}
public void removeHideCallbacks() {
if (styledPlayerControlView == null) {
return;
}
styledPlayerControlView.removeCallbacks(hideControllerRunnable);
styledPlayerControlView.removeCallbacks(hideAllBarsRunnable);
styledPlayerControlView.removeCallbacks(hideMainBarsRunnable);
styledPlayerControlView.removeCallbacks(hideProgressBarRunnable);
}
// TODO(insun): Pass StyledPlayerControlView to constructor and reduce multiple nullchecks.
public void onViewAttached(StyledPlayerControlView v) {
styledPlayerControlView = v;
v.setVisibility(initiallyHidden ? View.GONE : View.VISIBLE);
v.addOnLayoutChangeListener(onLayoutChangeListener);
// Relating to Center View
ViewGroup centerView = v.findViewById(R.id.exo_center_view);
embeddedTransportControls = v.findViewById(R.id.exo_embedded_transport_controls);
ViewGroup centerView = styledPlayerControlView.findViewById(R.id.exo_center_view);
embeddedTransportControls =
styledPlayerControlView.findViewById(R.id.exo_embedded_transport_controls);
// Relating to Minimal Layout
minimalControls = v.findViewById(R.id.exo_minimal_controls);
minimalControls = styledPlayerControlView.findViewById(R.id.exo_minimal_controls);
// Relating to Bottom Bar View
ViewGroup bottomBar = v.findViewById(R.id.exo_bottom_bar);
bottomBar = styledPlayerControlView.findViewById(R.id.exo_bottom_bar);
// Relating to Bottom Bar Left View
timeView = v.findViewById(R.id.exo_time);
View timeBar = v.findViewById(R.id.exo_progress);
timeView = styledPlayerControlView.findViewById(R.id.exo_time);
timeBar = styledPlayerControlView.findViewById(R.id.exo_progress);
// Relating to Bottom Bar Right View
basicControls = v.findViewById(R.id.exo_basic_controls);
extraControls = v.findViewById(R.id.exo_extra_controls);
extraControlsScrollView = v.findViewById(R.id.exo_extra_controls_scroll_view);
overflowShowButton = v.findViewById(R.id.exo_overflow_show);
View overflowHideButton = v.findViewById(R.id.exo_overflow_hide);
basicControls = styledPlayerControlView.findViewById(R.id.exo_basic_controls);
extraControls = styledPlayerControlView.findViewById(R.id.exo_extra_controls);
extraControlsScrollView =
styledPlayerControlView.findViewById(R.id.exo_extra_controls_scroll_view);
overflowShowButton = styledPlayerControlView.findViewById(R.id.exo_overflow_show);
View overflowHideButton = styledPlayerControlView.findViewById(R.id.exo_overflow_hide);
if (overflowShowButton != null && overflowHideButton != null) {
overflowShowButton.setOnClickListener(this::onOverflowButtonClick);
overflowHideButton.setOnClickListener(this::onOverflowButtonClick);
}
this.bottomBar = bottomBar;
this.timeBar = timeBar;
Resources resources = v.getResources();
float progressBarHeight = resources.getDimension(R.dimen.exo_custom_progress_thumb_size);
float bottomBarHeight = resources.getDimension(R.dimen.exo_bottom_bar_height);
Resources resources = styledPlayerControlView.getResources();
float bottomBarHeight =
resources.getDimension(R.dimen.exo_bottom_bar_height)
- resources.getDimension(R.dimen.exo_styled_progress_bar_height);
float progressBarHeight =
resources.getDimension(R.dimen.exo_styled_progress_margin_bottom)
+ resources.getDimension(R.dimen.exo_styled_progress_layout_height)
- bottomBarHeight;
ValueAnimator fadeOutAnimator = ValueAnimator.ofFloat(1.0f, 0.0f);
fadeOutAnimator.setInterpolator(new LinearInterpolator());
fadeOutAnimator.addUpdateListener(
animation -> {
float animatedValue = (float) animation.getAnimatedValue();
if (centerView != null) {
centerView.setAlpha(animatedValue);
}
@ -239,7 +170,6 @@ import java.util.List;
fadeInAnimator.addUpdateListener(
animation -> {
float animatedValue = (float) animation.getAnimatedValue();
if (centerView != null) {
centerView.setAlpha(animatedValue);
}
@ -276,9 +206,7 @@ import java.util.List;
public void onAnimationEnd(Animator animation) {
setUxState(UX_STATE_ONLY_PROGRESS_VISIBLE);
if (needToShowBars) {
if (styledPlayerControlView != null) {
styledPlayerControlView.post(showAllBarsRunnable);
}
needToShowBars = false;
}
}
@ -301,9 +229,7 @@ import java.util.List;
public void onAnimationEnd(Animator animation) {
setUxState(UX_STATE_NONE_VISIBLE);
if (needToShowBars) {
if (styledPlayerControlView != null) {
styledPlayerControlView.post(showAllBarsRunnable);
}
needToShowBars = false;
}
}
@ -325,9 +251,7 @@ import java.util.List;
public void onAnimationEnd(Animator animation) {
setUxState(UX_STATE_NONE_VISIBLE);
if (needToShowBars) {
if (styledPlayerControlView != null) {
styledPlayerControlView.post(showAllBarsRunnable);
}
needToShowBars = false;
}
}
@ -420,14 +344,78 @@ import java.util.List;
});
}
public void onViewDetached(StyledPlayerControlView v) {
v.removeOnLayoutChangeListener(onLayoutChangeListener);
public void show() {
if (!styledPlayerControlView.isVisible()) {
styledPlayerControlView.setVisibility(View.VISIBLE);
styledPlayerControlView.updateAll();
styledPlayerControlView.requestPlayPauseFocus();
}
showAllBars();
}
public void hide() {
if (uxState == UX_STATE_ANIMATING_HIDE || uxState == UX_STATE_NONE_VISIBLE) {
return;
}
removeHideCallbacks();
if (!animationEnabled) {
hideController();
} else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) {
hideProgressBar();
} else {
hideAllBars();
}
}
public void hideImmediately() {
if (uxState == UX_STATE_ANIMATING_HIDE || uxState == UX_STATE_NONE_VISIBLE) {
return;
}
removeHideCallbacks();
hideController();
}
public void setAnimationEnabled(boolean animationEnabled) {
this.animationEnabled = animationEnabled;
}
public boolean isAnimationEnabled() {
return animationEnabled;
}
public void resetHideCallbacks() {
if (uxState == UX_STATE_ANIMATING_HIDE) {
return;
}
removeHideCallbacks();
int showTimeoutMs = styledPlayerControlView.getShowTimeoutMs();
if (showTimeoutMs > 0) {
if (!animationEnabled) {
postDelayedRunnable(hideControllerRunnable, showTimeoutMs);
} else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) {
postDelayedRunnable(hideProgressBarRunnable, ANIMATION_INTERVAL_MS);
} else {
postDelayedRunnable(hideMainBarsRunnable, showTimeoutMs);
}
}
}
public void removeHideCallbacks() {
styledPlayerControlView.removeCallbacks(hideControllerRunnable);
styledPlayerControlView.removeCallbacks(hideAllBarsRunnable);
styledPlayerControlView.removeCallbacks(hideMainBarsRunnable);
styledPlayerControlView.removeCallbacks(hideProgressBarRunnable);
}
public void onAttachedToWindow() {
styledPlayerControlView.addOnLayoutChangeListener(onLayoutChangeListener);
}
public void onDetachedFromWindow() {
styledPlayerControlView.removeOnLayoutChangeListener(onLayoutChangeListener);
}
public boolean isFullyVisible() {
if (styledPlayerControlView == null) {
return false;
}
return uxState == UX_STATE_ALL_VISIBLE && styledPlayerControlView.isVisible();
}
@ -455,8 +443,6 @@ import java.util.List;
private void setUxState(int uxState) {
int prevUxState = this.uxState;
this.uxState = uxState;
if (styledPlayerControlView != null) {
StyledPlayerControlView styledPlayerControlView = this.styledPlayerControlView;
if (uxState == UX_STATE_NONE_VISIBLE) {
styledPlayerControlView.setVisibility(View.GONE);
} else if (prevUxState == UX_STATE_NONE_VISIBLE) {
@ -468,7 +454,6 @@ import java.util.List;
styledPlayerControlView.notifyOnVisibilityChange();
}
}
}
private void onLayoutChange(
View v,
@ -494,9 +479,9 @@ import java.util.List;
private void onOverflowButtonClick(View v) {
resetHideCallbacks();
if (v.getId() == R.id.exo_overflow_show && overflowShowAnimator != null) {
if (v.getId() == R.id.exo_overflow_show) {
overflowShowAnimator.start();
} else if (v.getId() == R.id.exo_overflow_hide && overflowHideAnimator != null) {
} else if (v.getId() == R.id.exo_overflow_hide) {
overflowHideAnimator.start();
}
}
@ -510,14 +495,10 @@ import java.util.List;
switch (uxState) {
case UX_STATE_NONE_VISIBLE:
if (showAllBarsAnimator != null) {
showAllBarsAnimator.start();
}
break;
case UX_STATE_ONLY_PROGRESS_VISIBLE:
if (showMainBarsAnimator != null) {
showMainBarsAnimator.start();
}
break;
case UX_STATE_ANIMATING_HIDE:
needToShowBars = true;
@ -531,23 +512,14 @@ import java.util.List;
}
private void hideAllBars() {
if (hideAllBarsAnimator == null) {
return;
}
hideAllBarsAnimator.start();
}
private void hideProgressBar() {
if (hideProgressBarAnimator == null) {
return;
}
hideProgressBarAnimator.start();
}
private void hideMainBars() {
if (hideMainBarsAnimator == null) {
return;
}
hideMainBarsAnimator.start();
postDelayedRunnable(hideProgressBarRunnable, ANIMATION_INTERVAL_MS);
}
@ -561,7 +533,7 @@ import java.util.List;
}
private void postDelayedRunnable(Runnable runnable, long interval) {
if (styledPlayerControlView != null && interval >= 0) {
if (interval >= 0) {
styledPlayerControlView.postDelayed(runnable, interval);
}
}
@ -582,19 +554,14 @@ import java.util.List;
}
private boolean shouldBeMinimalMode() {
if (this.styledPlayerControlView == null) {
return isMinimalMode;
}
ViewGroup playerControlView = this.styledPlayerControlView;
int width =
playerControlView.getWidth()
- playerControlView.getPaddingLeft()
- playerControlView.getPaddingRight();
styledPlayerControlView.getWidth()
- styledPlayerControlView.getPaddingLeft()
- styledPlayerControlView.getPaddingRight();
int height =
playerControlView.getHeight()
- playerControlView.getPaddingBottom()
- playerControlView.getPaddingTop();
styledPlayerControlView.getHeight()
- styledPlayerControlView.getPaddingBottom()
- styledPlayerControlView.getPaddingTop();
int defaultModeWidth =
Math.max(
getWidth(embeddedTransportControls), getWidth(timeView) + getWidth(overflowShowButton));
@ -605,16 +572,11 @@ import java.util.List;
}
private void updateLayoutForSizeChange() {
if (this.styledPlayerControlView == null) {
return;
}
StyledPlayerControlView playerControlView = this.styledPlayerControlView;
if (minimalControls != null) {
minimalControls.setVisibility(isMinimalMode ? View.VISIBLE : View.INVISIBLE);
}
View fullScreenButton = playerControlView.findViewById(R.id.exo_fullscreen);
View fullScreenButton = styledPlayerControlView.findViewById(R.id.exo_fullscreen);
if (fullScreenButton != null) {
ViewGroup parent = (ViewGroup) fullScreenButton.getParent();
parent.removeView(fullScreenButton);
@ -629,12 +591,11 @@ import java.util.List;
}
}
if (timeBar != null) {
View timeBar = this.timeBar;
MarginLayoutParams timeBarParams = (MarginLayoutParams) timeBar.getLayoutParams();
int timeBarMarginBottom =
playerControlView
styledPlayerControlView
.getResources()
.getDimensionPixelSize(R.dimen.exo_custom_progress_margin_bottom);
.getDimensionPixelSize(R.dimen.exo_styled_progress_margin_bottom);
timeBarParams.bottomMargin = (isMinimalMode ? 0 : timeBarMarginBottom);
timeBar.setLayoutParams(timeBarParams);
if (timeBar instanceof DefaultTimeBar
@ -668,18 +629,14 @@ import java.util.List;
if (basicControls == null || extraControls == null) {
return;
}
ViewGroup basicControls = this.basicControls;
ViewGroup extraControls = this.extraControls;
int width =
(styledPlayerControlView != null
? styledPlayerControlView.getWidth()
styledPlayerControlView.getWidth()
- styledPlayerControlView.getPaddingLeft()
- styledPlayerControlView.getPaddingRight()
: 0);
int basicBottomBarWidth = getWidth(timeView);
- styledPlayerControlView.getPaddingRight();
int bottomBarWidth = getWidth(timeView);
for (int i = 0; i < basicControls.getChildCount(); ++i) {
basicBottomBarWidth += basicControls.getChildAt(i).getWidth();
bottomBarWidth += basicControls.getChildAt(i).getWidth();
}
// BasicControls keeps overflow button at least.
@ -687,7 +644,7 @@ import java.util.List;
// ExtraControls keeps overflow button and settings button at least.
int minExtraControlsChildCount = 2;
if (basicBottomBarWidth > width) {
if (bottomBarWidth > width) {
// move control views from basicControls to extraControls
ArrayList<View> movingChildren = new ArrayList<>();
int movingWidth = 0;
@ -696,7 +653,7 @@ import java.util.List;
View child = basicControls.getChildAt(index);
movingWidth += child.getWidth();
movingChildren.add(child);
if (basicBottomBarWidth - movingWidth <= width) {
if (bottomBarWidth - movingWidth <= width) {
break;
}
}
@ -711,14 +668,14 @@ import java.util.List;
}
} else {
// move controls from extraControls to basicControls if possible, else do nothing
// Move controls from extraControls to basicControls if possible, else do nothing.
ArrayList<View> movingChildren = new ArrayList<>();
int movingWidth = 0;
int startIndex = extraControls.getChildCount() - minExtraControlsChildCount - 1;
for (int index = startIndex; index >= 0; index--) {
View child = extraControls.getChildAt(index);
movingWidth += child.getWidth();
if (basicBottomBarWidth + movingWidth > width) {
if (bottomBarWidth + movingWidth > width) {
break;
}
movingChildren.add(child);

View File

@ -511,11 +511,11 @@ public class StyledPlayerView extends FrameLayout implements AdsLoader.AdViewPro
this.controllerAutoShow = controllerAutoShow;
this.controllerHideDuringAds = controllerHideDuringAds;
this.useController = useController && controller != null;
hideController();
updateContentDescription();
if (controller != null) {
controller.hideImmediately();
controller.addVisibilityListener(/* listener= */ componentListener);
}
updateContentDescription();
}
/**

View File

@ -414,5 +414,4 @@ public final class SubtitleView extends FrameLayout implements TextOutput {
return cue;
}
}

View File

@ -25,6 +25,7 @@ import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
@ -32,6 +33,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedT
import com.google.android.exoplayer2.trackselection.TrackSelectionUtil;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/** Builder for a dialog with a {@link TrackSelectionView}. */
@ -62,6 +64,7 @@ public final class TrackSelectionDialogBuilder {
@Nullable private TrackNameProvider trackNameProvider;
private boolean isDisabled;
private List<SelectionOverride> overrides;
@Nullable private Comparator<Format> trackFormatComparator;
/**
* Creates a builder for a track selection dialog.
@ -208,6 +211,16 @@ public final class TrackSelectionDialogBuilder {
return this;
}
/**
* Sets a {@link Comparator} used to determine the display order of the tracks within each track
* group.
*
* @param trackFormatComparator The comparator, or {@code null} to use the original order.
*/
public void setTrackFormatComparator(@Nullable Comparator<Format> trackFormatComparator) {
this.trackFormatComparator = trackFormatComparator;
}
/**
* Sets the {@link TrackNameProvider} used to generate the user visible name of each track and
* updates the view with track names queried from the specified provider.
@ -287,7 +300,13 @@ public final class TrackSelectionDialogBuilder {
if (trackNameProvider != null) {
selectionView.setTrackNameProvider(trackNameProvider);
}
selectionView.init(mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ null);
selectionView.init(
mappedTrackInfo,
rendererIndex,
isDisabled,
overrides,
trackFormatComparator,
/* listener= */ null);
return (dialog, which) ->
callback.onTracksSelected(selectionView.getIsDisabled(), selectionView.getOverrides());
}

View File

@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ui;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Pair;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
@ -26,6 +25,7 @@ import android.widget.CheckedTextView;
import android.widget.LinearLayout;
import androidx.annotation.AttrRes;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
@ -35,6 +35,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedT
import com.google.android.exoplayer2.util.Assertions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@ -71,6 +72,7 @@ public class TrackSelectionView extends LinearLayout {
private int rendererIndex;
private TrackGroupArray trackGroups;
private boolean isDisabled;
@Nullable private Comparator<TrackInfo> trackInfoComparator;
@Nullable private TrackSelectionListener listener;
/** Creates a track selection view. */
@ -196,6 +198,8 @@ public class TrackSelectionView extends LinearLayout {
* @param overrides List of initial overrides to be shown for this renderer. There must be at most
* one override for each track group. If {@link #setAllowMultipleOverrides(boolean)} hasn't
* been set to {@code true}, only the first override is used.
* @param trackFormatComparator An optional comparator used to determine the display order of the
* tracks within each track group.
* @param listener An optional listener for track selection updates.
*/
public void init(
@ -203,10 +207,15 @@ public class TrackSelectionView extends LinearLayout {
int rendererIndex,
boolean isDisabled,
List<SelectionOverride> overrides,
@Nullable Comparator<Format> trackFormatComparator,
@Nullable TrackSelectionListener listener) {
this.mappedTrackInfo = mappedTrackInfo;
this.rendererIndex = rendererIndex;
this.isDisabled = isDisabled;
this.trackInfoComparator =
trackFormatComparator == null
? null
: (o1, o2) -> trackFormatComparator.compare(o1.format, o2.format);
this.listener = listener;
int maxOverrides = allowMultipleOverrides ? overrides.size() : Math.min(overrides.size(), 1);
for (int i = 0; i < maxOverrides; i++) {
@ -259,7 +268,16 @@ public class TrackSelectionView extends LinearLayout {
TrackGroup group = trackGroups.get(groupIndex);
boolean enableMultipleChoiceForAdaptiveSelections = shouldEnableAdaptiveSelection(groupIndex);
trackViews[groupIndex] = new CheckedTextView[group.length];
TrackInfo[] trackInfos = new TrackInfo[group.length];
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
trackInfos[trackIndex] = new TrackInfo(groupIndex, trackIndex, group.getFormat(trackIndex));
}
if (trackInfoComparator != null) {
Arrays.sort(trackInfos, trackInfoComparator);
}
for (int trackIndex = 0; trackIndex < trackInfos.length; trackIndex++) {
if (trackIndex == 0) {
addView(inflater.inflate(R.layout.exo_list_divider, this, false));
}
@ -270,11 +288,11 @@ public class TrackSelectionView extends LinearLayout {
CheckedTextView trackView =
(CheckedTextView) inflater.inflate(trackViewLayoutId, this, false);
trackView.setBackgroundResource(selectableItemBackgroundResourceId);
trackView.setText(trackNameProvider.getTrackName(group.getFormat(trackIndex)));
trackView.setText(trackNameProvider.getTrackName(trackInfos[trackIndex].format));
if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
== RendererCapabilities.FORMAT_HANDLED) {
trackView.setFocusable(true);
trackView.setTag(Pair.create(groupIndex, trackIndex));
trackView.setTag(trackInfos[trackIndex]);
trackView.setOnClickListener(componentListener);
} else {
trackView.setFocusable(false);
@ -294,7 +312,12 @@ public class TrackSelectionView extends LinearLayout {
for (int i = 0; i < trackViews.length; i++) {
SelectionOverride override = overrides.get(i);
for (int j = 0; j < trackViews[i].length; j++) {
trackViews[i][j].setChecked(override != null && override.containsTrack(j));
if (override != null) {
TrackInfo trackInfo = (TrackInfo) Assertions.checkNotNull(trackViews[i][j].getTag());
trackViews[i][j].setChecked(override.containsTrack(trackInfo.trackIndex));
} else {
trackViews[i][j].setChecked(false);
}
}
}
}
@ -325,10 +348,9 @@ public class TrackSelectionView extends LinearLayout {
private void onTrackViewClicked(View view) {
isDisabled = false;
@SuppressWarnings("unchecked")
Pair<Integer, Integer> tag = (Pair<Integer, Integer>) Assertions.checkNotNull(view.getTag());
int groupIndex = tag.first;
int trackIndex = tag.second;
TrackInfo trackInfo = (TrackInfo) Assertions.checkNotNull(view.getTag());
int groupIndex = trackInfo.groupIndex;
int trackIndex = trackInfo.trackIndex;
SelectionOverride override = overrides.get(groupIndex);
Assertions.checkNotNull(mappedTrackInfo);
if (override == null) {
@ -406,4 +428,16 @@ public class TrackSelectionView extends LinearLayout {
TrackSelectionView.this.onClick(view);
}
}
private static final class TrackInfo {
public final int groupIndex;
public final int trackIndex;
public final Format format;
public TrackInfo(int groupIndex, int trackIndex, Format format) {
this.groupIndex = groupIndex;
this.trackIndex = trackIndex;
this.format = format;
}
}
}

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@android:id/background">
<shape android:shape="rectangle" >
<solid android:color="#26000000" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape android:shape="rectangle" >
<solid android:color="#5Cffffff" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape android:shape="rectangle" >
<solid android:color="#ffffff" />
</shape>
</clip>
</item>
</layer-list>

View File

@ -20,9 +20,19 @@
android:orientation="horizontal"
android:visibility="visible">
<ImageButton android:id="@+id/exo_prev" style="@style/ExoStyledControls.Button.Center.Previous" />
<Button android:id="@+id/exo_rew_with_amount" style="@style/ExoStyledControls.Button.Center.RewWithAmount" />
<ImageButton android:id="@+id/exo_play_pause" style="@style/ExoStyledControls.Button.Center.PlayPause" />
<Button android:id="@+id/exo_ffwd_with_amount" style="@style/ExoStyledControls.Button.Center.FfwdWithAmount" />
<ImageButton android:id="@+id/exo_next" style="@style/ExoStyledControls.Button.Center.Next" />
<ImageButton android:id="@+id/exo_prev"
style="@style/ExoStyledControls.Button.Center.Previous"/>
<Button android:id="@+id/exo_rew_with_amount"
style="@style/ExoStyledControls.Button.Center.RewWithAmount"/>
<ImageButton android:id="@+id/exo_play_pause"
style="@style/ExoStyledControls.Button.Center.PlayPause"/>
<Button android:id="@+id/exo_ffwd_with_amount"
style="@style/ExoStyledControls.Button.Center.FfwdWithAmount"/>
<ImageButton android:id="@+id/exo_next"
style="@style/ExoStyledControls.Button.Center.Next"/>
</LinearLayout>

View File

@ -14,40 +14,38 @@
limitations under the License.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<FrameLayout
android:id="@+id/exo_center_view"
<FrameLayout android:id="@+id/exo_center_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layoutDirection="ltr">
<View
android:id="@+id/exo_center_view_background"
<View android:id="@+id/exo_center_view_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@color/exo_black_opacity_30" />
android:background="@color/exo_black_opacity_30"/>
<include
android:id="@+id/exo_embedded_transport_controls"
<include android:id="@+id/exo_embedded_transport_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
layout="@layout/exo_styled_embedded_transport_controls" />
layout="@layout/exo_styled_embedded_transport_controls"/>
</FrameLayout>
<FrameLayout
android:id="@+id/exo_bottom_bar"
<FrameLayout android:id="@+id/exo_bottom_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/exo_bottom_bar_height"
android:layout_gravity="bottom"
android:background="@color/exo_bottom_bar_background"
android:paddingBottom="@dimen/exo_bottom_bar_padding_bottom"
android:layoutDirection="ltr">
<LinearLayout
android:id="@+id/exo_time"
<LinearLayout android:id="@+id/exo_time"
android:layout_width="@dimen/exo_time_view_width"
android:layout_height="@dimen/exo_bottom_bar_height"
android:layout_height="@dimen/exo_small_icon_height"
android:paddingStart="@dimen/exo_time_view_padding"
android:paddingEnd="@dimen/exo_time_view_padding"
android:paddingLeft="@dimen/exo_time_view_padding"
@ -55,92 +53,84 @@
android:layout_gravity="bottom|start"
android:layoutDirection="ltr">
<TextView
android:id="@+id/exo_position"
style="@style/ExoStyledControls.TimeText.Position" />
<TextView android:id="@+id/exo_position"
style="@style/ExoStyledControls.TimeText.Position"/>
<TextView
android:id="@+id/exo_time_interpunct"
style="@style/ExoStyledControls.TimeText.Interpunct" />
style="@style/ExoStyledControls.TimeText.Separator"/>
<TextView android:id="@+id/exo_duration"
style="@style/ExoStyledControls.TimeText.Duration"/>
<TextView
android:id="@+id/exo_duration"
style="@style/ExoStyledControls.TimeText.Duration" />
</LinearLayout>
<LinearLayout
android:id="@+id/exo_basic_controls"
<LinearLayout android:id="@+id/exo_basic_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:orientation="horizontal"
android:layoutDirection="ltr">
<ImageButton
android:id="@id/exo_shuffle"
<ImageButton android:id="@id/exo_shuffle"
style="@style/ExoStyledControls.Button.Bottom.Shuffle"/>
<ImageButton
android:id="@id/exo_repeat_toggle"
<ImageButton android:id="@id/exo_repeat_toggle"
style="@style/ExoStyledControls.Button.Bottom.RepeatToggle"/>
<ImageButton
android:id="@id/exo_vr"
<ImageButton android:id="@id/exo_vr"
style="@style/ExoStyledControls.Button.Bottom.VR"/>
<ImageButton
android:id="@+id/exo_subtitle"
<ImageButton android:id="@+id/exo_subtitle"
style="@style/ExoStyledControls.Button.Bottom.CC"
android:alpha="0.5"
android:scaleType="fitCenter"/>
<ImageButton
android:id="@+id/exo_fullscreen"
<ImageButton android:id="@+id/exo_fullscreen"
style="@style/ExoStyledControls.Button.Bottom.FullScreen"/>
<ImageButton
android:id="@+id/exo_overflow_show"
style="@style/ExoStyledControls.Button.Bottom.OverflowShow" />
<ImageButton android:id="@+id/exo_overflow_show"
style="@style/ExoStyledControls.Button.Bottom.OverflowShow"/>
</LinearLayout>
<HorizontalScrollView
android:id="@+id/exo_extra_controls_scroll_view"
<HorizontalScrollView android:id="@+id/exo_extra_controls_scroll_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:visibility="invisible">
<LinearLayout
android:id="@+id/exo_extra_controls"
<LinearLayout android:id="@+id/exo_extra_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:orientation="horizontal"
android:layoutDirection="ltr">
<ImageButton
android:id="@+id/exo_settings"
style="@style/ExoStyledControls.Button.Bottom.Settings" />
<ImageButton android:id="@+id/exo_settings"
style="@style/ExoStyledControls.Button.Bottom.Settings"/>
<ImageButton android:id="@+id/exo_overflow_hide"
style="@style/ExoStyledControls.Button.Bottom.OverflowHide"/>
<ImageButton
android:id="@+id/exo_overflow_hide"
style="@style/ExoStyledControls.Button.Bottom.OverflowHide" />
</LinearLayout>
</HorizontalScrollView>
</FrameLayout>
<LinearLayout
android:id="@+id/exo_minimal_controls"
<LinearLayout android:id="@+id/exo_minimal_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="bottom|end"
android:layout_marginBottom="@dimen/exo_custom_progress_thumb_size"
android:layout_marginBottom="@dimen/exo_styled_progress_layout_height"
android:visibility="invisible">
</LinearLayout>
<View android:id="@id/exo_progress_placeholder"
android:layout_width="match_parent"
android:layout_height="@dimen/exo_custom_progress_thumb_size"
android:layout_height="@dimen/exo_styled_progress_layout_height"
android:layout_gravity="bottom"
android:layout_marginBottom="@dimen/exo_custom_progress_margin_bottom"/>
android:layout_marginBottom="@dimen/exo_styled_progress_margin_bottom"/>
</merge>

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