mirror of
https://github.com/androidx/media.git
synced 2025-05-06 23:20:42 +08:00
commit
be13805ce4
@ -1,5 +1,77 @@
|
|||||||
# Release notes
|
# 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) ###
|
### 2.12.0 (2020-09-11) ###
|
||||||
|
|
||||||
To learn more about what's new in 2.12, read the corresponding
|
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
|
* Redefine `Cue.lineType=LINE_TYPE_NUMBER` in terms of aligning the cue
|
||||||
text lines to grid of viewport lines. Only consider `Cue.lineAnchor`
|
text lines to grid of viewport lines. Only consider `Cue.lineAnchor`
|
||||||
when `Cue.lineType=LINE_TYPE_FRACTION`.
|
when `Cue.lineType=LINE_TYPE_FRACTION`.
|
||||||
* WebVTT
|
* WebVTT:
|
||||||
* Add support for default
|
* Add support for default
|
||||||
[text](https://www.w3.org/TR/webvtt1/#default-text-color) and
|
[text](https://www.w3.org/TR/webvtt1/#default-text-color) and
|
||||||
[background](https://www.w3.org/TR/webvtt1/#default-text-background)
|
[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 `ruby-position` CSS property.
|
||||||
* Parse the `text-combine-upright` CSS property (i.e., tate-chu-yoko).
|
* Parse the `text-combine-upright` CSS property (i.e., tate-chu-yoko).
|
||||||
* Parse the `<ruby>` and `<rt>` tags.
|
* Parse the `<ruby>` and `<rt>` tags.
|
||||||
* TTML
|
* TTML:
|
||||||
* Parse the `tts:combineText` property (i.e., tate-chu-yoko).
|
* Parse the `tts:combineText` property (i.e., tate-chu-yoko).
|
||||||
* Parse t`tts:ruby` and `tts:rubyPosition` properties.
|
* Parse t`tts:ruby` and `tts:rubyPosition` properties.
|
||||||
* CEA-608
|
* CEA-608:
|
||||||
* Implement timing-out of stuck captions, as permitted by
|
* 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
|
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)).
|
seconds ([#7181](https://github.com/google/ExoPlayer/issues/7181)).
|
||||||
|
@ -17,7 +17,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
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.novoda:bintray-release:0.9.1'
|
||||||
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1'
|
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1'
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
// ExoPlayer version and version code.
|
// ExoPlayer version and version code.
|
||||||
releaseVersion = '2.12.0'
|
releaseVersion = '2.12.1'
|
||||||
releaseVersionCode = 2012000
|
releaseVersionCode = 2012001
|
||||||
minSdkVersion = 16
|
minSdkVersion = 16
|
||||||
appTargetSdkVersion = 29
|
appTargetSdkVersion = 29
|
||||||
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
|
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
|
||||||
|
@ -29,6 +29,7 @@ include modulePrefix + 'library-extractor'
|
|||||||
include modulePrefix + 'library-hls'
|
include modulePrefix + 'library-hls'
|
||||||
include modulePrefix + 'library-smoothstreaming'
|
include modulePrefix + 'library-smoothstreaming'
|
||||||
include modulePrefix + 'library-ui'
|
include modulePrefix + 'library-ui'
|
||||||
|
include modulePrefix + 'robolectricutils'
|
||||||
include modulePrefix + 'testutils'
|
include modulePrefix + 'testutils'
|
||||||
include modulePrefix + 'testdata'
|
include modulePrefix + 'testdata'
|
||||||
include modulePrefix + 'extension-av1'
|
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-hls').projectDir = new File(rootDir, 'library/hls')
|
||||||
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
|
project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming')
|
||||||
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
|
project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui')
|
||||||
|
project(modulePrefix + 'robolectricutils').projectDir = new File(rootDir, 'robolectricutils')
|
||||||
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
|
project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils')
|
||||||
project(modulePrefix + 'testdata').projectDir = new File(rootDir, 'testdata')
|
project(modulePrefix + 'testdata').projectDir = new File(rootDir, 'testdata')
|
||||||
project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1')
|
project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1')
|
||||||
|
@ -70,13 +70,6 @@ dependencies {
|
|||||||
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||||
implementation 'com.google.android.material:material:1.2.1'
|
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-core')
|
||||||
implementation project(modulePrefix + 'library-dash')
|
implementation project(modulePrefix + 'library-dash')
|
||||||
implementation project(modulePrefix + 'library-hls')
|
implementation project(modulePrefix + 'library-hls')
|
||||||
|
@ -527,6 +527,20 @@
|
|||||||
{
|
{
|
||||||
"name": "MPEG-4 Timed Text (tx3g, mov_text)",
|
"name": "MPEG-4 Timed Text (tx3g, mov_text)",
|
||||||
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -102,7 +102,7 @@ public class PlayerActivity extends AppCompatActivity
|
|||||||
private int startWindow;
|
private int startWindow;
|
||||||
private long startPosition;
|
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 AdsLoader adsLoader;
|
||||||
private Uri loadedAdTagUri;
|
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.
|
// The ads loader is reused for multiple playbacks, so that ad playback can resume.
|
||||||
if (adsLoader == null) {
|
if (adsLoader == null) {
|
||||||
adsLoader = new ImaAdsLoader(/* context= */ PlayerActivity.this, adTagUri);
|
adsLoader = new ImaAdsLoader.Builder(/* context= */ this).build();
|
||||||
}
|
}
|
||||||
adsLoader.setPlayer(player);
|
adsLoader.setPlayer(player);
|
||||||
return adsLoader;
|
return adsLoader;
|
||||||
|
@ -354,7 +354,12 @@ public final class TrackSelectionDialog extends DialogFragment {
|
|||||||
trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides);
|
trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides);
|
||||||
trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);
|
trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);
|
||||||
trackSelectionView.init(
|
trackSelectionView.init(
|
||||||
mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this);
|
mappedTrackInfo,
|
||||||
|
rendererIndex,
|
||||||
|
isDisabled,
|
||||||
|
overrides,
|
||||||
|
/* trackFormatComparator= */ null,
|
||||||
|
/* listener= */ this);
|
||||||
return rootView;
|
return rootView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
@Override
|
||||||
public void setMediaItems(
|
public void setMediaItems(
|
||||||
List<MediaItem> mediaItems, int startWindowIndex, long startPositionMs) {
|
List<MediaItem> mediaItems, int startWindowIndex, long startPositionMs) {
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
package com.google.android.exoplayer2.ext.cast;
|
package com.google.android.exoplayer2.ext.cast;
|
||||||
|
|
||||||
import android.content.Context;
|
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.CastOptions;
|
||||||
import com.google.android.gms.cast.framework.OptionsProvider;
|
import com.google.android.gms.cast.framework.OptionsProvider;
|
||||||
import com.google.android.gms.cast.framework.SessionProvider;
|
import com.google.android.gms.cast.framework.SessionProvider;
|
||||||
@ -29,24 +28,12 @@ import java.util.List;
|
|||||||
public final class DefaultCastOptionsProvider implements OptionsProvider {
|
public final class DefaultCastOptionsProvider implements OptionsProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App id of the Default Media Receiver app. Apps that do not require DRM support may use this
|
* App id that points to the Default Media Receiver app with basic DRM support.
|
||||||
* receiver receiver app ID.
|
|
||||||
*
|
*
|
||||||
* <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";
|
public static final String APP_ID_DEFAULT_RECEIVER_WITH_DRM = "A12D4273";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -17,13 +17,6 @@ dependencies {
|
|||||||
api "com.google.android.gms:play-services-cronet:17.0.0"
|
api "com.google.android.gms:play-services-cronet:17.0.0"
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
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-qual:' + checkerframeworkVersion
|
||||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
testImplementation project(modulePrefix + 'library')
|
testImplementation project(modulePrefix + 'library')
|
||||||
|
@ -25,39 +25,18 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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 project(modulePrefix + 'library-core')
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
|
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
|
||||||
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-qual:' + checkerframeworkVersion
|
||||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
androidTestImplementation project(modulePrefix + 'testutils')
|
androidTestImplementation project(modulePrefix + 'testutils')
|
||||||
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||||
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
|
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
|
||||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
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
|
androidTestCompileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
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
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.checkArgument;
|
||||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
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.Assertions.checkState;
|
||||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
|
||||||
import static java.lang.Math.max;
|
import static java.lang.Math.max;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -33,7 +32,6 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
|
||||||
import com.google.ads.interactivemedia.v3.api.AdError;
|
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;
|
||||||
import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
|
import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
|
||||||
import com.google.ads.interactivemedia.v3.api.AdEvent;
|
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.ExoPlayerLibraryInfo;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
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.source.ads.AdsMediaSource.AdLoadException;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
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
|
* #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
|
||||||
* {@link #release()}.
|
* {@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
|
* <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
|
* 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
|
* be registered via the {@link AdViewProvider} passed to the {@link AdsMediaSource}. See the <a
|
||||||
* 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
|
||||||
* href="https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/omsdk">
|
* SDK Open Measurement documentation</a> for more information.
|
||||||
* IMA SDK Open Measurement documentation</a> for more information.
|
|
||||||
*/
|
*/
|
||||||
public final class ImaAdsLoader
|
public final class ImaAdsLoader
|
||||||
implements Player.EventListener, com.google.android.exoplayer2.source.ads.AdsLoader {
|
implements Player.EventListener, com.google.android.exoplayer2.source.ads.AdsLoader {
|
||||||
@ -126,6 +134,8 @@ public final class ImaAdsLoader
|
|||||||
@Nullable private ImaSdkSettings imaSdkSettings;
|
@Nullable private ImaSdkSettings imaSdkSettings;
|
||||||
@Nullable private AdErrorListener adErrorListener;
|
@Nullable private AdErrorListener adErrorListener;
|
||||||
@Nullable private AdEventListener adEventListener;
|
@Nullable private AdEventListener adEventListener;
|
||||||
|
@Nullable private VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback;
|
||||||
|
@Nullable private List<String> adMediaMimeTypes;
|
||||||
@Nullable private Set<UiElement> adUiElements;
|
@Nullable private Set<UiElement> adUiElements;
|
||||||
@Nullable private Collection<CompanionAdSlot> companionAdSlots;
|
@Nullable private Collection<CompanionAdSlot> companionAdSlots;
|
||||||
private long adPreloadTimeoutMs;
|
private long adPreloadTimeoutMs;
|
||||||
@ -134,7 +144,8 @@ public final class ImaAdsLoader
|
|||||||
private int mediaBitrate;
|
private int mediaBitrate;
|
||||||
private boolean focusSkipButtonWhenAvailable;
|
private boolean focusSkipButtonWhenAvailable;
|
||||||
private boolean playAdBeforeStartPosition;
|
private boolean playAdBeforeStartPosition;
|
||||||
private ImaFactory imaFactory;
|
private boolean debugModeEnabled;
|
||||||
|
private ImaUtil.ImaFactory imaFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new builder for {@link ImaAdsLoader}.
|
* Creates a new builder for {@link ImaAdsLoader}.
|
||||||
@ -142,7 +153,7 @@ public final class ImaAdsLoader
|
|||||||
* @param context The context;
|
* @param context The context;
|
||||||
*/
|
*/
|
||||||
public Builder(Context context) {
|
public Builder(Context context) {
|
||||||
this.context = checkNotNull(context);
|
this.context = checkNotNull(context).getApplicationContext();
|
||||||
adPreloadTimeoutMs = DEFAULT_AD_PRELOAD_TIMEOUT_MS;
|
adPreloadTimeoutMs = DEFAULT_AD_PRELOAD_TIMEOUT_MS;
|
||||||
vastLoadTimeoutMs = TIMEOUT_UNSET;
|
vastLoadTimeoutMs = TIMEOUT_UNSET;
|
||||||
mediaLoadTimeoutMs = TIMEOUT_UNSET;
|
mediaLoadTimeoutMs = TIMEOUT_UNSET;
|
||||||
@ -191,6 +202,22 @@ public final class ImaAdsLoader
|
|||||||
return this;
|
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.
|
* Sets the ad UI elements to be rendered by the IMA SDK.
|
||||||
*
|
*
|
||||||
@ -215,6 +242,23 @@ public final class ImaAdsLoader
|
|||||||
return this;
|
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
|
* 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
|
* 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;
|
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
|
@VisibleForTesting
|
||||||
/* package */ Builder setImaFactory(ImaFactory imaFactory) {
|
/* package */ Builder setImaFactory(ImaUtil.ImaFactory imaFactory) {
|
||||||
this.imaFactory = checkNotNull(imaFactory);
|
this.imaFactory = checkNotNull(imaFactory);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -315,24 +374,18 @@ public final class ImaAdsLoader
|
|||||||
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
|
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
|
||||||
* information on compatible ad tags.
|
* information on compatible ad tags.
|
||||||
* @return The new {@link ImaAdsLoader}.
|
* @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) {
|
public ImaAdsLoader buildForAdTag(Uri adTagUri) {
|
||||||
return new ImaAdsLoader(
|
return new ImaAdsLoader(
|
||||||
context,
|
context,
|
||||||
adTagUri,
|
getConfiguration(),
|
||||||
imaSdkSettings,
|
imaFactory,
|
||||||
/* adsResponse= */ null,
|
/* adTagUri= */ adTagUri,
|
||||||
adPreloadTimeoutMs,
|
/* adsResponse= */ null);
|
||||||
vastLoadTimeoutMs,
|
|
||||||
mediaLoadTimeoutMs,
|
|
||||||
mediaBitrate,
|
|
||||||
focusSkipButtonWhenAvailable,
|
|
||||||
playAdBeforeStartPosition,
|
|
||||||
adUiElements,
|
|
||||||
companionAdSlots,
|
|
||||||
adErrorListener,
|
|
||||||
adEventListener,
|
|
||||||
imaFactory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -341,28 +394,44 @@ public final class ImaAdsLoader
|
|||||||
* @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of
|
* @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of
|
||||||
* making a request via an ad tag URL.
|
* making a request via an ad tag URL.
|
||||||
* @return The new {@link ImaAdsLoader}.
|
* @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) {
|
public ImaAdsLoader buildForAdsResponse(String adsResponse) {
|
||||||
return new ImaAdsLoader(
|
return new ImaAdsLoader(
|
||||||
context,
|
context, getConfiguration(), imaFactory, /* adTagUri= */ null, adsResponse);
|
||||||
/* adTagUri= */ null,
|
}
|
||||||
imaSdkSettings,
|
|
||||||
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,
|
adPreloadTimeoutMs,
|
||||||
vastLoadTimeoutMs,
|
vastLoadTimeoutMs,
|
||||||
mediaLoadTimeoutMs,
|
mediaLoadTimeoutMs,
|
||||||
mediaBitrate,
|
|
||||||
focusSkipButtonWhenAvailable,
|
focusSkipButtonWhenAvailable,
|
||||||
playAdBeforeStartPosition,
|
playAdBeforeStartPosition,
|
||||||
|
mediaBitrate,
|
||||||
|
adMediaMimeTypes,
|
||||||
adUiElements,
|
adUiElements,
|
||||||
companionAdSlots,
|
companionAdSlots,
|
||||||
adErrorListener,
|
adErrorListener,
|
||||||
adEventListener,
|
adEventListener,
|
||||||
imaFactory);
|
videoAdPlayerCallback,
|
||||||
|
imaSdkSettings,
|
||||||
|
debugModeEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final boolean DEBUG = false;
|
|
||||||
private static final String TAG = "ImaAdsLoader";
|
private static final String TAG = "ImaAdsLoader";
|
||||||
|
|
||||||
private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima";
|
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 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 Context context;
|
||||||
|
private final ImaUtil.ImaFactory imaFactory;
|
||||||
@Nullable private final Uri adTagUri;
|
@Nullable private final Uri adTagUri;
|
||||||
@Nullable private final String adsResponse;
|
@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 ImaSdkSettings imaSdkSettings;
|
||||||
private final Timeline.Period period;
|
private final Timeline.Period period;
|
||||||
private final Handler handler;
|
private final Handler handler;
|
||||||
@ -443,6 +505,7 @@ public final class ImaAdsLoader
|
|||||||
private List<String> supportedMimeTypes;
|
private List<String> supportedMimeTypes;
|
||||||
@Nullable private EventListener eventListener;
|
@Nullable private EventListener eventListener;
|
||||||
@Nullable private Player player;
|
@Nullable private Player player;
|
||||||
|
private DataSpec adTagDataSpec;
|
||||||
private VideoProgressUpdate lastContentProgress;
|
private VideoProgressUpdate lastContentProgress;
|
||||||
private VideoProgressUpdate lastAdProgress;
|
private VideoProgressUpdate lastAdProgress;
|
||||||
private int lastVolumePercent;
|
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
|
* @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
|
* https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
|
||||||
* more information.
|
* 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) {
|
public ImaAdsLoader(Context context, Uri adTagUri) {
|
||||||
this(
|
this(
|
||||||
context,
|
context,
|
||||||
|
new Builder(context).getConfiguration(),
|
||||||
|
new DefaultImaFactory(),
|
||||||
adTagUri,
|
adTagUri,
|
||||||
/* imaSdkSettings= */ null,
|
/* adsResponse= */ 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings({"nullness:argument.type.incompatible", "methodref.receiver.bound.invalid"})
|
@SuppressWarnings({"nullness:argument.type.incompatible", "methodref.receiver.bound.invalid"})
|
||||||
private ImaAdsLoader(
|
private ImaAdsLoader(
|
||||||
Context context,
|
Context context,
|
||||||
|
ImaUtil.Configuration configuration,
|
||||||
|
ImaUtil.ImaFactory imaFactory,
|
||||||
@Nullable Uri adTagUri,
|
@Nullable Uri adTagUri,
|
||||||
@Nullable ImaSdkSettings imaSdkSettings,
|
@Nullable String adsResponse) {
|
||||||
@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);
|
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.imaFactory = imaFactory;
|
||||||
this.adTagUri = adTagUri;
|
this.adTagUri = adTagUri;
|
||||||
this.adsResponse = adsResponse;
|
this.adsResponse = adsResponse;
|
||||||
this.adPreloadTimeoutMs = adPreloadTimeoutMs;
|
@Nullable ImaSdkSettings imaSdkSettings = configuration.imaSdkSettings;
|
||||||
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;
|
|
||||||
if (imaSdkSettings == null) {
|
if (imaSdkSettings == null) {
|
||||||
imaSdkSettings = imaFactory.createImaSdkSettings();
|
imaSdkSettings = imaFactory.createImaSdkSettings();
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
imaSdkSettings.setDebugMode(true);
|
imaSdkSettings.setDebugMode(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -583,9 +621,13 @@ public final class ImaAdsLoader
|
|||||||
handler = Util.createHandler(getImaLooper(), /* callback= */ null);
|
handler = Util.createHandler(getImaLooper(), /* callback= */ null);
|
||||||
componentListener = new ComponentListener();
|
componentListener = new ComponentListener();
|
||||||
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
|
adCallbacks = new ArrayList<>(/* initialCapacity= */ 1);
|
||||||
|
if (configuration.applicationVideoAdPlayerCallback != null) {
|
||||||
|
adCallbacks.add(configuration.applicationVideoAdPlayerCallback);
|
||||||
|
}
|
||||||
updateAdProgressRunnable = this::updateAdProgress;
|
updateAdProgressRunnable = this::updateAdProgress;
|
||||||
adInfoByAdMediaInfo = HashBiMap.create();
|
adInfoByAdMediaInfo = HashBiMap.create();
|
||||||
supportedMimeTypes = Collections.emptyList();
|
supportedMimeTypes = Collections.emptyList();
|
||||||
|
adTagDataSpec = EMPTY_AD_TAG_DATA_SPEC;
|
||||||
lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
|
lastContentProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
|
||||||
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
|
lastAdProgress = VideoProgressUpdate.VIDEO_TIME_NOT_READY;
|
||||||
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
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
|
* @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI, or {@code
|
||||||
* null} if playing audio-only ads.
|
* 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) {
|
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) {
|
if (hasAdPlaybackState || adsManager != null || pendingAdRequestContext != null) {
|
||||||
// Ads have already been requested.
|
// Ads have already been requested.
|
||||||
return;
|
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) {
|
if (adViewGroup != null) {
|
||||||
adDisplayContainer =
|
adDisplayContainer =
|
||||||
imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener);
|
imaFactory.createAdDisplayContainer(adViewGroup, /* player= */ componentListener);
|
||||||
@ -644,27 +736,16 @@ public final class ImaAdsLoader
|
|||||||
adDisplayContainer =
|
adDisplayContainer =
|
||||||
imaFactory.createAudioAdDisplayContainer(context, /* player= */ componentListener);
|
imaFactory.createAudioAdDisplayContainer(context, /* player= */ componentListener);
|
||||||
}
|
}
|
||||||
if (companionAdSlots != null) {
|
if (configuration.companionAdSlots != null) {
|
||||||
adDisplayContainer.setCompanionSlots(companionAdSlots);
|
adDisplayContainer.setCompanionSlots(configuration.companionAdSlots);
|
||||||
}
|
}
|
||||||
|
|
||||||
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
|
adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer);
|
||||||
adsLoader.addAdErrorListener(componentListener);
|
adsLoader.addAdErrorListener(componentListener);
|
||||||
if (adErrorListener != null) {
|
if (configuration.applicationAdErrorListener != null) {
|
||||||
adsLoader.addAdErrorListener(adErrorListener);
|
adsLoader.addAdErrorListener(configuration.applicationAdErrorListener);
|
||||||
}
|
}
|
||||||
adsLoader.addAdsLoadedListener(componentListener);
|
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);
|
adsLoader.requestAds(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -713,6 +794,11 @@ public final class ImaAdsLoader
|
|||||||
this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes);
|
this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAdTagDataSpec(DataSpec adTagDataSpec) {
|
||||||
|
this.adTagDataSpec = adTagDataSpec;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(EventListener eventListener, AdViewProvider adViewProvider) {
|
public void start(EventListener eventListener, AdViewProvider adViewProvider) {
|
||||||
checkState(
|
checkState(
|
||||||
@ -735,18 +821,18 @@ public final class ImaAdsLoader
|
|||||||
adsManager.resume();
|
adsManager.resume();
|
||||||
}
|
}
|
||||||
} else if (adsManager != null) {
|
} else if (adsManager != null) {
|
||||||
adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints());
|
adPlaybackState = ImaUtil.getInitialAdPlaybackStateForCuePoints(adsManager.getAdCuePoints());
|
||||||
updateAdPlaybackState();
|
updateAdPlaybackState();
|
||||||
} else {
|
} else {
|
||||||
// Ads haven't loaded yet, so request them.
|
// Ads haven't loaded yet, so request them.
|
||||||
requestAds(adViewProvider.getAdViewGroup());
|
requestAds(adTagDataSpec, adViewProvider.getAdViewGroup());
|
||||||
}
|
}
|
||||||
if (adDisplayContainer != null) {
|
if (adDisplayContainer != null) {
|
||||||
for (OverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) {
|
for (OverlayInfo overlayInfo : adViewProvider.getAdOverlayInfos()) {
|
||||||
adDisplayContainer.registerFriendlyObstruction(
|
adDisplayContainer.registerFriendlyObstruction(
|
||||||
imaFactory.createFriendlyObstruction(
|
imaFactory.createFriendlyObstruction(
|
||||||
overlayInfo.view,
|
overlayInfo.view,
|
||||||
getFriendlyObstructionPurpose(overlayInfo.purpose),
|
ImaUtil.getFriendlyObstructionPurpose(overlayInfo.purpose),
|
||||||
overlayInfo.reasonDetail));
|
overlayInfo.reasonDetail));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -782,8 +868,8 @@ public final class ImaAdsLoader
|
|||||||
if (adsLoader != null) {
|
if (adsLoader != null) {
|
||||||
adsLoader.removeAdsLoadedListener(componentListener);
|
adsLoader.removeAdsLoadedListener(componentListener);
|
||||||
adsLoader.removeAdErrorListener(componentListener);
|
adsLoader.removeAdErrorListener(componentListener);
|
||||||
if (adErrorListener != null) {
|
if (configuration.applicationAdErrorListener != null) {
|
||||||
adsLoader.removeAdErrorListener(adErrorListener);
|
adsLoader.removeAdErrorListener(configuration.applicationAdErrorListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
imaPausedContent = false;
|
imaPausedContent = false;
|
||||||
@ -800,7 +886,7 @@ public final class ImaAdsLoader
|
|||||||
@Override
|
@Override
|
||||||
public void handlePrepareComplete(int adGroupIndex, int adIndexInAdGroup) {
|
public void handlePrepareComplete(int adGroupIndex, int adIndexInAdGroup) {
|
||||||
AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
|
AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "Prepared ad " + adInfo);
|
Log.d(TAG, "Prepared ad " + adInfo);
|
||||||
}
|
}
|
||||||
@Nullable AdMediaInfo adMediaInfo = adInfoByAdMediaInfo.inverse().get(adInfo);
|
@Nullable AdMediaInfo adMediaInfo = adInfoByAdMediaInfo.inverse().get(adInfo);
|
||||||
@ -850,7 +936,7 @@ public final class ImaAdsLoader
|
|||||||
} else {
|
} else {
|
||||||
adsManager.init(adsRenderingSettings);
|
adsManager.init(adsRenderingSettings);
|
||||||
adsManager.start();
|
adsManager.start();
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings);
|
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 adGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
|
||||||
long contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
|
long contentPositionMs = getContentPeriodPositionMs(player, timeline, period);
|
||||||
long timeUntilAdMs = adGroupTimeMs - contentPositionMs;
|
long timeUntilAdMs = adGroupTimeMs - contentPositionMs;
|
||||||
if (timeUntilAdMs < adPreloadTimeoutMs) {
|
if (timeUntilAdMs < configuration.adPreloadTimeoutMs) {
|
||||||
waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
waitingForPreloadElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
||||||
}
|
}
|
||||||
} else if (playbackState == Player.STATE_READY) {
|
} else if (playbackState == Player.STATE_READY) {
|
||||||
@ -936,16 +1022,20 @@ public final class ImaAdsLoader
|
|||||||
private AdsRenderingSettings setupAdsRendering() {
|
private AdsRenderingSettings setupAdsRendering() {
|
||||||
AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();
|
AdsRenderingSettings adsRenderingSettings = imaFactory.createAdsRenderingSettings();
|
||||||
adsRenderingSettings.setEnablePreloading(true);
|
adsRenderingSettings.setEnablePreloading(true);
|
||||||
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
|
adsRenderingSettings.setMimeTypes(
|
||||||
if (mediaLoadTimeoutMs != TIMEOUT_UNSET) {
|
configuration.adMediaMimeTypes != null
|
||||||
adsRenderingSettings.setLoadVideoTimeout(mediaLoadTimeoutMs);
|
? configuration.adMediaMimeTypes
|
||||||
|
: supportedMimeTypes);
|
||||||
|
if (configuration.mediaLoadTimeoutMs != TIMEOUT_UNSET) {
|
||||||
|
adsRenderingSettings.setLoadVideoTimeout(configuration.mediaLoadTimeoutMs);
|
||||||
}
|
}
|
||||||
if (mediaBitrate != BITRATE_UNSET) {
|
if (configuration.mediaBitrate != BITRATE_UNSET) {
|
||||||
adsRenderingSettings.setBitrateKbps(mediaBitrate / 1000);
|
adsRenderingSettings.setBitrateKbps(configuration.mediaBitrate / 1000);
|
||||||
}
|
}
|
||||||
adsRenderingSettings.setFocusSkipButtonWhenAvailable(focusSkipButtonWhenAvailable);
|
adsRenderingSettings.setFocusSkipButtonWhenAvailable(
|
||||||
if (adUiElements != null) {
|
configuration.focusSkipButtonWhenAvailable);
|
||||||
adsRenderingSettings.setUiElements(adUiElements);
|
if (configuration.adUiElements != null) {
|
||||||
|
adsRenderingSettings.setUiElements(configuration.adUiElements);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip ads based on the start position as required.
|
// Skip ads based on the start position as required.
|
||||||
@ -956,7 +1046,7 @@ public final class ImaAdsLoader
|
|||||||
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
|
C.msToUs(contentPositionMs), C.msToUs(contentDurationMs));
|
||||||
if (adGroupForPositionIndex != C.INDEX_UNSET) {
|
if (adGroupForPositionIndex != C.INDEX_UNSET) {
|
||||||
boolean playAdWhenStartingPlayback =
|
boolean playAdWhenStartingPlayback =
|
||||||
playAdBeforeStartPosition
|
configuration.playAdBeforeStartPosition
|
||||||
|| adGroupTimesUs[adGroupForPositionIndex] == C.msToUs(contentPositionMs);
|
|| adGroupTimesUs[adGroupForPositionIndex] == C.msToUs(contentPositionMs);
|
||||||
if (!playAdWhenStartingPlayback) {
|
if (!playAdWhenStartingPlayback) {
|
||||||
adGroupForPositionIndex++;
|
adGroupForPositionIndex++;
|
||||||
@ -1069,7 +1159,7 @@ public final class ImaAdsLoader
|
|||||||
switch (adEvent.getType()) {
|
switch (adEvent.getType()) {
|
||||||
case AD_BREAK_FETCH_ERROR:
|
case AD_BREAK_FETCH_ERROR:
|
||||||
String adGroupTimeSecondsString = checkNotNull(adEvent.getAdData().get("adBreakTime"));
|
String adGroupTimeSecondsString = checkNotNull(adEvent.getAdData().get("adBreakTime"));
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds");
|
Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds");
|
||||||
}
|
}
|
||||||
double adGroupTimeSeconds = Double.parseDouble(adGroupTimeSecondsString);
|
double adGroupTimeSeconds = Double.parseDouble(adGroupTimeSecondsString);
|
||||||
@ -1077,7 +1167,7 @@ public final class ImaAdsLoader
|
|||||||
adGroupTimeSeconds == -1.0
|
adGroupTimeSeconds == -1.0
|
||||||
? adPlaybackState.adGroupCount - 1
|
? adPlaybackState.adGroupCount - 1
|
||||||
: getAdGroupIndexForCuePointTimeSeconds(adGroupTimeSeconds);
|
: getAdGroupIndexForCuePointTimeSeconds(adGroupTimeSeconds);
|
||||||
handleAdGroupFetchError(adGroupIndex);
|
markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex);
|
||||||
break;
|
break;
|
||||||
case CONTENT_PAUSE_REQUESTED:
|
case CONTENT_PAUSE_REQUESTED:
|
||||||
// After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
|
// 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);
|
adCallbacks.get(i).onEnded(adMediaInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlaybackStateChanged");
|
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlaybackStateChanged");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1196,7 +1286,7 @@ public final class ImaAdsLoader
|
|||||||
adCallbacks.get(i).onEnded(adMediaInfo);
|
adCallbacks.get(i).onEnded(adMediaInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
|
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1218,7 +1308,7 @@ public final class ImaAdsLoader
|
|||||||
private void loadAdInternal(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) {
|
private void loadAdInternal(AdMediaInfo adMediaInfo, AdPodInfo adPodInfo) {
|
||||||
if (adsManager == null) {
|
if (adsManager == null) {
|
||||||
// Drop events after release.
|
// Drop events after release.
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"loadAd after release " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo);
|
"loadAd after release " + getAdMediaInfoString(adMediaInfo) + ", ad pod " + adPodInfo);
|
||||||
@ -1230,7 +1320,7 @@ public final class ImaAdsLoader
|
|||||||
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
|
int adIndexInAdGroup = adPodInfo.getAdPosition() - 1;
|
||||||
AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
|
AdInfo adInfo = new AdInfo(adGroupIndex, adIndexInAdGroup);
|
||||||
adInfoByAdMediaInfo.put(adMediaInfo, adInfo);
|
adInfoByAdMediaInfo.put(adMediaInfo, adInfo);
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo));
|
Log.d(TAG, "loadAd " + getAdMediaInfoString(adMediaInfo));
|
||||||
}
|
}
|
||||||
if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) {
|
if (adPlaybackState.isAdInErrorState(adGroupIndex, adIndexInAdGroup)) {
|
||||||
@ -1261,7 +1351,7 @@ public final class ImaAdsLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void playAdInternal(AdMediaInfo adMediaInfo) {
|
private void playAdInternal(AdMediaInfo adMediaInfo) {
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo));
|
Log.d(TAG, "playAd " + getAdMediaInfoString(adMediaInfo));
|
||||||
}
|
}
|
||||||
if (adsManager == null) {
|
if (adsManager == null) {
|
||||||
@ -1305,7 +1395,7 @@ public final class ImaAdsLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void pauseAdInternal(AdMediaInfo adMediaInfo) {
|
private void pauseAdInternal(AdMediaInfo adMediaInfo) {
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo));
|
Log.d(TAG, "pauseAd " + getAdMediaInfoString(adMediaInfo));
|
||||||
}
|
}
|
||||||
if (adsManager == null) {
|
if (adsManager == null) {
|
||||||
@ -1325,7 +1415,7 @@ public final class ImaAdsLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void stopAdInternal(AdMediaInfo adMediaInfo) {
|
private void stopAdInternal(AdMediaInfo adMediaInfo) {
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo));
|
Log.d(TAG, "stopAd " + getAdMediaInfoString(adMediaInfo));
|
||||||
}
|
}
|
||||||
if (adsManager == null) {
|
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) {
|
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();
|
int adGroupIndex = getLoadingAdGroupIndex();
|
||||||
if (adGroupIndex == C.INDEX_UNSET) {
|
if (adGroupIndex == C.INDEX_UNSET) {
|
||||||
Log.w(TAG, "Unable to determine ad group index for ad group load error", error);
|
Log.w(TAG, "Unable to determine ad group index for ad group load error", error);
|
||||||
return;
|
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];
|
AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
|
||||||
if (adGroup.count == C.LENGTH_UNSET) {
|
if (adGroup.count == C.LENGTH_UNSET) {
|
||||||
adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, max(1, adGroup.states.length));
|
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++) {
|
for (int i = 0; i < adGroup.count; i++) {
|
||||||
if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
|
if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex);
|
Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex);
|
||||||
}
|
}
|
||||||
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i);
|
adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateAdPlaybackState();
|
updateAdPlaybackState();
|
||||||
if (pendingAdLoadError == null) {
|
// Clear any pending content position that triggered attempting to load the ad group.
|
||||||
pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex);
|
|
||||||
}
|
|
||||||
pendingContentPositionMs = C.TIME_UNSET;
|
pendingContentPositionMs = C.TIME_UNSET;
|
||||||
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
|
private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
|
TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
|
||||||
}
|
}
|
||||||
@ -1467,7 +1540,7 @@ public final class ImaAdsLoader
|
|||||||
adCallbacks.get(i).onContentComplete();
|
adCallbacks.get(i).onContentComplete();
|
||||||
}
|
}
|
||||||
sentContentComplete = true;
|
sentContentComplete = true;
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "adsLoader.contentComplete");
|
Log.d(TAG, "adsLoader.contentComplete");
|
||||||
}
|
}
|
||||||
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
|
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
|
||||||
@ -1487,7 +1560,7 @@ public final class ImaAdsLoader
|
|||||||
|
|
||||||
private void maybeNotifyPendingAdLoadError() {
|
private void maybeNotifyPendingAdLoadError() {
|
||||||
if (pendingAdLoadError != null && eventListener != null) {
|
if (pendingAdLoadError != null && eventListener != null) {
|
||||||
eventListener.onAdLoadError(pendingAdLoadError, getAdsDataSpec(adTagUri));
|
eventListener.onAdLoadError(pendingAdLoadError, adTagDataSpec);
|
||||||
pendingAdLoadError = null;
|
pendingAdLoadError = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1502,8 +1575,7 @@ public final class ImaAdsLoader
|
|||||||
updateAdPlaybackState();
|
updateAdPlaybackState();
|
||||||
if (eventListener != null) {
|
if (eventListener != null) {
|
||||||
eventListener.onAdLoadError(
|
eventListener.onAdLoadError(
|
||||||
AdLoadException.createForUnexpected(new RuntimeException(message, cause)),
|
AdLoadException.createForUnexpected(new RuntimeException(message, cause)), adTagDataSpec);
|
||||||
getAdsDataSpec(adTagUri));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1522,8 +1594,10 @@ public final class ImaAdsLoader
|
|||||||
* no such ad group.
|
* no such ad group.
|
||||||
*/
|
*/
|
||||||
private int getLoadingAdGroupIndex() {
|
private int getLoadingAdGroupIndex() {
|
||||||
long playerPositionUs =
|
if (player == null) {
|
||||||
C.msToUs(getContentPeriodPositionMs(checkNotNull(player), timeline, period));
|
return C.INDEX_UNSET;
|
||||||
|
}
|
||||||
|
long playerPositionUs = C.msToUs(getContentPeriodPositionMs(player, timeline, period));
|
||||||
int adGroupIndex =
|
int adGroupIndex =
|
||||||
adPlaybackState.getAdGroupIndexForPositionUs(playerPositionUs, C.msToUs(contentDurationMs));
|
adPlaybackState.getAdGroupIndexForPositionUs(playerPositionUs, C.msToUs(contentDurationMs));
|
||||||
if (adGroupIndex == C.INDEX_UNSET) {
|
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
|
// 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
|
// 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).
|
// 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++) {
|
for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) {
|
||||||
long adGroupTimeUs = adPlaybackState.adGroupTimesUs[adGroupIndex];
|
long adGroupTimeUs = adPlaybackState.adGroupTimesUs[adGroupIndex];
|
||||||
if (adGroupTimeUs != C.TIME_END_OF_SOURCE
|
if (adGroupTimeUs != C.TIME_END_OF_SOURCE
|
||||||
@ -1554,25 +1629,6 @@ public final class ImaAdsLoader
|
|||||||
return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]";
|
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(
|
private static long getContentPeriodPositionMs(
|
||||||
Player player, Timeline timeline, Timeline.Period period) {
|
Player player, Timeline timeline, Timeline.Period period) {
|
||||||
long contentWindowPositionMs = player.getContentPosition();
|
long contentWindowPositionMs = player.getContentPosition();
|
||||||
@ -1582,13 +1638,6 @@ public final class ImaAdsLoader
|
|||||||
: timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs());
|
: 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() {
|
private static Looper getImaLooper() {
|
||||||
// IMA SDK callbacks occur on the main thread. This method can be used to check that the player
|
// 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.
|
// 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() {
|
private void destroyAdsManager() {
|
||||||
if (adsManager != null) {
|
if (adsManager != null) {
|
||||||
adsManager.removeAdErrorListener(componentListener);
|
adsManager.removeAdErrorListener(componentListener);
|
||||||
if (adErrorListener != null) {
|
if (configuration.applicationAdErrorListener != null) {
|
||||||
adsManager.removeAdErrorListener(adErrorListener);
|
adsManager.removeAdErrorListener(configuration.applicationAdErrorListener);
|
||||||
}
|
}
|
||||||
adsManager.removeAdEventListener(componentListener);
|
adsManager.removeAdEventListener(componentListener);
|
||||||
if (adEventListener != null) {
|
if (configuration.applicationAdEventListener != null) {
|
||||||
adsManager.removeAdEventListener(adEventListener);
|
adsManager.removeAdEventListener(configuration.applicationAdEventListener);
|
||||||
}
|
}
|
||||||
adsManager.destroy();
|
adsManager.destroy();
|
||||||
adsManager = null;
|
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
|
private final class ComponentListener
|
||||||
implements AdsLoadedListener,
|
implements AdsLoadedListener,
|
||||||
ContentProgressProvider,
|
ContentProgressProvider,
|
||||||
@ -1673,17 +1690,18 @@ public final class ImaAdsLoader
|
|||||||
pendingAdRequestContext = null;
|
pendingAdRequestContext = null;
|
||||||
ImaAdsLoader.this.adsManager = adsManager;
|
ImaAdsLoader.this.adsManager = adsManager;
|
||||||
adsManager.addAdErrorListener(this);
|
adsManager.addAdErrorListener(this);
|
||||||
if (adErrorListener != null) {
|
if (configuration.applicationAdErrorListener != null) {
|
||||||
adsManager.addAdErrorListener(adErrorListener);
|
adsManager.addAdErrorListener(configuration.applicationAdErrorListener);
|
||||||
}
|
}
|
||||||
adsManager.addAdEventListener(this);
|
adsManager.addAdEventListener(this);
|
||||||
if (adEventListener != null) {
|
if (configuration.applicationAdEventListener != null) {
|
||||||
adsManager.addAdEventListener(adEventListener);
|
adsManager.addAdEventListener(configuration.applicationAdEventListener);
|
||||||
}
|
}
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
// If a player is attached already, start playback immediately.
|
// If a player is attached already, start playback immediately.
|
||||||
try {
|
try {
|
||||||
adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints());
|
adPlaybackState =
|
||||||
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(adsManager.getAdCuePoints());
|
||||||
hasAdPlaybackState = true;
|
hasAdPlaybackState = true;
|
||||||
updateAdPlaybackState();
|
updateAdPlaybackState();
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
@ -1697,7 +1715,7 @@ public final class ImaAdsLoader
|
|||||||
@Override
|
@Override
|
||||||
public VideoProgressUpdate getContentProgress() {
|
public VideoProgressUpdate getContentProgress() {
|
||||||
VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate();
|
VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate();
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) {
|
if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) {
|
||||||
Log.d(TAG, "Content progress: not ready");
|
Log.d(TAG, "Content progress: not ready");
|
||||||
} else {
|
} else {
|
||||||
@ -1729,7 +1747,7 @@ public final class ImaAdsLoader
|
|||||||
@Override
|
@Override
|
||||||
public void onAdEvent(AdEvent adEvent) {
|
public void onAdEvent(AdEvent adEvent) {
|
||||||
AdEventType adEventType = adEvent.getType();
|
AdEventType adEventType = adEvent.getType();
|
||||||
if (DEBUG && adEventType != AdEventType.AD_PROGRESS) {
|
if (configuration.debugModeEnabled && adEventType != AdEventType.AD_PROGRESS) {
|
||||||
Log.d(TAG, "onAdEvent: " + adEventType);
|
Log.d(TAG, "onAdEvent: " + adEventType);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -1744,7 +1762,7 @@ public final class ImaAdsLoader
|
|||||||
@Override
|
@Override
|
||||||
public void onAdError(AdErrorEvent adErrorEvent) {
|
public void onAdError(AdErrorEvent adErrorEvent) {
|
||||||
AdError error = adErrorEvent.getError();
|
AdError error = adErrorEvent.getError();
|
||||||
if (DEBUG) {
|
if (configuration.debugModeEnabled) {
|
||||||
Log.d(TAG, "onAdError", error);
|
Log.d(TAG, "onAdError", error);
|
||||||
}
|
}
|
||||||
if (adsManager == null) {
|
if (adsManager == null) {
|
||||||
@ -1753,7 +1771,7 @@ public final class ImaAdsLoader
|
|||||||
adPlaybackState = AdPlaybackState.NONE;
|
adPlaybackState = AdPlaybackState.NONE;
|
||||||
hasAdPlaybackState = true;
|
hasAdPlaybackState = true;
|
||||||
updateAdPlaybackState();
|
updateAdPlaybackState();
|
||||||
} else if (isAdGroupLoadError(error)) {
|
} else if (ImaUtil.isAdGroupLoadError(error)) {
|
||||||
try {
|
try {
|
||||||
handleAdGroupLoadError(error);
|
handleAdGroupLoadError(error);
|
||||||
} catch (RuntimeException e) {
|
} 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
|
@Override
|
||||||
public ImaSdkSettings createImaSdkSettings() {
|
public ImaSdkSettings createImaSdkSettings() {
|
||||||
return ImaSdkFactory.getInstance().createImaSdkSettings();
|
return ImaSdkFactory.getInstance().createImaSdkSettings();
|
||||||
|
@ -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() {}
|
||||||
|
}
|
@ -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.AdMediaInfo;
|
||||||
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
|
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.VideoAdPlayer;
|
||||||
|
import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.Timeline.Period;
|
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.MaskingMediaSource.PlaceholderTimeline;
|
||||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsLoader;
|
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;
|
||||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
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.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -96,8 +99,9 @@ public final class ImaAdsLoaderTest {
|
|||||||
/* isSeekable= */ true, /* isDynamic= */ false, CONTENT_DURATION_US));
|
/* isSeekable= */ true, /* isDynamic= */ false, CONTENT_DURATION_US));
|
||||||
private static final long CONTENT_PERIOD_DURATION_US =
|
private static final long CONTENT_PERIOD_DURATION_US =
|
||||||
CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs;
|
CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs;
|
||||||
private static final Uri TEST_URI = Uri.EMPTY;
|
private static final Uri TEST_URI = Uri.parse("https://www.google.com");
|
||||||
private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo(TEST_URI.toString());
|
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 long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND;
|
||||||
private static final ImmutableList<Float> PREROLL_CUE_POINTS_SECONDS = ImmutableList.of(0f);
|
private static final ImmutableList<Float> PREROLL_CUE_POINTS_SECONDS = ImmutableList.of(0f);
|
||||||
|
|
||||||
@ -284,7 +288,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
new AdPlaybackState(/* adGroupTimesUs...= */ 0)
|
new AdPlaybackState(/* adGroupTimesUs...= */ 0)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
.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}})
|
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
|
||||||
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
|
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
|
||||||
.withAdResumePositionUs(/* adResumePositionUs= */ 0));
|
.withAdResumePositionUs(/* adResumePositionUs= */ 0));
|
||||||
@ -311,6 +315,31 @@ public final class ImaAdsLoaderTest {
|
|||||||
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
|
.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
|
@Test
|
||||||
public void playback_withPostrollFetchError_marksAdAsInErrorState() {
|
public void playback_withPostrollFetchError_marksAdAsInErrorState() {
|
||||||
AdEvent mockPostrollFetchErrorAdEvent = mock(AdEvent.class);
|
AdEvent mockPostrollFetchErrorAdEvent = mock(AdEvent.class);
|
||||||
@ -352,7 +381,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
|
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,7 +405,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
|
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||||
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
|
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
|
||||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||||
@ -398,7 +427,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
|
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,7 +451,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||||
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
||||||
}
|
}
|
||||||
@ -447,7 +476,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||||
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
||||||
}
|
}
|
||||||
@ -474,7 +503,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
|
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -505,7 +534,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||||
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
||||||
}
|
}
|
||||||
@ -524,7 +553,8 @@ public final class ImaAdsLoaderTest {
|
|||||||
.setPlayAdBeforeStartPosition(false)
|
.setPlayAdBeforeStartPosition(false)
|
||||||
.setImaFactory(mockImaFactory)
|
.setImaFactory(mockImaFactory)
|
||||||
.setImaSdkSettings(mockImaSdkSettings)
|
.setImaSdkSettings(mockImaSdkSettings)
|
||||||
.buildForAdTag(TEST_URI));
|
.build(),
|
||||||
|
TEST_DATA_SPEC);
|
||||||
|
|
||||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000);
|
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000);
|
||||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||||
@ -537,7 +567,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withSkippedAdGroup(/* adGroupIndex= */ 0)
|
.withSkippedAdGroup(/* adGroupIndex= */ 0)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
||||||
}
|
}
|
||||||
@ -556,7 +586,8 @@ public final class ImaAdsLoaderTest {
|
|||||||
.setPlayAdBeforeStartPosition(false)
|
.setPlayAdBeforeStartPosition(false)
|
||||||
.setImaFactory(mockImaFactory)
|
.setImaFactory(mockImaFactory)
|
||||||
.setImaSdkSettings(mockImaSdkSettings)
|
.setImaSdkSettings(mockImaSdkSettings)
|
||||||
.buildForAdTag(TEST_URI));
|
.build(),
|
||||||
|
TEST_DATA_SPEC);
|
||||||
|
|
||||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs));
|
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs));
|
||||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||||
@ -569,7 +600,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||||
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
||||||
}
|
}
|
||||||
@ -588,7 +619,8 @@ public final class ImaAdsLoaderTest {
|
|||||||
.setPlayAdBeforeStartPosition(false)
|
.setPlayAdBeforeStartPosition(false)
|
||||||
.setImaFactory(mockImaFactory)
|
.setImaFactory(mockImaFactory)
|
||||||
.setImaSdkSettings(mockImaSdkSettings)
|
.setImaSdkSettings(mockImaSdkSettings)
|
||||||
.buildForAdTag(TEST_URI));
|
.build(),
|
||||||
|
TEST_DATA_SPEC);
|
||||||
|
|
||||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000);
|
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000);
|
||||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||||
@ -596,7 +628,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
verify(mockAdsManager).destroy();
|
verify(mockAdsManager).destroy();
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||||
.withSkippedAdGroup(/* adGroupIndex= */ 0)
|
.withSkippedAdGroup(/* adGroupIndex= */ 0)
|
||||||
.withSkippedAdGroup(/* adGroupIndex= */ 1));
|
.withSkippedAdGroup(/* adGroupIndex= */ 1));
|
||||||
@ -624,7 +656,8 @@ public final class ImaAdsLoaderTest {
|
|||||||
.setPlayAdBeforeStartPosition(false)
|
.setPlayAdBeforeStartPosition(false)
|
||||||
.setImaFactory(mockImaFactory)
|
.setImaFactory(mockImaFactory)
|
||||||
.setImaSdkSettings(mockImaSdkSettings)
|
.setImaSdkSettings(mockImaSdkSettings)
|
||||||
.buildForAdTag(TEST_URI));
|
.build(),
|
||||||
|
TEST_DATA_SPEC);
|
||||||
|
|
||||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000);
|
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000);
|
||||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||||
@ -637,7 +670,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withSkippedAdGroup(/* adGroupIndex= */ 0)
|
.withSkippedAdGroup(/* adGroupIndex= */ 0)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
|
||||||
}
|
}
|
||||||
@ -663,7 +696,8 @@ public final class ImaAdsLoaderTest {
|
|||||||
.setPlayAdBeforeStartPosition(false)
|
.setPlayAdBeforeStartPosition(false)
|
||||||
.setImaFactory(mockImaFactory)
|
.setImaFactory(mockImaFactory)
|
||||||
.setImaSdkSettings(mockImaSdkSettings)
|
.setImaSdkSettings(mockImaSdkSettings)
|
||||||
.buildForAdTag(TEST_URI));
|
.build(),
|
||||||
|
TEST_DATA_SPEC);
|
||||||
|
|
||||||
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs));
|
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs));
|
||||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||||
@ -676,16 +710,90 @@ public final class ImaAdsLoaderTest {
|
|||||||
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||||
.withSkippedAdGroup(/* adGroupIndex= */ 0));
|
.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
|
@Test
|
||||||
public void stop_unregistersAllVideoControlOverlays() {
|
public void stop_unregistersAllVideoControlOverlays() {
|
||||||
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
|
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
|
||||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||||
imaAdsLoader.requestAds(adViewGroup);
|
imaAdsLoader.requestAds(TEST_DATA_SPEC, adViewGroup);
|
||||||
imaAdsLoader.stop();
|
imaAdsLoader.stop();
|
||||||
|
|
||||||
InOrder inOrder = inOrder(mockAdDisplayContainer);
|
InOrder inOrder = inOrder(mockAdDisplayContainer);
|
||||||
@ -695,7 +803,8 @@ public final class ImaAdsLoaderTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void loadAd_withLargeAdCuePoint_updatesAdPlaybackStateWithLoadedAd() {
|
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);
|
ImmutableList<Float> cuePoints = ImmutableList.of(midrollTimeSecs);
|
||||||
setupPlayback(CONTENT_TIMELINE, cuePoints);
|
setupPlayback(CONTENT_TIMELINE, cuePoints);
|
||||||
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
imaAdsLoader.start(adsLoaderListener, adViewProvider);
|
||||||
@ -735,7 +844,7 @@ public final class ImaAdsLoaderTest {
|
|||||||
|
|
||||||
assertThat(adsLoaderListener.adPlaybackState)
|
assertThat(adsLoaderListener.adPlaybackState)
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
AdPlaybackStateFactory.fromCuePoints(cuePoints)
|
ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints)
|
||||||
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
|
||||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||||
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI)
|
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI)
|
||||||
@ -749,16 +858,21 @@ public final class ImaAdsLoaderTest {
|
|||||||
new ImaAdsLoader.Builder(getApplicationContext())
|
new ImaAdsLoader.Builder(getApplicationContext())
|
||||||
.setImaFactory(mockImaFactory)
|
.setImaFactory(mockImaFactory)
|
||||||
.setImaSdkSettings(mockImaSdkSettings)
|
.setImaSdkSettings(mockImaSdkSettings)
|
||||||
.buildForAdTag(TEST_URI));
|
.build(),
|
||||||
|
TEST_DATA_SPEC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupPlayback(
|
private void setupPlayback(
|
||||||
Timeline contentTimeline, List<Float> cuePoints, ImaAdsLoader imaAdsLoader) {
|
Timeline contentTimeline,
|
||||||
|
List<Float> cuePoints,
|
||||||
|
ImaAdsLoader imaAdsLoader,
|
||||||
|
DataSpec adTagDataSpec) {
|
||||||
fakeExoPlayer = new FakePlayer();
|
fakeExoPlayer = new FakePlayer();
|
||||||
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline);
|
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline);
|
||||||
when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints);
|
when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints);
|
||||||
this.imaAdsLoader = imaAdsLoader;
|
this.imaAdsLoader = imaAdsLoader;
|
||||||
imaAdsLoader.setPlayer(fakeExoPlayer);
|
imaAdsLoader.setPlayer(fakeExoPlayer);
|
||||||
|
imaAdsLoader.setAdTagDataSpec(adTagDataSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupMocks() {
|
private void setupMocks() {
|
||||||
|
@ -19,13 +19,6 @@ dependencies {
|
|||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'androidx.collection:collection:' + androidxCollectionVersion
|
implementation 'androidx.collection:collection:' + androidxCollectionVersion
|
||||||
implementation 'androidx.concurrent:concurrent-futures:1.1.0'
|
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'
|
api 'androidx.media2:media2-session:1.0.3'
|
||||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
@ -45,7 +45,7 @@ public class DefaultMediaItemConverter implements MediaItemConverter {
|
|||||||
if (media2MediaItem instanceof CallbackMediaItem) {
|
if (media2MediaItem instanceof CallbackMediaItem) {
|
||||||
throw new IllegalStateException("CallbackMediaItem isn't supported");
|
throw new IllegalStateException("CallbackMediaItem isn't supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable Uri uri = null;
|
@Nullable Uri uri = null;
|
||||||
@Nullable String mediaId = null;
|
@Nullable String mediaId = null;
|
||||||
@Nullable String title = null;
|
@Nullable String title = null;
|
||||||
|
@ -23,13 +23,13 @@ import com.google.android.exoplayer2.MediaItem;
|
|||||||
*/
|
*/
|
||||||
public interface MediaItemConverter {
|
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}.
|
* ExoPlayer MediaItem}.
|
||||||
*/
|
*/
|
||||||
MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem media2MediaItem);
|
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}.
|
* Media2 MediaItem}.
|
||||||
*/
|
*/
|
||||||
androidx.media2.common.MediaItem convertToMedia2MediaItem(MediaItem exoPlayerMediaItem);
|
androidx.media2.common.MediaItem convertToMedia2MediaItem(MediaItem exoPlayerMediaItem);
|
||||||
|
@ -16,13 +16,6 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
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-qual:' + checkerframeworkVersion
|
||||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
|
@ -18,16 +18,6 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'androidx.work:work-runtime:2.4.0'
|
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
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,14 +16,16 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
|||||||
android.buildTypes.debug.testCoverageEnabled true
|
android.buildTypes.debug.testCoverageEnabled true
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
api ('com.google.guava:guava:' + guavaVersion) {
|
||||||
implementation ('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: 'com.google.code.findbugs', module: 'jsr305'
|
||||||
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
|
exclude group: 'org.checkerframework', module: 'checker-compat-qual'
|
||||||
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
|
exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
|
||||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||||
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
|
exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations'
|
||||||
}
|
}
|
||||||
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version
|
compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version
|
||||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
@ -4,3 +4,6 @@
|
|||||||
-dontwarn org.checkerframework.**
|
-dontwarn org.checkerframework.**
|
||||||
-dontwarn kotlin.annotations.jvm.**
|
-dontwarn kotlin.annotations.jvm.**
|
||||||
-dontwarn javax.annotation.**
|
-dontwarn javax.annotation.**
|
||||||
|
|
||||||
|
# From https://github.com/google/guava/wiki/UsingProGuardWithGuava
|
||||||
|
-dontwarn java.lang.ClassValue
|
||||||
|
@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo {
|
|||||||
|
|
||||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||||
public static final String VERSION = "2.12.0";
|
public static final String VERSION = "2.12.1";
|
||||||
|
|
||||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.12.0";
|
public static final String VERSION_SLASHY = "ExoPlayerLib/2.12.1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of the library expressed as an integer, for example 1002003.
|
* The version of the library expressed as an integer, for example 1002003.
|
||||||
@ -44,7 +44,7 @@ public final class ExoPlayerLibraryInfo {
|
|||||||
* integer version 123045006 (123-045-006).
|
* integer version 123045006 (123-045-006).
|
||||||
*/
|
*/
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||||
public static final int VERSION_INT = 2012000;
|
public static final int VERSION_INT = 2012001;
|
||||||
|
|
||||||
/** The default user agent for requests made by the library. */
|
/** The default user agent for requests made by the library. */
|
||||||
public static final String DEFAULT_USER_AGENT =
|
public static final String DEFAULT_USER_AGENT =
|
||||||
|
@ -669,6 +669,10 @@ public final class MediaItem {
|
|||||||
@Nullable public final String language;
|
@Nullable public final String language;
|
||||||
/** The selection flags. */
|
/** The selection flags. */
|
||||||
@C.SelectionFlags public final int selectionFlags;
|
@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.
|
* 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 uri The {@link Uri URI} to the subtitle file.
|
||||||
* @param mimeType The MIME type.
|
* @param mimeType The MIME type.
|
||||||
@ -691,10 +695,32 @@ public final class MediaItem {
|
|||||||
*/
|
*/
|
||||||
public Subtitle(
|
public Subtitle(
|
||||||
Uri uri, String mimeType, @Nullable String language, @C.SelectionFlags int selectionFlags) {
|
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.uri = uri;
|
||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
this.language = language;
|
this.language = language;
|
||||||
this.selectionFlags = selectionFlags;
|
this.selectionFlags = selectionFlags;
|
||||||
|
this.roleFlags = roleFlags;
|
||||||
|
this.label = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -711,7 +737,9 @@ public final class MediaItem {
|
|||||||
return uri.equals(other.uri)
|
return uri.equals(other.uri)
|
||||||
&& mimeType.equals(other.mimeType)
|
&& mimeType.equals(other.mimeType)
|
||||||
&& Util.areEqual(language, other.language)
|
&& Util.areEqual(language, other.language)
|
||||||
&& selectionFlags == other.selectionFlags;
|
&& selectionFlags == other.selectionFlags
|
||||||
|
&& roleFlags == other.roleFlags
|
||||||
|
&& Util.areEqual(label, other.label);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -720,6 +748,8 @@ public final class MediaItem {
|
|||||||
result = 31 * result + mimeType.hashCode();
|
result = 31 * result + mimeType.hashCode();
|
||||||
result = 31 * result + (language == null ? 0 : language.hashCode());
|
result = 31 * result + (language == null ? 0 : language.hashCode());
|
||||||
result = 31 * result + selectionFlags;
|
result = 31 * result + selectionFlags;
|
||||||
|
result = 31 * result + roleFlags;
|
||||||
|
result = 31 * result + (label == null ? 0 : label.hashCode());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util;
|
|||||||
|
|
||||||
import static android.content.Context.UI_MODE_SERVICE;
|
import static android.content.Context.UI_MODE_SERVICE;
|
||||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
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.max;
|
||||||
import static java.lang.Math.min;
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ import android.os.SystemClock;
|
|||||||
import android.security.NetworkSecurityPolicy;
|
import android.security.NetworkSecurityPolicy;
|
||||||
import android.telephony.TelephonyManager;
|
import android.telephony.TelephonyManager;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Base64;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.SurfaceView;
|
import android.view.SurfaceView;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
@ -530,6 +532,54 @@ public final class Util {
|
|||||||
return Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, threadName));
|
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.
|
* Closes a {@link DataSource}, suppressing any {@link IOException} that may occur.
|
||||||
*
|
*
|
||||||
@ -1844,13 +1894,16 @@ public final class Util {
|
|||||||
if (timeMs == C.TIME_UNSET) {
|
if (timeMs == C.TIME_UNSET) {
|
||||||
timeMs = 0;
|
timeMs = 0;
|
||||||
}
|
}
|
||||||
|
String prefix = timeMs < 0 ? "-" : "";
|
||||||
|
timeMs = abs(timeMs);
|
||||||
long totalSeconds = (timeMs + 500) / 1000;
|
long totalSeconds = (timeMs + 500) / 1000;
|
||||||
long seconds = totalSeconds % 60;
|
long seconds = totalSeconds % 60;
|
||||||
long minutes = (totalSeconds / 60) % 60;
|
long minutes = (totalSeconds / 60) % 60;
|
||||||
long hours = totalSeconds / 3600;
|
long hours = totalSeconds / 3600;
|
||||||
builder.setLength(0);
|
builder.setLength(0);
|
||||||
return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
|
return hours > 0
|
||||||
: formatter.format("%02d:%02d", minutes, seconds).toString();
|
? 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();
|
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,
|
* A hacky method that always throws {@code t} even if {@code t} is a checked exception,
|
||||||
* and is not declared to be thrown.
|
* and is not declared to be thrown.
|
||||||
|
@ -173,7 +173,14 @@ public class MediaItemTest {
|
|||||||
Uri.parse(URI_STRING + "/de"),
|
Uri.parse(URI_STRING + "/de"),
|
||||||
MimeTypes.APPLICATION_TTML,
|
MimeTypes.APPLICATION_TTML,
|
||||||
/* language= */ null,
|
/* 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 =
|
MediaItem mediaItem =
|
||||||
new MediaItem.Builder().setUri(URI_STRING).setSubtitles(subtitles).build();
|
new MediaItem.Builder().setUri(URI_STRING).setSubtitles(subtitles).build();
|
||||||
@ -317,7 +324,10 @@ public class MediaItemTest {
|
|||||||
new MediaItem.Subtitle(
|
new MediaItem.Subtitle(
|
||||||
Uri.parse(URI_STRING + "/en"),
|
Uri.parse(URI_STRING + "/en"),
|
||||||
MimeTypes.APPLICATION_TTML,
|
MimeTypes.APPLICATION_TTML,
|
||||||
/* language= */ "en")))
|
/* language= */ "en",
|
||||||
|
C.SELECTION_FLAG_FORCED,
|
||||||
|
C.ROLE_FLAG_ALTERNATE,
|
||||||
|
"label")))
|
||||||
.setTag(new Object())
|
.setTag(new Object())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -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.binarySearchFloor;
|
||||||
import static com.google.android.exoplayer2.util.Util.escapeFileName;
|
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.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.parseXsDateTime;
|
||||||
import static com.google.android.exoplayer2.util.Util.parseXsDuration;
|
import static com.google.android.exoplayer2.util.Util.parseXsDuration;
|
||||||
import static com.google.android.exoplayer2.util.Util.unescapeFileName;
|
import static com.google.android.exoplayer2.util.Util.unescapeFileName;
|
||||||
@ -37,6 +38,7 @@ import java.nio.ByteBuffer;
|
|||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Formatter;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.zip.Deflater;
|
import java.util.zip.Deflater;
|
||||||
import org.junit.Test;
|
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
|
@Test
|
||||||
public void crc32_returnsUpdatedCrc32() {
|
public void crc32_returnsUpdatedCrc32() {
|
||||||
byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F};
|
byte[] bytes = {0x5F, 0x78, 0x04, 0x7B, 0x5F};
|
||||||
@ -1082,6 +1092,12 @@ public class UtilTest {
|
|||||||
assertThat(Util.tableExists(database, "table")).isFalse();
|
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) {
|
private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) {
|
||||||
assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName);
|
assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName);
|
||||||
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);
|
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);
|
||||||
|
@ -37,40 +37,20 @@ dependencies {
|
|||||||
api project(modulePrefix + 'library-common')
|
api project(modulePrefix + 'library-common')
|
||||||
api project(modulePrefix + 'library-extractor')
|
api project(modulePrefix + 'library-extractor')
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
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 'com.google.code.findbugs:jsr305:' + jsr305Version
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
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:' + dexmakerVersion
|
||||||
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||||
androidTestImplementation(project(modulePrefix + 'testutils')) {
|
androidTestImplementation(project(modulePrefix + 'testutils')) {
|
||||||
exclude module: modulePrefix.substring(1) + 'library-core'
|
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 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
|
testImplementation project(modulePrefix + 'robolectricutils')
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
@ -71,8 +71,3 @@
|
|||||||
-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
|
-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
|
||||||
<init>(com.google.android.exoplayer2.upstream.DataSource$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.**
|
|
||||||
|
@ -44,12 +44,6 @@ public abstract class BasePlayer implements Player {
|
|||||||
setMediaItems(Collections.singletonList(mediaItem), resetPosition);
|
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
|
@Override
|
||||||
public void setMediaItems(List<MediaItem> mediaItems) {
|
public void setMediaItems(List<MediaItem> mediaItems) {
|
||||||
setMediaItems(mediaItems, /* resetPosition= */ true);
|
setMediaItems(mediaItems, /* resetPosition= */ true);
|
||||||
|
@ -16,11 +16,13 @@
|
|||||||
package com.google.android.exoplayer2;
|
package com.google.android.exoplayer2;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.media.AudioTrack;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||||
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
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.DefaultAudioSink;
|
||||||
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
||||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||||
@ -622,14 +624,13 @@ public interface ExoPlayer extends Player {
|
|||||||
* the following:
|
* the following:
|
||||||
*
|
*
|
||||||
* <ul>
|
* <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
|
* 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)}.
|
* 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).
|
* MP3 or AAC).
|
||||||
* <li>The {@link com.google.android.exoplayer2.audio.AudioSink} is playing with an offload
|
* <li>The {@link AudioSink} is playing with an offload {@link AudioTrack}.
|
||||||
* {@link android.media.AudioTrack}.
|
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p>This method is experimental, and will be renamed or removed in a future release.
|
* <p>This method is experimental, and will be renamed or removed in a future release.
|
||||||
|
@ -153,7 +153,7 @@ import java.util.concurrent.TimeoutException;
|
|||||||
new TrackSelectorResult(
|
new TrackSelectorResult(
|
||||||
new RendererConfiguration[renderers.length],
|
new RendererConfiguration[renderers.length],
|
||||||
new TrackSelection[renderers.length],
|
new TrackSelection[renderers.length],
|
||||||
null);
|
/* info= */ null);
|
||||||
period = new Timeline.Period();
|
period = new Timeline.Period();
|
||||||
maskingWindowIndex = C.INDEX_UNSET;
|
maskingWindowIndex = C.INDEX_UNSET;
|
||||||
playbackInfoUpdateHandler = new Handler(applicationLooper);
|
playbackInfoUpdateHandler = new Handler(applicationLooper);
|
||||||
@ -347,6 +347,11 @@ import java.util.concurrent.TimeoutException;
|
|||||||
prepare();
|
prepare();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
|
||||||
|
setMediaSources(createMediaSources(mediaItems), resetPosition);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setMediaItems(
|
public void setMediaItems(
|
||||||
List<MediaItem> mediaItems, int startWindowIndex, long startPositionMs) {
|
List<MediaItem> mediaItems, int startWindowIndex, long startPositionMs) {
|
||||||
|
@ -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
|
// tracks in the current period have uneven durations and are still being read by another
|
||||||
// renderer. See: https://github.com/google/ExoPlayer/issues/1874.
|
// renderer. See: https://github.com/google/ExoPlayer/issues/1874.
|
||||||
boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream();
|
boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream();
|
||||||
boolean isWaitingForNextStream =
|
boolean isWaitingForNextStream = !isReadingAhead && renderer.hasReadStreamToEnd();
|
||||||
!isReadingAhead
|
|
||||||
&& playingPeriodHolder.getNext() != null
|
|
||||||
&& renderer.hasReadStreamToEnd();
|
|
||||||
boolean allowsPlayback =
|
boolean allowsPlayback =
|
||||||
isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded();
|
isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded();
|
||||||
renderersAllowPlayback = renderersAllowPlayback && allowsPlayback;
|
renderersAllowPlayback = renderersAllowPlayback && allowsPlayback;
|
||||||
|
@ -315,8 +315,8 @@ import com.google.common.collect.ImmutableList;
|
|||||||
public boolean updateQueuedPeriods(
|
public boolean updateQueuedPeriods(
|
||||||
Timeline timeline, long rendererPositionUs, long maxRendererReadPositionUs) {
|
Timeline timeline, long rendererPositionUs, long maxRendererReadPositionUs) {
|
||||||
// TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline
|
// 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
|
// is set, once all cases handled by ExoPlayerImplInternal.handleMediaSourceListInfoRefreshed
|
||||||
// handled here.
|
// can be handled here.
|
||||||
MediaPeriodHolder previousPeriodHolder = null;
|
MediaPeriodHolder previousPeriodHolder = null;
|
||||||
MediaPeriodHolder periodHolder = playing;
|
MediaPeriodHolder periodHolder = playing;
|
||||||
while (periodHolder != null) {
|
while (periodHolder != null) {
|
||||||
@ -326,8 +326,8 @@ import com.google.common.collect.ImmutableList;
|
|||||||
MediaPeriodInfo newPeriodInfo;
|
MediaPeriodInfo newPeriodInfo;
|
||||||
if (previousPeriodHolder == null) {
|
if (previousPeriodHolder == null) {
|
||||||
// The id and start position of the first period have already been verified by
|
// The id and start position of the first period have already been verified by
|
||||||
// ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline
|
// ExoPlayerImplInternal.handleMediaSourceListInfoRefreshed. Just update duration,
|
||||||
// and isLastInPeriod flags.
|
// isLastInTimeline and isLastInPeriod flags.
|
||||||
newPeriodInfo = getUpdatedMediaPeriodInfo(timeline, oldPeriodInfo);
|
newPeriodInfo = getUpdatedMediaPeriodInfo(timeline, oldPeriodInfo);
|
||||||
} else {
|
} else {
|
||||||
newPeriodInfo =
|
newPeriodInfo =
|
||||||
|
@ -289,7 +289,10 @@ import java.lang.reflect.Method;
|
|||||||
if (elapsedSincePreviousModeUs < MODE_SWITCH_SMOOTHING_DURATION_US) {
|
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
|
// Use a ramp to smooth between the old mode and the new one to avoid introducing a sudden
|
||||||
// jump if the two modes disagree.
|
// 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.
|
// A ramp consisting of 1000 points distributed over MODE_SWITCH_SMOOTHING_DURATION_US.
|
||||||
long rampPoint = (elapsedSincePreviousModeUs * 1000) / MODE_SWITCH_SMOOTHING_DURATION_US;
|
long rampPoint = (elapsedSincePreviousModeUs * 1000) / MODE_SWITCH_SMOOTHING_DURATION_US;
|
||||||
positionUs *= rampPoint;
|
positionUs *= rampPoint;
|
||||||
|
@ -28,6 +28,7 @@ import android.os.Handler;
|
|||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
@ -335,6 +336,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
private boolean tunneling;
|
private boolean tunneling;
|
||||||
private long lastFeedElapsedRealtimeMs;
|
private long lastFeedElapsedRealtimeMs;
|
||||||
private boolean offloadDisabledUntilNextConfiguration;
|
private boolean offloadDisabledUntilNextConfiguration;
|
||||||
|
private boolean isWaitingForOffloadEndOfStreamHandled;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new default audio sink.
|
* Creates a new default audio sink.
|
||||||
@ -711,6 +713,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
audioTrack.setOffloadEndOfStream();
|
audioTrack.setOffloadEndOfStream();
|
||||||
audioTrack.setOffloadDelayPadding(
|
audioTrack.setOffloadDelayPadding(
|
||||||
configuration.inputFormat.encoderDelay, configuration.inputFormat.encoderPadding);
|
configuration.inputFormat.encoderDelay, configuration.inputFormat.encoderPadding);
|
||||||
|
isWaitingForOffloadEndOfStreamHandled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Re-apply playback parameters.
|
// Re-apply playback parameters.
|
||||||
@ -931,13 +934,26 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
throw new WriteException(bytesWritten);
|
throw new WriteException(bytesWritten);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playing
|
if (isOffloadedPlayback(audioTrack)) {
|
||||||
&& listener != null
|
// After calling AudioTrack.setOffloadEndOfStream, the AudioTrack internally stops and
|
||||||
&& bytesWritten < bytesRemaining
|
// restarts during which AudioTrack.write will return 0. This situation must be detected to
|
||||||
&& isOffloadedPlayback(audioTrack)) {
|
// prevent reporting the buffer as full even though it is not which could lead ExoPlayer to
|
||||||
long pendingDurationMs =
|
// sleep forever waiting for a onDataRequest that will never come.
|
||||||
audioTrackPositionTracker.getPendingBufferDurationMs(writtenEncodedFrames);
|
if (writtenEncodedFrames > 0) {
|
||||||
listener.onOffloadBufferFull(pendingDurationMs);
|
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
|
||||||
|
&& !isWaitingForOffloadEndOfStreamHandled) {
|
||||||
|
long pendingDurationMs =
|
||||||
|
audioTrackPositionTracker.getPendingBufferDurationMs(writtenEncodedFrames);
|
||||||
|
listener.onOffloadBufferFull(pendingDurationMs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configuration.outputMode == OUTPUT_MODE_PCM) {
|
if (configuration.outputMode == OUTPUT_MODE_PCM) {
|
||||||
@ -1220,6 +1236,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
submittedEncodedFrames = 0;
|
submittedEncodedFrames = 0;
|
||||||
writtenPcmBytes = 0;
|
writtenPcmBytes = 0;
|
||||||
writtenEncodedFrames = 0;
|
writtenEncodedFrames = 0;
|
||||||
|
isWaitingForOffloadEndOfStreamHandled = false;
|
||||||
framesPerEncodedSample = 0;
|
framesPerEncodedSample = 0;
|
||||||
mediaPositionParameters =
|
mediaPositionParameters =
|
||||||
new MediaPositionParameters(
|
new MediaPositionParameters(
|
||||||
@ -1679,27 +1696,43 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(29)
|
@RequiresApi(29)
|
||||||
private final class StreamEventCallbackV29 extends AudioTrack.StreamEventCallback {
|
private final class StreamEventCallbackV29 {
|
||||||
private final Handler handler;
|
private final Handler handler;
|
||||||
|
private final AudioTrack.StreamEventCallback callback;
|
||||||
|
|
||||||
public StreamEventCallbackV29() {
|
public StreamEventCallbackV29() {
|
||||||
handler = new Handler();
|
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);
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onOffloadBufferEmptying();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDataRequest(AudioTrack track, int size) {
|
public void onTearDown(@NonNull AudioTrack track) {
|
||||||
Assertions.checkState(track == DefaultAudioSink.this.audioTrack);
|
if (listener != null && playing) {
|
||||||
if (listener != null) {
|
// A new Audio Track needs to be created and it's buffer filled, which will be done
|
||||||
listener.onOffloadBufferEmptying();
|
// 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) {
|
public void register(AudioTrack audioTrack) {
|
||||||
audioTrack.registerStreamEventCallback(handler::post, this);
|
audioTrack.registerStreamEventCallback(handler::post, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unregister(AudioTrack audioTrack) {
|
public void unregister(AudioTrack audioTrack) {
|
||||||
audioTrack.unregisterStreamEventCallback(this);
|
audioTrack.unregisterStreamEventCallback(callback);
|
||||||
handler.removeCallbacksAndMessages(/* token= */ null);
|
handler.removeCallbacksAndMessages(/* token= */ null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -363,6 +363,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
@Nullable private DrmSession sourceDrmSession;
|
@Nullable private DrmSession sourceDrmSession;
|
||||||
@Nullable private MediaCrypto mediaCrypto;
|
@Nullable private MediaCrypto mediaCrypto;
|
||||||
private boolean mediaCryptoRequiresSecureDecoder;
|
private boolean mediaCryptoRequiresSecureDecoder;
|
||||||
|
private long renderTimeLimitMs;
|
||||||
private float operatingRate;
|
private float operatingRate;
|
||||||
@Nullable private MediaCodec codec;
|
@Nullable private MediaCodec codec;
|
||||||
@Nullable private MediaCodecAdapter codecAdapter;
|
@Nullable private MediaCodecAdapter codecAdapter;
|
||||||
@ -442,6 +443,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
outputBufferInfo = new MediaCodec.BufferInfo();
|
outputBufferInfo = new MediaCodec.BufferInfo();
|
||||||
operatingRate = 1f;
|
operatingRate = 1f;
|
||||||
mediaCodecOperationMode = OPERATION_MODE_SYNCHRONOUS;
|
mediaCodecOperationMode = OPERATION_MODE_SYNCHRONOUS;
|
||||||
|
renderTimeLimitMs = C.TIME_UNSET;
|
||||||
pendingOutputStreamStartPositionsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
|
pendingOutputStreamStartPositionsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
|
||||||
pendingOutputStreamOffsetsUs = 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];
|
pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT];
|
||||||
@ -451,6 +453,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
resetCodecStateForRelease();
|
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}.
|
* Set the mode of operation of the underlying {@link MediaCodec}.
|
||||||
*
|
*
|
||||||
@ -837,9 +852,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
while (bypassRender(positionUs, elapsedRealtimeUs)) {}
|
while (bypassRender(positionUs, elapsedRealtimeUs)) {}
|
||||||
TraceUtil.endSection();
|
TraceUtil.endSection();
|
||||||
} else if (codec != null) {
|
} else if (codec != null) {
|
||||||
|
long renderStartTimeMs = SystemClock.elapsedRealtime();
|
||||||
TraceUtil.beginSection("drainAndFeed");
|
TraceUtil.beginSection("drainAndFeed");
|
||||||
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
|
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)
|
||||||
while (feedInputBuffer()) {}
|
&& shouldContinueRendering(renderStartTimeMs)) {}
|
||||||
|
while (feedInputBuffer() && shouldContinueRendering(renderStartTimeMs)) {}
|
||||||
TraceUtil.endSection();
|
TraceUtil.endSection();
|
||||||
} else {
|
} else {
|
||||||
decoderCounters.skippedInputBufferCount += skipSource(positionUs);
|
decoderCounters.skippedInputBufferCount += skipSource(positionUs);
|
||||||
@ -1171,6 +1188,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
onCodecInitialized(codecName, codecInitializedTimestamp, elapsed);
|
onCodecInitialized(codecName, codecInitializedTimestamp, elapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean shouldContinueRendering(long renderStartTimeMs) {
|
||||||
|
return renderTimeLimitMs == C.TIME_UNSET
|
||||||
|
|| SystemClock.elapsedRealtime() - renderStartTimeMs < renderTimeLimitMs;
|
||||||
|
}
|
||||||
|
|
||||||
private void getCodecBuffers(MediaCodec codec) {
|
private void getCodecBuffers(MediaCodec codec) {
|
||||||
if (Util.SDK_INT < 21) {
|
if (Util.SDK_INT < 21) {
|
||||||
inputBuffers = codec.getInputBuffers();
|
inputBuffers = codec.getInputBuffers();
|
||||||
|
@ -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.AdsLoader.AdViewProvider;
|
||||||
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
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.DefaultDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||||
@ -280,7 +281,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||||||
|
|
||||||
private MediaSource maybeWrapWithAdsMediaSource(MediaItem mediaItem, MediaSource mediaSource) {
|
private MediaSource maybeWrapWithAdsMediaSource(MediaItem mediaItem, MediaSource mediaSource) {
|
||||||
Assertions.checkNotNull(mediaItem.playbackProperties);
|
Assertions.checkNotNull(mediaItem.playbackProperties);
|
||||||
if (mediaItem.playbackProperties.adTagUri == null) {
|
@Nullable Uri adTagUri = mediaItem.playbackProperties.adTagUri;
|
||||||
|
if (adTagUri == null) {
|
||||||
return mediaSource;
|
return mediaSource;
|
||||||
}
|
}
|
||||||
AdsLoaderProvider adsLoaderProvider = this.adsLoaderProvider;
|
AdsLoaderProvider adsLoaderProvider = this.adsLoaderProvider;
|
||||||
@ -292,14 +294,17 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||||||
+ " setAdViewProvider.");
|
+ " setAdViewProvider.");
|
||||||
return mediaSource;
|
return mediaSource;
|
||||||
}
|
}
|
||||||
@Nullable
|
@Nullable AdsLoader adsLoader = adsLoaderProvider.getAdsLoader(adTagUri);
|
||||||
AdsLoader adsLoader = adsLoaderProvider.getAdsLoader(mediaItem.playbackProperties.adTagUri);
|
|
||||||
if (adsLoader == null) {
|
if (adsLoader == null) {
|
||||||
Log.w(TAG, "Playing media without ads. No AdsLoader for provided adTagUri");
|
Log.w(TAG, "Playing media without ads. No AdsLoader for provided adTagUri");
|
||||||
return mediaSource;
|
return mediaSource;
|
||||||
}
|
}
|
||||||
return new AdsMediaSource(
|
return new AdsMediaSource(
|
||||||
mediaSource, /* adMediaSourceFactory= */ this, adsLoader, adViewProvider);
|
mediaSource,
|
||||||
|
new DataSpec(adTagUri),
|
||||||
|
/* adMediaSourceFactory= */ this,
|
||||||
|
adsLoader,
|
||||||
|
adViewProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SparseArray<MediaSourceFactory> loadDelegates(
|
private static SparseArray<MediaSourceFactory> loadDelegates(
|
||||||
|
@ -340,7 +340,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||||||
/* manifest= */ null,
|
/* manifest= */ null,
|
||||||
mediaItem);
|
mediaItem);
|
||||||
if (timelineIsPlaceholder) {
|
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.
|
// placeholder. See https://github.com/google/ExoPlayer/issues/4727.
|
||||||
timeline =
|
timeline =
|
||||||
new ForwardingTimeline(timeline) {
|
new ForwardingTimeline(timeline) {
|
||||||
|
@ -281,6 +281,8 @@ public final class SingleSampleMediaSource extends BaseMediaSource {
|
|||||||
.setSampleMimeType(subtitle.mimeType)
|
.setSampleMimeType(subtitle.mimeType)
|
||||||
.setLanguage(subtitle.language)
|
.setLanguage(subtitle.language)
|
||||||
.setSelectionFlags(subtitle.selectionFlags)
|
.setSelectionFlags(subtitle.selectionFlags)
|
||||||
|
.setRoleFlags(subtitle.roleFlags)
|
||||||
|
.setLabel(subtitle.label)
|
||||||
.build();
|
.build();
|
||||||
dataSpec =
|
dataSpec =
|
||||||
new DataSpec.Builder().setUri(subtitle.uri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build();
|
new DataSpec.Builder().setUri(subtitle.uri).setFlags(DataSpec.FLAG_ALLOW_GZIP).build();
|
||||||
|
@ -198,6 +198,14 @@ public interface AdsLoader {
|
|||||||
*/
|
*/
|
||||||
void setSupportedContentTypes(@C.ContentType int... contentTypes);
|
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}.
|
* Starts using the ads loader for playback. Called on the main thread by {@link AdsMediaSource}.
|
||||||
*
|
*
|
||||||
|
@ -128,6 +128,7 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
private final MediaSourceFactory adMediaSourceFactory;
|
private final MediaSourceFactory adMediaSourceFactory;
|
||||||
private final AdsLoader adsLoader;
|
private final AdsLoader adsLoader;
|
||||||
private final AdsLoader.AdViewProvider adViewProvider;
|
private final AdsLoader.AdViewProvider adViewProvider;
|
||||||
|
@Nullable private final DataSpec adTagDataSpec;
|
||||||
private final Handler mainHandler;
|
private final Handler mainHandler;
|
||||||
private final Timeline.Period period;
|
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 dataSourceFactory Factory for data sources used to load ad media.
|
||||||
* @param adsLoader The loader for ads.
|
* @param adsLoader The loader for ads.
|
||||||
* @param adViewProvider Provider of views for the ad UI.
|
* @param adViewProvider Provider of views for the ad UI.
|
||||||
|
* @deprecated Use {@link AdsMediaSource#AdsMediaSource(MediaSource, DataSpec, MediaSourceFactory,
|
||||||
|
* AdsLoader, AdsLoader.AdViewProvider)} instead.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public AdsMediaSource(
|
public AdsMediaSource(
|
||||||
MediaSource contentMediaSource,
|
MediaSource contentMediaSource,
|
||||||
DataSource.Factory dataSourceFactory,
|
DataSource.Factory dataSourceFactory,
|
||||||
@ -155,7 +159,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
contentMediaSource,
|
contentMediaSource,
|
||||||
new ProgressiveMediaSource.Factory(dataSourceFactory),
|
new ProgressiveMediaSource.Factory(dataSourceFactory),
|
||||||
adsLoader,
|
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 adMediaSourceFactory Factory for media sources used to load ad media.
|
||||||
* @param adsLoader The loader for ads.
|
* @param adsLoader The loader for ads.
|
||||||
* @param adViewProvider Provider of views for the ad UI.
|
* @param adViewProvider Provider of views for the ad UI.
|
||||||
|
* @deprecated Use {@link AdsMediaSource#AdsMediaSource(MediaSource, DataSpec, MediaSourceFactory,
|
||||||
|
* AdsLoader, AdsLoader.AdViewProvider)} instead.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public AdsMediaSource(
|
public AdsMediaSource(
|
||||||
MediaSource contentMediaSource,
|
MediaSource contentMediaSource,
|
||||||
MediaSourceFactory adMediaSourceFactory,
|
MediaSourceFactory adMediaSourceFactory,
|
||||||
AdsLoader adsLoader,
|
AdsLoader adsLoader,
|
||||||
AdsLoader.AdViewProvider adViewProvider) {
|
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.contentMediaSource = contentMediaSource;
|
||||||
this.adMediaSourceFactory = adMediaSourceFactory;
|
this.adMediaSourceFactory = adMediaSourceFactory;
|
||||||
this.adsLoader = adsLoader;
|
this.adsLoader = adsLoader;
|
||||||
this.adViewProvider = adViewProvider;
|
this.adViewProvider = adViewProvider;
|
||||||
|
this.adTagDataSpec = adTagDataSpec;
|
||||||
mainHandler = new Handler(Looper.getMainLooper());
|
mainHandler = new Handler(Looper.getMainLooper());
|
||||||
period = new Timeline.Period();
|
period = new Timeline.Period();
|
||||||
adMediaSourceHolders = new AdMediaSourceHolder[0][];
|
adMediaSourceHolders = new AdMediaSourceHolder[0][];
|
||||||
@ -204,7 +246,13 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||||||
ComponentListener componentListener = new ComponentListener();
|
ComponentListener componentListener = new ComponentListener();
|
||||||
this.componentListener = componentListener;
|
this.componentListener = componentListener;
|
||||||
prepareChildSource(CHILD_SOURCE_MEDIA_PERIOD_ID, contentMediaSource);
|
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
|
@Override
|
||||||
|
@ -325,10 +325,13 @@ public final class TextRenderer extends BaseRenderer implements Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private long getNextEventTime() {
|
private long getNextEventTime() {
|
||||||
|
if (nextSubtitleEventIndex == C.INDEX_UNSET) {
|
||||||
|
return Long.MAX_VALUE;
|
||||||
|
}
|
||||||
checkNotNull(subtitle);
|
checkNotNull(subtitle);
|
||||||
return nextSubtitleEventIndex == C.INDEX_UNSET
|
return nextSubtitleEventIndex >= subtitle.getEventTimeCount()
|
||||||
|| nextSubtitleEventIndex >= subtitle.getEventTimeCount()
|
? Long.MAX_VALUE
|
||||||
? Long.MAX_VALUE : subtitle.getEventTime(nextSubtitleEventIndex);
|
: subtitle.getEventTime(nextSubtitleEventIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateOutput(List<Cue> cues) {
|
private void updateOutput(List<Cue> cues) {
|
||||||
|
@ -263,8 +263,9 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
|
|||||||
SsaStyle.Overrides styleOverrides = SsaStyle.Overrides.parseFromDialogue(rawText);
|
SsaStyle.Overrides styleOverrides = SsaStyle.Overrides.parseFromDialogue(rawText);
|
||||||
String text =
|
String text =
|
||||||
SsaStyle.Overrides.stripStyleOverrides(rawText)
|
SsaStyle.Overrides.stripStyleOverrides(rawText)
|
||||||
.replaceAll("\\\\N", "\n")
|
.replace("\\N", "\n")
|
||||||
.replaceAll("\\\\n", "\n");
|
.replace("\\n", "\n")
|
||||||
|
.replace("\\h", "\u00A0");
|
||||||
Cue cue = createCue(text, style, styleOverrides, screenWidth, screenHeight);
|
Cue cue = createCue(text, style, styleOverrides, screenWidth, screenHeight);
|
||||||
|
|
||||||
int startTimeIndex = addCuePlacerholderByTime(startTimeUs, cueTimesUs, cues);
|
int startTimeIndex = addCuePlacerholderByTime(startTimeUs, cueTimesUs, cues);
|
||||||
|
@ -339,14 +339,15 @@ public abstract class MappingTrackSelector extends TrackSelector {
|
|||||||
* Returns the mapping information for the currently active track selection, or null if no
|
* Returns the mapping information for the currently active track selection, or null if no
|
||||||
* selection is currently active.
|
* selection is currently active.
|
||||||
*/
|
*/
|
||||||
public final @Nullable MappedTrackInfo getCurrentMappedTrackInfo() {
|
@Nullable
|
||||||
|
public final MappedTrackInfo getCurrentMappedTrackInfo() {
|
||||||
return currentMappedTrackInfo;
|
return currentMappedTrackInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackSelector implementation.
|
// TrackSelector implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void onSelectionActivated(Object info) {
|
public final void onSelectionActivated(@Nullable Object info) {
|
||||||
currentMappedTrackInfo = (MappedTrackInfo) info;
|
currentMappedTrackInfo = (MappedTrackInfo) info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ public abstract class TrackSelector {
|
|||||||
*
|
*
|
||||||
* @param info The value of {@link TrackSelectorResult#info} in the activated selection.
|
* @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
|
* Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously
|
||||||
|
@ -40,19 +40,20 @@ public final class TrackSelectorResult {
|
|||||||
* An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)}
|
* An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)}
|
||||||
* should the selections be activated.
|
* 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
|
* @param rendererConfigurations A {@link RendererConfiguration} for each renderer. A null entry
|
||||||
* indicates the corresponding renderer should be disabled.
|
* indicates the corresponding renderer should be disabled.
|
||||||
* @param selections A {@link TrackSelectionArray} containing the selection for each renderer.
|
* @param selections A {@link TrackSelectionArray} containing the selection for each renderer.
|
||||||
* @param info An opaque object that will be returned to {@link
|
* @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(
|
public TrackSelectorResult(
|
||||||
@NullableType RendererConfiguration[] rendererConfigurations,
|
@NullableType RendererConfiguration[] rendererConfigurations,
|
||||||
@NullableType TrackSelection[] selections,
|
@NullableType TrackSelection[] selections,
|
||||||
Object info) {
|
@Nullable Object info) {
|
||||||
this.rendererConfigurations = rendererConfigurations;
|
this.rendererConfigurations = rendererConfigurations;
|
||||||
this.selections = new TrackSelectionArray(selections);
|
this.selections = new TrackSelectionArray(selections);
|
||||||
this.info = info;
|
this.info = info;
|
||||||
|
@ -59,7 +59,8 @@ public final class DataSchemeDataSource extends BaseDataSource {
|
|||||||
String dataString = uriParts[1];
|
String dataString = uriParts[1];
|
||||||
if (uriParts[0].contains(";base64")) {
|
if (uriParts[0].contains(";base64")) {
|
||||||
try {
|
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) {
|
} catch (IllegalArgumentException e) {
|
||||||
throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e);
|
throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e);
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.upstream;
|
package com.google.android.exoplayer2.upstream;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.Nullable;
|
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.
|
* <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
|
* rawresource:///resourceId, where rawResourceId is the integer identifier of the raw
|
||||||
* resource).
|
* 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>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
|
* <li>rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an
|
||||||
* explicit dependency on ExoPlayer's RTMP extension.
|
* 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_CONTENT = "content";
|
||||||
private static final String SCHEME_RTMP = "rtmp";
|
private static final String SCHEME_RTMP = "rtmp";
|
||||||
private static final String SCHEME_UDP = "udp";
|
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_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME;
|
||||||
|
private static final String SCHEME_ANDROID_RESOURCE = ContentResolver.SCHEME_ANDROID_RESOURCE;
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final List<TransferListener> transferListeners;
|
private final List<TransferListener> transferListeners;
|
||||||
@ -182,9 +188,9 @@ public final class DefaultDataSource implements DataSource {
|
|||||||
dataSource = getRtmpDataSource();
|
dataSource = getRtmpDataSource();
|
||||||
} else if (SCHEME_UDP.equals(scheme)) {
|
} else if (SCHEME_UDP.equals(scheme)) {
|
||||||
dataSource = getUdpDataSource();
|
dataSource = getUdpDataSource();
|
||||||
} else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) {
|
} else if (SCHEME_DATA.equals(scheme)) {
|
||||||
dataSource = getDataSchemeDataSource();
|
dataSource = getDataSchemeDataSource();
|
||||||
} else if (SCHEME_RAW.equals(scheme)) {
|
} else if (SCHEME_RAW.equals(scheme) || SCHEME_ANDROID_RESOURCE.equals(scheme)) {
|
||||||
dataSource = getRawResourceDataSource();
|
dataSource = getRawResourceDataSource();
|
||||||
} else {
|
} else {
|
||||||
dataSource = baseDataSource;
|
dataSource = baseDataSource;
|
||||||
|
@ -72,9 +72,12 @@ public class DefaultLoadErrorHandlingPolicy implements LoadErrorHandlingPolicy {
|
|||||||
IOException exception = loadErrorInfo.exception;
|
IOException exception = loadErrorInfo.exception;
|
||||||
if (exception instanceof InvalidResponseCodeException) {
|
if (exception instanceof InvalidResponseCodeException) {
|
||||||
int responseCode = ((InvalidResponseCodeException) exception).responseCode;
|
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 == 410 // HTTP 410 Gone.
|
||||||
|| responseCode == 416 // HTTP 416 Range Not Satisfiable.
|
|| responseCode == 416 // HTTP 416 Range Not Satisfiable.
|
||||||
|
|| responseCode == 500 // HTTP 500 Internal Server Error.
|
||||||
|
|| responseCode == 503 // HTTP 503 Service Unavailable.
|
||||||
? DEFAULT_TRACK_BLACKLIST_MS
|
? DEFAULT_TRACK_BLACKLIST_MS
|
||||||
: C.TIME_UNSET;
|
: C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream;
|
|||||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||||
import static java.lang.Math.min;
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.AssetFileDescriptor;
|
import android.content.res.AssetFileDescriptor;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
@ -34,9 +35,20 @@ import java.io.InputStream;
|
|||||||
/**
|
/**
|
||||||
* A {@link DataSource} for reading a raw resource inside the APK.
|
* 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
|
* <p>URIs supported by this source are of one of the forms:
|
||||||
* rawResourceId is the integer identifier of a raw resource. {@link #buildRawResourceUri(int)} can
|
*
|
||||||
* be used to build {@link Uri}s in this format.
|
* <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 {
|
public final class RawResourceDataSource extends BaseDataSource {
|
||||||
|
|
||||||
@ -67,6 +79,7 @@ public final class RawResourceDataSource extends BaseDataSource {
|
|||||||
public static final String RAW_RESOURCE_SCHEME = "rawresource";
|
public static final String RAW_RESOURCE_SCHEME = "rawresource";
|
||||||
|
|
||||||
private final Resources resources;
|
private final Resources resources;
|
||||||
|
private final String packageName;
|
||||||
|
|
||||||
@Nullable private Uri uri;
|
@Nullable private Uri uri;
|
||||||
@Nullable private AssetFileDescriptor assetFileDescriptor;
|
@Nullable private AssetFileDescriptor assetFileDescriptor;
|
||||||
@ -80,33 +93,55 @@ public final class RawResourceDataSource extends BaseDataSource {
|
|||||||
public RawResourceDataSource(Context context) {
|
public RawResourceDataSource(Context context) {
|
||||||
super(/* isNetwork= */ false);
|
super(/* isNetwork= */ false);
|
||||||
this.resources = context.getResources();
|
this.resources = context.getResources();
|
||||||
|
this.packageName = context.getPackageName();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long open(DataSpec dataSpec) throws RawResourceDataSourceException {
|
public long open(DataSpec dataSpec) throws RawResourceDataSourceException {
|
||||||
try {
|
Uri uri = dataSpec.uri;
|
||||||
Uri uri = dataSpec.uri;
|
this.uri = uri;
|
||||||
this.uri = uri;
|
|
||||||
if (!TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())) {
|
|
||||||
throw new RawResourceDataSourceException("URI must use scheme " + RAW_RESOURCE_SCHEME);
|
|
||||||
}
|
|
||||||
|
|
||||||
int resourceId;
|
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 {
|
try {
|
||||||
resourceId = Integer.parseInt(Assertions.checkNotNull(uri.getLastPathSegment()));
|
resourceId = Integer.parseInt(Assertions.checkNotNull(uri.getLastPathSegment()));
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
throw new RawResourceDataSourceException("Resource identifier must be an integer.");
|
throw new RawResourceDataSourceException("Resource identifier must be an integer.");
|
||||||
}
|
}
|
||||||
|
} else if (TextUtils.equals(ContentResolver.SCHEME_ANDROID_RESOURCE, uri.getScheme())) {
|
||||||
transferInitializing(dataSpec);
|
String path = Assertions.checkNotNull(uri.getPath());
|
||||||
AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId);
|
if (path.startsWith("/")) {
|
||||||
this.assetFileDescriptor = assetFileDescriptor;
|
path = path.substring(1);
|
||||||
if (assetFileDescriptor == null) {
|
|
||||||
throw new RawResourceDataSourceException("Resource is compressed: " + uri);
|
|
||||||
}
|
}
|
||||||
FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
|
@Nullable String host = uri.getHost();
|
||||||
this.inputStream = inputStream;
|
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);
|
||||||
|
this.assetFileDescriptor = assetFileDescriptor;
|
||||||
|
if (assetFileDescriptor == null) {
|
||||||
|
throw new RawResourceDataSourceException("Resource is compressed: " + uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
try {
|
||||||
inputStream.skip(assetFileDescriptor.getStartOffset());
|
inputStream.skip(assetFileDescriptor.getStartOffset());
|
||||||
long skipped = inputStream.skip(dataSpec.position);
|
long skipped = inputStream.skip(dataSpec.position);
|
||||||
if (skipped < dataSpec.position) {
|
if (skipped < dataSpec.position) {
|
||||||
@ -114,18 +149,21 @@ public final class RawResourceDataSource extends BaseDataSource {
|
|||||||
// skip beyond the end of the data.
|
// skip beyond the end of the data.
|
||||||
throw new EOFException();
|
throw new EOFException();
|
||||||
}
|
}
|
||||||
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) {
|
} catch (IOException e) {
|
||||||
throw new RawResourceDataSourceException(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);
|
||||||
|
}
|
||||||
|
|
||||||
opened = true;
|
opened = true;
|
||||||
transferStarted(dataSpec);
|
transferStarted(dataSpec);
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ import java.net.DatagramPacket;
|
|||||||
import java.net.DatagramSocket;
|
import java.net.DatagramSocket;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.ConcurrentModificationException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static utility to retrieve the device time offset using SNTP.
|
* Static utility to retrieve the device time offset using SNTP.
|
||||||
@ -37,6 +38,9 @@ import java.util.Arrays;
|
|||||||
*/
|
*/
|
||||||
public final class SntpClient {
|
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)}. */
|
/** Callback for calls to {@link #initialize(Loader, InitializationCallback)}. */
|
||||||
public interface InitializationCallback {
|
public interface InitializationCallback {
|
||||||
|
|
||||||
@ -51,7 +55,6 @@ public final class SntpClient {
|
|||||||
void onInitializationFailed(IOException error);
|
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 TIMEOUT_MS = 10_000;
|
||||||
|
|
||||||
private static final int ORIGINATE_TIME_OFFSET = 24;
|
private static final int ORIGINATE_TIME_OFFSET = 24;
|
||||||
@ -80,8 +83,37 @@ public final class SntpClient {
|
|||||||
@GuardedBy("valueLock")
|
@GuardedBy("valueLock")
|
||||||
private static long elapsedRealtimeOffsetMs;
|
private static long elapsedRealtimeOffsetMs;
|
||||||
|
|
||||||
|
@GuardedBy("valueLock")
|
||||||
|
private static String ntpHost = DEFAULT_NTP_HOST;
|
||||||
|
|
||||||
private SntpClient() {}
|
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.
|
* Returns whether the device time offset has already been loaded.
|
||||||
*
|
*
|
||||||
@ -129,7 +161,7 @@ public final class SntpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static long loadNtpTimeOffsetMs() throws IOException {
|
private static long loadNtpTimeOffsetMs() throws IOException {
|
||||||
InetAddress address = InetAddress.getByName(NTP_HOST);
|
InetAddress address = InetAddress.getByName(getNtpHost());
|
||||||
try (DatagramSocket socket = new DatagramSocket()) {
|
try (DatagramSocket socket = new DatagramSocket()) {
|
||||||
socket.setSoTimeout(TIMEOUT_MS);
|
socket.setSoTimeout(TIMEOUT_MS);
|
||||||
byte[] buffer = new byte[NTP_PACKET_SIZE];
|
byte[] buffer = new byte[NTP_PACKET_SIZE];
|
||||||
@ -282,9 +314,14 @@ public final class SntpClient {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadCompleted(Loadable loadable, long elapsedRealtimeMs, long loadDurationMs) {
|
public void onLoadCompleted(Loadable loadable, long elapsedRealtimeMs, long loadDurationMs) {
|
||||||
Assertions.checkState(SntpClient.isInitialized());
|
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onInitialized();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1551,180 +1551,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
}
|
}
|
||||||
synchronized (MediaCodecVideoRenderer.class) {
|
synchronized (MediaCodecVideoRenderer.class) {
|
||||||
if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) {
|
if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) {
|
||||||
if ("dangal".equals(Util.DEVICE)) {
|
deviceNeedsSetOutputSurfaceWorkaround = evaluateDeviceNeedsSetOutputSurfaceWorkaround();
|
||||||
// Workaround for MiTV devices:
|
|
||||||
// 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)) {
|
|
||||||
// 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:
|
|
||||||
// https://github.com/google/ExoPlayer/issues/3236,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/3355,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/3439,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/3724,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/3835,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/4006,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/4084,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/4104,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/4134,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/4315,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/4419,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/4460,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/4468,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/5312,
|
|
||||||
// https://github.com/google/ExoPlayer/issues/6503.
|
|
||||||
switch (Util.DEVICE) {
|
|
||||||
case "1601":
|
|
||||||
case "1713":
|
|
||||||
case "1714":
|
|
||||||
case "A10-70F":
|
|
||||||
case "A10-70L":
|
|
||||||
case "A1601":
|
|
||||||
case "A2016a40":
|
|
||||||
case "A7000-a":
|
|
||||||
case "A7000plus":
|
|
||||||
case "A7010a48":
|
|
||||||
case "A7020a48":
|
|
||||||
case "AquaPowerM":
|
|
||||||
case "ASUS_X00AD_2":
|
|
||||||
case "Aura_Note_2":
|
|
||||||
case "BLACK-1X":
|
|
||||||
case "BRAVIA_ATV2":
|
|
||||||
case "BRAVIA_ATV3_4K":
|
|
||||||
case "C1":
|
|
||||||
case "ComioS1":
|
|
||||||
case "CP8676_I02":
|
|
||||||
case "CPH1609":
|
|
||||||
case "CPY83_I00":
|
|
||||||
case "cv1":
|
|
||||||
case "cv3":
|
|
||||||
case "deb":
|
|
||||||
case "E5643":
|
|
||||||
case "ELUGA_A3_Pro":
|
|
||||||
case "ELUGA_Note":
|
|
||||||
case "ELUGA_Prim":
|
|
||||||
case "ELUGA_Ray_X":
|
|
||||||
case "EverStar_S":
|
|
||||||
case "F02H":
|
|
||||||
case "F03H":
|
|
||||||
case "F3111":
|
|
||||||
case "F3113":
|
|
||||||
case "F3116":
|
|
||||||
case "F3211":
|
|
||||||
case "F3213":
|
|
||||||
case "F3215":
|
|
||||||
case "F3311":
|
|
||||||
case "flo":
|
|
||||||
case "fugu":
|
|
||||||
case "GiONEE_CBL7513":
|
|
||||||
case "GiONEE_GBL7319":
|
|
||||||
case "GIONEE_GBL7360":
|
|
||||||
case "GIONEE_SWW1609":
|
|
||||||
case "GIONEE_SWW1627":
|
|
||||||
case "GIONEE_SWW1631":
|
|
||||||
case "GIONEE_WBL5708":
|
|
||||||
case "GIONEE_WBL7365":
|
|
||||||
case "GIONEE_WBL7519":
|
|
||||||
case "griffin":
|
|
||||||
case "htc_e56ml_dtul":
|
|
||||||
case "hwALE-H":
|
|
||||||
case "HWBLN-H":
|
|
||||||
case "HWCAM-H":
|
|
||||||
case "HWVNS-H":
|
|
||||||
case "HWWAS-H":
|
|
||||||
case "i9031":
|
|
||||||
case "iball8735_9806":
|
|
||||||
case "Infinix-X572":
|
|
||||||
case "iris60":
|
|
||||||
case "itel_S41":
|
|
||||||
case "j2xlteins":
|
|
||||||
case "JGZ":
|
|
||||||
case "K50a40":
|
|
||||||
case "kate":
|
|
||||||
case "l5460":
|
|
||||||
case "le_x6":
|
|
||||||
case "LS-5017":
|
|
||||||
case "M5c":
|
|
||||||
case "manning":
|
|
||||||
case "marino_f":
|
|
||||||
case "MEIZU_M5":
|
|
||||||
case "mh":
|
|
||||||
case "mido":
|
|
||||||
case "MX6":
|
|
||||||
case "namath":
|
|
||||||
case "nicklaus_f":
|
|
||||||
case "NX541J":
|
|
||||||
case "NX573J":
|
|
||||||
case "OnePlus5T":
|
|
||||||
case "p212":
|
|
||||||
case "P681":
|
|
||||||
case "P85":
|
|
||||||
case "panell_d":
|
|
||||||
case "panell_dl":
|
|
||||||
case "panell_ds":
|
|
||||||
case "panell_dt":
|
|
||||||
case "PB2-670M":
|
|
||||||
case "PGN528":
|
|
||||||
case "PGN610":
|
|
||||||
case "PGN611":
|
|
||||||
case "Phantom6":
|
|
||||||
case "Pixi4-7_3G":
|
|
||||||
case "Pixi5-10_4G":
|
|
||||||
case "PLE":
|
|
||||||
case "PRO7S":
|
|
||||||
case "Q350":
|
|
||||||
case "Q4260":
|
|
||||||
case "Q427":
|
|
||||||
case "Q4310":
|
|
||||||
case "Q5":
|
|
||||||
case "QM16XE_U":
|
|
||||||
case "QX1":
|
|
||||||
case "santoni":
|
|
||||||
case "Slate_Pro":
|
|
||||||
case "SVP-DTV15":
|
|
||||||
case "s905x018":
|
|
||||||
case "taido_row":
|
|
||||||
case "TB3-730F":
|
|
||||||
case "TB3-730X":
|
|
||||||
case "TB3-850F":
|
|
||||||
case "TB3-850M":
|
|
||||||
case "tcl_eu":
|
|
||||||
case "V1":
|
|
||||||
case "V23GB":
|
|
||||||
case "V5":
|
|
||||||
case "vernee_M5":
|
|
||||||
case "watson":
|
|
||||||
case "whyred":
|
|
||||||
case "woods_f":
|
|
||||||
case "woods_fn":
|
|
||||||
case "X3_HK":
|
|
||||||
case "XE2X":
|
|
||||||
case "XT1663":
|
|
||||||
case "Z12_PRO":
|
|
||||||
case "Z80":
|
|
||||||
deviceNeedsSetOutputSurfaceWorkaround = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Do nothing.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (Util.MODEL) {
|
|
||||||
case "AFTA":
|
|
||||||
case "AFTN":
|
|
||||||
case "JSN-L21":
|
|
||||||
deviceNeedsSetOutputSurfaceWorkaround = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Do nothing.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true;
|
evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1746,7 +1573,205 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||||||
this.height = height;
|
this.height = height;
|
||||||
this.inputSize = inputSize;
|
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.
|
||||||
|
// 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.
|
||||||
|
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,
|
||||||
|
// https://github.com/google/ExoPlayer/issues/3724,
|
||||||
|
// https://github.com/google/ExoPlayer/issues/3835,
|
||||||
|
// https://github.com/google/ExoPlayer/issues/4006,
|
||||||
|
// https://github.com/google/ExoPlayer/issues/4084,
|
||||||
|
// https://github.com/google/ExoPlayer/issues/4104,
|
||||||
|
// https://github.com/google/ExoPlayer/issues/4134,
|
||||||
|
// https://github.com/google/ExoPlayer/issues/4315,
|
||||||
|
// https://github.com/google/ExoPlayer/issues/4419,
|
||||||
|
// https://github.com/google/ExoPlayer/issues/4460,
|
||||||
|
// 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":
|
||||||
|
case "A2016a40":
|
||||||
|
case "A7000-a":
|
||||||
|
case "A7000plus":
|
||||||
|
case "A7010a48":
|
||||||
|
case "A7020a48":
|
||||||
|
case "AquaPowerM":
|
||||||
|
case "ASUS_X00AD_2":
|
||||||
|
case "Aura_Note_2":
|
||||||
|
case "b5":
|
||||||
|
case "BLACK-1X":
|
||||||
|
case "BRAVIA_ATV2":
|
||||||
|
case "BRAVIA_ATV3_4K":
|
||||||
|
case "C1":
|
||||||
|
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":
|
||||||
|
case "F3211":
|
||||||
|
case "F3213":
|
||||||
|
case "F3215":
|
||||||
|
case "F3311":
|
||||||
|
case "flo":
|
||||||
|
case "fugu":
|
||||||
|
case "GiONEE_CBL7513":
|
||||||
|
case "GiONEE_GBL7319":
|
||||||
|
case "GIONEE_GBL7360":
|
||||||
|
case "GIONEE_SWW1609":
|
||||||
|
case "GIONEE_SWW1627":
|
||||||
|
case "GIONEE_SWW1631":
|
||||||
|
case "GIONEE_WBL5708":
|
||||||
|
case "GIONEE_WBL7365":
|
||||||
|
case "GIONEE_WBL7519":
|
||||||
|
case "griffin":
|
||||||
|
case "htc_e56ml_dtul":
|
||||||
|
case "hwALE-H":
|
||||||
|
case "HWBLN-H":
|
||||||
|
case "HWCAM-H":
|
||||||
|
case "HWVNS-H":
|
||||||
|
case "HWWAS-H":
|
||||||
|
case "i9031":
|
||||||
|
case "iball8735_9806":
|
||||||
|
case "Infinix-X572":
|
||||||
|
case "iris60":
|
||||||
|
case "itel_S41":
|
||||||
|
case "j2xlteins":
|
||||||
|
case "JGZ":
|
||||||
|
case "K50a40":
|
||||||
|
case "kate":
|
||||||
|
case "l5460":
|
||||||
|
case "le_x6":
|
||||||
|
case "LS-5017":
|
||||||
|
case "M04":
|
||||||
|
case "M5c":
|
||||||
|
case "manning":
|
||||||
|
case "marino_f":
|
||||||
|
case "MEIZU_M5":
|
||||||
|
case "mh":
|
||||||
|
case "mido":
|
||||||
|
case "MX6":
|
||||||
|
case "namath":
|
||||||
|
case "nicklaus_f":
|
||||||
|
case "NX541J":
|
||||||
|
case "NX573J":
|
||||||
|
case "OnePlus5T":
|
||||||
|
case "p212":
|
||||||
|
case "P681":
|
||||||
|
case "P85":
|
||||||
|
case "pacificrim":
|
||||||
|
case "panell_d":
|
||||||
|
case "panell_dl":
|
||||||
|
case "panell_ds":
|
||||||
|
case "panell_dt":
|
||||||
|
case "PB2-670M":
|
||||||
|
case "PGN528":
|
||||||
|
case "PGN610":
|
||||||
|
case "PGN611":
|
||||||
|
case "Phantom6":
|
||||||
|
case "Pixi4-7_3G":
|
||||||
|
case "Pixi5-10_4G":
|
||||||
|
case "PLE":
|
||||||
|
case "PRO7S":
|
||||||
|
case "Q350":
|
||||||
|
case "Q4260":
|
||||||
|
case "Q427":
|
||||||
|
case "Q4310":
|
||||||
|
case "Q5":
|
||||||
|
case "QM16XE_U":
|
||||||
|
case "QX1":
|
||||||
|
case "RAIJIN":
|
||||||
|
case "santoni":
|
||||||
|
case "Slate_Pro":
|
||||||
|
case "SVP-DTV15":
|
||||||
|
case "s905x018":
|
||||||
|
case "taido_row":
|
||||||
|
case "TB3-730F":
|
||||||
|
case "TB3-730X":
|
||||||
|
case "TB3-850F":
|
||||||
|
case "TB3-850M":
|
||||||
|
case "tcl_eu":
|
||||||
|
case "V1":
|
||||||
|
case "V23GB":
|
||||||
|
case "V5":
|
||||||
|
case "vernee_M5":
|
||||||
|
case "watson":
|
||||||
|
case "whyred":
|
||||||
|
case "woods_f":
|
||||||
|
case "woods_fn":
|
||||||
|
case "X3_HK":
|
||||||
|
case "XE2X":
|
||||||
|
case "XT1663":
|
||||||
|
case "Z12_PRO":
|
||||||
|
case "Z80":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
break; // Do nothing.
|
||||||
|
}
|
||||||
|
switch (Util.MODEL) {
|
||||||
|
case "AFTA":
|
||||||
|
case "AFTN":
|
||||||
|
case "JSN-L21":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
break; // Do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(23)
|
@RequiresApi(23)
|
||||||
|
@ -58,6 +58,7 @@ import com.google.android.exoplayer2.drm.DrmSessionManager;
|
|||||||
import com.google.android.exoplayer2.source.ClippingMediaSource;
|
import com.google.android.exoplayer2.source.ClippingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.CompositeMediaSource;
|
import com.google.android.exoplayer2.source.CompositeMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
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.LoopingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MaskingMediaSource;
|
import com.google.android.exoplayer2.source.MaskingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
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.trackselection.TrackSelectionArray;
|
||||||
import com.google.android.exoplayer2.upstream.Allocation;
|
import com.google.android.exoplayer2.upstream.Allocation;
|
||||||
import com.google.android.exoplayer2.upstream.Allocator;
|
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.Loader;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Clock;
|
import com.google.android.exoplayer2.util.Clock;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -5553,7 +5555,8 @@ public final class ExoPlayerTest {
|
|||||||
AdsMediaSource adsMediaSource =
|
AdsMediaSource adsMediaSource =
|
||||||
new AdsMediaSource(
|
new AdsMediaSource(
|
||||||
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)),
|
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)),
|
||||||
new DefaultDataSourceFactory(context),
|
/* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
|
||||||
|
new DefaultMediaSourceFactory(context),
|
||||||
new FakeAdsLoader(),
|
new FakeAdsLoader(),
|
||||||
new FakeAdViewProvider());
|
new FakeAdViewProvider());
|
||||||
Exception[] exception = {null};
|
Exception[] exception = {null};
|
||||||
@ -5590,7 +5593,8 @@ public final class ExoPlayerTest {
|
|||||||
AdsMediaSource adsMediaSource =
|
AdsMediaSource adsMediaSource =
|
||||||
new AdsMediaSource(
|
new AdsMediaSource(
|
||||||
mediaSource,
|
mediaSource,
|
||||||
new DefaultDataSourceFactory(context),
|
/* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
|
||||||
|
new DefaultMediaSourceFactory(context),
|
||||||
new FakeAdsLoader(),
|
new FakeAdsLoader(),
|
||||||
new FakeAdViewProvider());
|
new FakeAdViewProvider());
|
||||||
final Exception[] exception = {null};
|
final Exception[] exception = {null};
|
||||||
@ -5629,7 +5633,8 @@ public final class ExoPlayerTest {
|
|||||||
AdsMediaSource adsMediaSource =
|
AdsMediaSource adsMediaSource =
|
||||||
new AdsMediaSource(
|
new AdsMediaSource(
|
||||||
mediaSource,
|
mediaSource,
|
||||||
new DefaultDataSourceFactory(context),
|
/* adTagDataSpec= */ new DataSpec(Uri.EMPTY),
|
||||||
|
new DefaultMediaSourceFactory(context),
|
||||||
new FakeAdsLoader(),
|
new FakeAdsLoader(),
|
||||||
new FakeAdViewProvider());
|
new FakeAdViewProvider());
|
||||||
final Exception[] exception = {null};
|
final Exception[] exception = {null};
|
||||||
@ -5699,6 +5704,42 @@ public final class ExoPlayerTest {
|
|||||||
assertArrayEquals(new int[] {0, 0, 0}, currentWindowIndices);
|
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
|
@Test
|
||||||
public void setMediaSources_empty_whenEmpty_validInitialSeek_correctMaskingWindowIndex()
|
public void setMediaSources_empty_whenEmpty_validInitialSeek_correctMaskingWindowIndex()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
@ -7334,6 +7375,8 @@ public final class ExoPlayerTest {
|
|||||||
new DefaultLoadControl.Builder()
|
new DefaultLoadControl.Builder()
|
||||||
.setTargetBufferBytes(10 * C.DEFAULT_BUFFER_SEGMENT_SIZE)
|
.setTargetBufferBytes(10 * C.DEFAULT_BUFFER_SEGMENT_SIZE)
|
||||||
.build();
|
.build();
|
||||||
|
// Return no end of stream signal to prevent playback from ending.
|
||||||
|
FakeMediaPeriod.TrackDataFactory trackDataWithoutEos = (format, periodId) -> ImmutableList.of();
|
||||||
MediaSource continuouslyAllocatingMediaSource =
|
MediaSource continuouslyAllocatingMediaSource =
|
||||||
new FakeMediaSource(
|
new FakeMediaSource(
|
||||||
new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT) {
|
new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT) {
|
||||||
@ -7348,8 +7391,11 @@ public final class ExoPlayerTest {
|
|||||||
@Nullable TransferListener transferListener) {
|
@Nullable TransferListener transferListener) {
|
||||||
return new FakeMediaPeriod(
|
return new FakeMediaPeriod(
|
||||||
trackGroupArray,
|
trackGroupArray,
|
||||||
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
|
trackDataWithoutEos,
|
||||||
mediaSourceEventDispatcher) {
|
mediaSourceEventDispatcher,
|
||||||
|
drmSessionManager,
|
||||||
|
drmEventDispatcher,
|
||||||
|
/* deferOnPrepared= */ false) {
|
||||||
|
|
||||||
private final List<Allocation> allocations = new ArrayList<>();
|
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 =
|
ExoPlayerTestRunner testRunner =
|
||||||
new ExoPlayerTestRunner.Builder(context)
|
new ExoPlayerTestRunner.Builder(context)
|
||||||
.setActionSchedule(actionSchedule)
|
|
||||||
.setMediaSources(continuouslyAllocatingMediaSource)
|
.setMediaSources(continuouslyAllocatingMediaSource)
|
||||||
.setLoadControl(loadControl)
|
.setLoadControl(loadControl)
|
||||||
.build();
|
.build();
|
||||||
@ -8512,6 +8552,9 @@ public final class ExoPlayerTest {
|
|||||||
@Override
|
@Override
|
||||||
public void setSupportedContentTypes(int... contentTypes) {}
|
public void setSupportedContentTypes(int... contentTypes) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAdTagDataSpec(DataSpec adTagDataSpec) {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(AdsLoader.EventListener eventListener, AdViewProvider adViewProvider) {}
|
public void start(AdsLoader.EventListener eventListener, AdViewProvider adViewProvider) {}
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.e2etest.util.PlaybackOutput;
|
import com.google.android.exoplayer2.robolectric.PlaybackOutput;
|
||||||
import com.google.android.exoplayer2.e2etest.util.ShadowMediaCodecConfig;
|
import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
|
||||||
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
|
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
|
||||||
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
|
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
|
||||||
import com.google.android.exoplayer2.testutil.TestExoPlayer;
|
import com.google.android.exoplayer2.testutil.TestExoPlayer;
|
||||||
|
@ -22,8 +22,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.e2etest.util.PlaybackOutput;
|
import com.google.android.exoplayer2.robolectric.PlaybackOutput;
|
||||||
import com.google.android.exoplayer2.e2etest.util.ShadowMediaCodecConfig;
|
import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
|
||||||
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
|
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
|
||||||
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
|
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
|
||||||
import com.google.android.exoplayer2.testutil.TestExoPlayer;
|
import com.google.android.exoplayer2.testutil.TestExoPlayer;
|
||||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.trackselection;
|
|||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.RendererCapabilities;
|
import com.google.android.exoplayer2.RendererCapabilities;
|
||||||
@ -52,7 +53,7 @@ public class TrackSelectorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSelectionActivated(Object info) {}
|
public void onSelectionActivated(@Nullable Object info) {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ import static org.junit.Assert.fail;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import org.junit.Before;
|
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) {
|
private static DataSpec buildDataSpec(String uriString) {
|
||||||
return buildDataSpec(uriString, /* position= */ 0, /* length= */ C.LENGTH_UNSET);
|
return buildDataSpec(uriString, /* position= */ 0, /* length= */ C.LENGTH_UNSET);
|
||||||
}
|
}
|
||||||
@ -167,7 +174,7 @@ public final class DataSchemeDataSourceTest {
|
|||||||
try {
|
try {
|
||||||
long length = dataSource.open(dataSpec);
|
long length = dataSource.open(dataSpec);
|
||||||
assertThat(length).isEqualTo(expectedData.length);
|
assertThat(length).isEqualTo(expectedData.length);
|
||||||
byte[] readData = TestUtil.readToEnd(dataSource);
|
byte[] readData = Util.readToEnd(dataSource);
|
||||||
assertThat(readData).isEqualTo(expectedData);
|
assertThat(readData).isEqualTo(expectedData);
|
||||||
} finally {
|
} finally {
|
||||||
dataSource.close();
|
dataSource.close();
|
||||||
|
@ -107,7 +107,7 @@ public final class DefaultBandwidthMeterTest {
|
|||||||
/* isAvailable= */ true,
|
/* isAvailable= */ true,
|
||||||
CONNECTED);
|
CONNECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void defaultInitialBitrateEstimate_forWifi_isGreaterThanEstimateFor2G() {
|
public void defaultInitialBitrateEstimate_forWifi_isGreaterThanEstimateFor2G() {
|
||||||
setActiveNetworkInfo(networkInfoWifi);
|
setActiveNetworkInfo(networkInfoWifi);
|
||||||
|
@ -47,41 +47,46 @@ public final class DefaultLoadErrorHandlingPolicyTest {
|
|||||||
private static final MediaLoadData PLACEHOLDER_MEDIA_LOAD_DATA =
|
private static final MediaLoadData PLACEHOLDER_MEDIA_LOAD_DATA =
|
||||||
new MediaLoadData(/* dataType= */ C.DATA_TYPE_UNKNOWN);
|
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
|
@Test
|
||||||
public void getExclusionDurationMsFor_responseCode404() {
|
public void getExclusionDurationMsFor_responseCode404() {
|
||||||
InvalidResponseCodeException exception =
|
InvalidResponseCodeException exception = buildInvalidResponseCodeException(404, "Not found");
|
||||||
new InvalidResponseCodeException(
|
|
||||||
404,
|
|
||||||
"Not Found",
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DataSpec(Uri.EMPTY),
|
|
||||||
/* responseBody= */ Util.EMPTY_BYTE_ARRAY);
|
|
||||||
assertThat(getDefaultPolicyExclusionDurationMsFor(exception))
|
assertThat(getDefaultPolicyExclusionDurationMsFor(exception))
|
||||||
.isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS);
|
.isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getExclusionDurationMsFor_responseCode410() {
|
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 =
|
InvalidResponseCodeException exception =
|
||||||
new InvalidResponseCodeException(
|
buildInvalidResponseCodeException(500, "Internal server error");
|
||||||
410,
|
assertThat(getDefaultPolicyExclusionDurationMsFor(exception))
|
||||||
"Gone",
|
.isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS);
|
||||||
Collections.emptyMap(),
|
}
|
||||||
new DataSpec(Uri.EMPTY),
|
|
||||||
/* responseBody= */ Util.EMPTY_BYTE_ARRAY);
|
@Test
|
||||||
|
public void getExclusionDurationMsFor_responseCode503() {
|
||||||
|
InvalidResponseCodeException exception =
|
||||||
|
buildInvalidResponseCodeException(503, "Service unavailable");
|
||||||
assertThat(getDefaultPolicyExclusionDurationMsFor(exception))
|
assertThat(getDefaultPolicyExclusionDurationMsFor(exception))
|
||||||
.isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS);
|
.isEqualTo(DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_BLACKLIST_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getExclusionDurationMsFor_dontExcludeUnexpectedHttpCodes() {
|
public void getExclusionDurationMsFor_dontExcludeUnexpectedHttpCodes() {
|
||||||
InvalidResponseCodeException exception =
|
InvalidResponseCodeException exception = buildInvalidResponseCodeException(418, "I'm a teapot");
|
||||||
new InvalidResponseCodeException(
|
|
||||||
500,
|
|
||||||
"Internal Server Error",
|
|
||||||
Collections.emptyMap(),
|
|
||||||
new DataSpec(Uri.EMPTY),
|
|
||||||
/* responseBody= */ Util.EMPTY_BYTE_ARRAY);
|
|
||||||
assertThat(getDefaultPolicyExclusionDurationMsFor(exception)).isEqualTo(C.TIME_UNSET);
|
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);
|
PLACEHOLDER_LOAD_EVENT_INFO, PLACEHOLDER_MEDIA_LOAD_DATA, exception, errorCount);
|
||||||
return new DefaultLoadErrorHandlingPolicy().getRetryDelayMsFor(loadErrorInfo);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -302,7 +302,7 @@ public final class CacheDataSourceTest {
|
|||||||
CacheDataSource cacheDataSource = new CacheDataSource(cache, upstream, 0);
|
CacheDataSource cacheDataSource = new CacheDataSource(cache, upstream, 0);
|
||||||
|
|
||||||
cacheDataSource.open(unboundedDataSpec);
|
cacheDataSource.open(unboundedDataSpec);
|
||||||
TestUtil.readToEnd(cacheDataSource);
|
Util.readToEnd(cacheDataSource);
|
||||||
cacheDataSource.close();
|
cacheDataSource.close();
|
||||||
|
|
||||||
assertThat(upstream.getAndClearOpenedDataSpecs()).hasLength(1);
|
assertThat(upstream.getAndClearOpenedDataSpecs()).hasLength(1);
|
||||||
@ -319,7 +319,7 @@ public final class CacheDataSourceTest {
|
|||||||
cache, upstream, CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);
|
cache, upstream, CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);
|
||||||
|
|
||||||
cacheDataSource.open(unboundedDataSpec);
|
cacheDataSource.open(unboundedDataSpec);
|
||||||
TestUtil.readToEnd(cacheDataSource);
|
Util.readToEnd(cacheDataSource);
|
||||||
cacheDataSource.close();
|
cacheDataSource.close();
|
||||||
|
|
||||||
assertThat(cache.getKeys()).isEmpty();
|
assertThat(cache.getKeys()).isEmpty();
|
||||||
@ -369,7 +369,7 @@ public final class CacheDataSourceTest {
|
|||||||
cacheWriter.cache();
|
cacheWriter.cache();
|
||||||
|
|
||||||
// Read the rest of the data.
|
// Read the rest of the data.
|
||||||
TestUtil.readToEnd(cacheDataSource);
|
Util.readToEnd(cacheDataSource);
|
||||||
cacheDataSource.close();
|
cacheDataSource.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,7 +419,7 @@ public final class CacheDataSourceTest {
|
|||||||
cacheWriter.cache();
|
cacheWriter.cache();
|
||||||
|
|
||||||
// Read the rest of the data.
|
// Read the rest of the data.
|
||||||
TestUtil.readToEnd(cacheDataSource);
|
Util.readToEnd(cacheDataSource);
|
||||||
cacheDataSource.close();
|
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.
|
// Open source and read some data from upstream as the data hasn't cached yet.
|
||||||
cacheDataSource.open(unboundedDataSpec);
|
cacheDataSource.open(unboundedDataSpec);
|
||||||
TestUtil.readExactly(cacheDataSource, 100);
|
Util.readExactly(cacheDataSource, 100);
|
||||||
|
|
||||||
// Delete cached data.
|
// Delete cached data.
|
||||||
cache.removeResource(cacheDataSource.getCacheKeyFactory().buildCacheKey(unboundedDataSpec));
|
cache.removeResource(cacheDataSource.getCacheKeyFactory().buildCacheKey(unboundedDataSpec));
|
||||||
assertCacheEmpty(cache);
|
assertCacheEmpty(cache);
|
||||||
|
|
||||||
// Read the rest of the data.
|
// Read the rest of the data.
|
||||||
TestUtil.readToEnd(cacheDataSource);
|
Util.readToEnd(cacheDataSource);
|
||||||
cacheDataSource.close();
|
cacheDataSource.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -487,7 +487,7 @@ public final class CacheDataSourceTest {
|
|||||||
cacheDataSource.open(unboundedDataSpec);
|
cacheDataSource.open(unboundedDataSpec);
|
||||||
|
|
||||||
// Read the first half from upstream as it hasn't cached yet.
|
// 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.
|
// Delete the cached latter half.
|
||||||
NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(defaultCacheKey);
|
NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(defaultCacheKey);
|
||||||
@ -498,7 +498,7 @@ public final class CacheDataSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read the rest of the data.
|
// Read the rest of the data.
|
||||||
TestUtil.readToEnd(cacheDataSource);
|
Util.readToEnd(cacheDataSource);
|
||||||
cacheDataSource.close();
|
cacheDataSource.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,6 @@ import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
|||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
|
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
|
||||||
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
|
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;
|
||||||
import com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem;
|
import com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
@ -107,8 +106,7 @@ public class MediaCodecVideoRendererTest {
|
|||||||
/* maxDroppedFramesToNotify= */ 1) {
|
/* maxDroppedFramesToNotify= */ 1) {
|
||||||
@Override
|
@Override
|
||||||
@Capabilities
|
@Capabilities
|
||||||
protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format)
|
protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) {
|
||||||
throws DecoderQueryException {
|
|
||||||
return RendererCapabilities.create(FORMAT_HANDLED);
|
return RendererCapabilities.create(FORMAT_HANDLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,17 +25,11 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(modulePrefix + 'library-core')
|
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-qual:' + checkerframeworkVersion
|
||||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
|
testImplementation project(modulePrefix + 'robolectricutils')
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,16 @@ public class DashManifestParser extends DefaultHandler
|
|||||||
private static final Pattern CEA_708_ACCESSIBILITY_PATTERN =
|
private static final Pattern CEA_708_ACCESSIBILITY_PATTERN =
|
||||||
Pattern.compile("([1-9]|[1-5][0-9]|6[0-3])=.*");
|
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;
|
private final XmlPullParserFactory xmlParserFactory;
|
||||||
|
|
||||||
public DashManifestParser() {
|
public DashManifestParser() {
|
||||||
@ -1156,13 +1166,22 @@ public class DashManifestParser extends DefaultHandler
|
|||||||
protected int parseAudioChannelConfiguration(XmlPullParser xpp)
|
protected int parseAudioChannelConfiguration(XmlPullParser xpp)
|
||||||
throws XmlPullParserException, IOException {
|
throws XmlPullParserException, IOException {
|
||||||
String schemeIdUri = parseString(xpp, "schemeIdUri", null);
|
String schemeIdUri = parseString(xpp, "schemeIdUri", null);
|
||||||
int audioChannels =
|
int audioChannels;
|
||||||
"urn:mpeg:dash:23003:3:audio_channel_configuration:2011".equals(schemeIdUri)
|
switch (schemeIdUri) {
|
||||||
? parseInt(xpp, "value", Format.NO_VALUE)
|
case "urn:mpeg:dash:23003:3:audio_channel_configuration:2011":
|
||||||
: ("tag:dolby.com,2014:dash:audio_channel_configuration:2011".equals(schemeIdUri)
|
audioChannels = parseInt(xpp, "value", Format.NO_VALUE);
|
||||||
|| "urn:dolby:dash:audio_channel_configuration:2011".equals(schemeIdUri)
|
break;
|
||||||
? parseDolbyChannelConfiguration(xpp)
|
case "urn:mpeg:mpegB:cicp:ChannelConfiguration":
|
||||||
: Format.NO_VALUE);
|
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 {
|
do {
|
||||||
xpp.next();
|
xpp.next();
|
||||||
} while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration"));
|
} while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration"));
|
||||||
@ -1344,7 +1363,8 @@ public class DashManifestParser extends DefaultHandler
|
|||||||
// All other text types are raw formats.
|
// All other text types are raw formats.
|
||||||
return containerMimeType;
|
return containerMimeType;
|
||||||
} else if (MimeTypes.APPLICATION_MP4.equals(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;
|
return null;
|
||||||
}
|
}
|
||||||
@ -1528,6 +1548,21 @@ public class DashManifestParser extends DefaultHandler
|
|||||||
return value == null ? defaultValue : value;
|
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
|
* 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
|
* schemeIdUri "tag:dolby.com,2014:dash:audio_channel_configuration:2011", as defined by table E.5
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -26,13 +26,6 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
implementation project(modulePrefix + 'library-common')
|
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-qual:' + checkerframeworkVersion
|
||||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
|
@ -131,9 +131,11 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
|
|||||||
@Mp3Extractor.Flags private int mp3Flags;
|
@Mp3Extractor.Flags private int mp3Flags;
|
||||||
@TsExtractor.Mode private int tsMode;
|
@TsExtractor.Mode private int tsMode;
|
||||||
@DefaultTsPayloadReaderFactory.Flags private int tsFlags;
|
@DefaultTsPayloadReaderFactory.Flags private int tsFlags;
|
||||||
|
private int tsTimestampSearchBytes;
|
||||||
|
|
||||||
public DefaultExtractorsFactory() {
|
public DefaultExtractorsFactory() {
|
||||||
tsMode = TsExtractor.MODE_SINGLE_PMT;
|
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.
|
* 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.
|
* @param mode The mode to use.
|
||||||
* @return The factory, for convenience.
|
* @return The factory, for convenience.
|
||||||
*/
|
*/
|
||||||
@ -269,6 +271,20 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
|
|||||||
return this;
|
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
|
@Override
|
||||||
public synchronized Extractor[] createExtractors() {
|
public synchronized Extractor[] createExtractors() {
|
||||||
return createExtractors(Uri.EMPTY, new HashMap<>());
|
return createExtractors(Uri.EMPTY, new HashMap<>());
|
||||||
@ -361,7 +377,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
|
|||||||
extractors.add(new PsExtractor());
|
extractors.add(new PsExtractor());
|
||||||
break;
|
break;
|
||||||
case FileTypes.TS:
|
case FileTypes.TS:
|
||||||
extractors.add(new TsExtractor(tsMode, tsFlags));
|
extractors.add(new TsExtractor(tsMode, tsFlags, tsTimestampSearchBytes));
|
||||||
break;
|
break;
|
||||||
case FileTypes.WAV:
|
case FileTypes.WAV:
|
||||||
extractors.add(new WavExtractor());
|
extractors.add(new WavExtractor());
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,7 @@ import com.google.android.exoplayer2.extractor.Extractor;
|
|||||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
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.PositionHolder;
|
||||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
@ -135,8 +136,12 @@ public final class FlvExtractor implements Extractor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void seek(long position, long timeUs) {
|
public void seek(long position, long timeUs) {
|
||||||
state = STATE_READING_FLV_HEADER;
|
if (position == 0) {
|
||||||
outputFirstSample = false;
|
state = STATE_READING_FLV_HEADER;
|
||||||
|
outputFirstSample = false;
|
||||||
|
} else {
|
||||||
|
state = STATE_READING_TAG_HEADER;
|
||||||
|
}
|
||||||
bytesToNextTagHeader = 0;
|
bytesToNextTagHeader = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,7 +272,11 @@ public final class FlvExtractor implements Extractor {
|
|||||||
wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs);
|
wasSampleOutput = metadataReader.consume(prepareTagData(input), timestampUs);
|
||||||
long durationUs = metadataReader.getDurationUs();
|
long durationUs = metadataReader.getDurationUs();
|
||||||
if (durationUs != C.TIME_UNSET) {
|
if (durationUs != C.TIME_UNSET) {
|
||||||
extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
|
extractorOutput.seekMap(
|
||||||
|
new IndexSeekMap(
|
||||||
|
metadataReader.getKeyFrameTagPositions(),
|
||||||
|
metadataReader.getKeyFrameTimesUs(),
|
||||||
|
durationUs));
|
||||||
outputSeekMap = true;
|
outputSeekMap = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -22,6 +22,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,6 +32,9 @@ import java.util.Map;
|
|||||||
|
|
||||||
private static final String NAME_METADATA = "onMetaData";
|
private static final String NAME_METADATA = "onMetaData";
|
||||||
private static final String KEY_DURATION = "duration";
|
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
|
// AMF object types
|
||||||
private static final int AMF_TYPE_NUMBER = 0;
|
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 static final int AMF_TYPE_DATE = 11;
|
||||||
|
|
||||||
private long durationUs;
|
private long durationUs;
|
||||||
|
private long[] keyFrameTimesUs;
|
||||||
|
private long[] keyFrameTagPositions;
|
||||||
|
|
||||||
public ScriptTagPayloadReader() {
|
public ScriptTagPayloadReader() {
|
||||||
super(new DummyTrackOutput());
|
super(new DummyTrackOutput());
|
||||||
durationUs = C.TIME_UNSET;
|
durationUs = C.TIME_UNSET;
|
||||||
|
keyFrameTimesUs = new long[0];
|
||||||
|
keyFrameTagPositions = new long[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getDurationUs() {
|
public long getDurationUs() {
|
||||||
return durationUs;
|
return durationUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long[] getKeyFrameTimesUs() {
|
||||||
|
return keyFrameTimesUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long[] getKeyFrameTagPositions() {
|
||||||
|
return keyFrameTagPositions;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void seek() {
|
public void seek() {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
@ -80,14 +96,41 @@ import java.util.Map;
|
|||||||
// We're not interested in this metadata.
|
// We're not interested in this metadata.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Set the duration to the value contained in the metadata, if present.
|
|
||||||
Map<String, Object> metadata = readAmfEcmaArray(data);
|
Map<String, Object> metadata = readAmfEcmaArray(data);
|
||||||
if (metadata.containsKey(KEY_DURATION)) {
|
// Set the duration to the value contained in the metadata, if present.
|
||||||
double durationSeconds = (double) metadata.get(KEY_DURATION);
|
@Nullable Object durationSecondsObj = metadata.get(KEY_DURATION);
|
||||||
|
if (durationSecondsObj instanceof Double) {
|
||||||
|
double durationSeconds = (double) durationSecondsObj;
|
||||||
if (durationSeconds > 0.0) {
|
if (durationSeconds > 0.0) {
|
||||||
durationUs = (long) (durationSeconds * C.MICROS_PER_SECOND);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
int targetIndex =
|
int targetIndex =
|
||||||
Util.binarySearchFloor(timesUs, timeUs, /* inclusive= */ true, /* stayInBounds= */ true);
|
Util.binarySearchFloor(timesUs, timeUs, /* inclusive= */ true, /* stayInBounds= */ true);
|
||||||
SeekPoint seekPoint = new SeekPoint(timesUs.get(targetIndex), positions.get(targetIndex));
|
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);
|
return new SeekPoints(seekPoint);
|
||||||
} else {
|
} else {
|
||||||
SeekPoint nextSeekPoint =
|
SeekPoint nextSeekPoint =
|
||||||
|
@ -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 firstFramePosition The position of the start of the first frame in the stream.
|
||||||
* @param mlltFrame The MLLT frame with seeking metadata.
|
* @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.
|
* @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;
|
int referenceCount = mlltFrame.bytesDeviations.length;
|
||||||
long[] referencePositions = new long[1 + referenceCount];
|
long[] referencePositions = new long[1 + referenceCount];
|
||||||
long[] referenceTimesMs = new long[1 + referenceCount];
|
long[] referenceTimesMs = new long[1 + referenceCount];
|
||||||
@ -45,19 +47,22 @@ import com.google.android.exoplayer2.util.Util;
|
|||||||
referencePositions[i] = position;
|
referencePositions[i] = position;
|
||||||
referenceTimesMs[i] = timeMs;
|
referenceTimesMs[i] = timeMs;
|
||||||
}
|
}
|
||||||
return new MlltSeeker(referencePositions, referenceTimesMs);
|
return new MlltSeeker(referencePositions, referenceTimesMs, durationUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final long[] referencePositions;
|
private final long[] referencePositions;
|
||||||
private final long[] referenceTimesMs;
|
private final long[] referenceTimesMs;
|
||||||
private final long durationUs;
|
private final long durationUs;
|
||||||
|
|
||||||
private MlltSeeker(long[] referencePositions, long[] referenceTimesMs) {
|
private MlltSeeker(long[] referencePositions, long[] referenceTimesMs, long durationUs) {
|
||||||
this.referencePositions = referencePositions;
|
this.referencePositions = referencePositions;
|
||||||
this.referenceTimesMs = referenceTimesMs;
|
this.referenceTimesMs = referenceTimesMs;
|
||||||
// Use the last reference point as the duration, as extrapolating variable bitrate at the end of
|
// Use the last reference point as the duration if it is unknown, as extrapolating variable
|
||||||
// the stream may give a large error.
|
// bitrate at the end of the stream may give a large error.
|
||||||
durationUs = C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]);
|
this.durationUs =
|
||||||
|
durationUs != C.TIME_UNSET
|
||||||
|
? durationUs
|
||||||
|
: C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -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;
|
||||||
import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
|
import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
|
||||||
import com.google.android.exoplayer2.metadata.id3.MlltFrame;
|
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.Assertions;
|
||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
@ -432,7 +433,7 @@ public final class Mp3Extractor implements Extractor {
|
|||||||
|
|
||||||
@Nullable Seeker resultSeeker = null;
|
@Nullable Seeker resultSeeker = null;
|
||||||
if ((flags & FLAG_ENABLE_INDEX_SEEKING) != 0) {
|
if ((flags & FLAG_ENABLE_INDEX_SEEKING) != 0) {
|
||||||
long durationUs = C.TIME_UNSET;
|
long durationUs;
|
||||||
long dataEndPosition = C.POSITION_UNSET;
|
long dataEndPosition = C.POSITION_UNSET;
|
||||||
if (metadataSeeker != null) {
|
if (metadataSeeker != null) {
|
||||||
durationUs = metadataSeeker.getDurationUs();
|
durationUs = metadataSeeker.getDurationUs();
|
||||||
@ -440,6 +441,8 @@ public final class Mp3Extractor implements Extractor {
|
|||||||
} else if (seekFrameSeeker != null) {
|
} else if (seekFrameSeeker != null) {
|
||||||
durationUs = seekFrameSeeker.getDurationUs();
|
durationUs = seekFrameSeeker.getDurationUs();
|
||||||
dataEndPosition = seekFrameSeeker.getDataEndPosition();
|
dataEndPosition = seekFrameSeeker.getDataEndPosition();
|
||||||
|
} else {
|
||||||
|
durationUs = getId3TlenUs(metadata);
|
||||||
}
|
}
|
||||||
resultSeeker =
|
resultSeeker =
|
||||||
new IndexSeeker(
|
new IndexSeeker(
|
||||||
@ -554,10 +557,24 @@ public final class Mp3Extractor implements Extractor {
|
|||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
Metadata.Entry entry = metadata.get(i);
|
Metadata.Entry entry = metadata.get(i);
|
||||||
if (entry instanceof MlltFrame) {
|
if (entry instanceof MlltFrame) {
|
||||||
return MlltSeeker.create(firstFramePosition, (MlltFrame) entry);
|
return MlltSeeker.create(firstFramePosition, (MlltFrame) entry, getId3TlenUs(metadata));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,6 +115,9 @@ import java.util.List;
|
|||||||
@SuppressWarnings("ConstantCaseForConstants")
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
public static final int TYPE_mp4a = 0x6d703461;
|
public static final int TYPE_mp4a = 0x6d703461;
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
|
public static final int TYPE__mp2 = 0x2e6d7032;
|
||||||
|
|
||||||
@SuppressWarnings("ConstantCaseForConstants")
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
public static final int TYPE__mp3 = 0x2e6d7033;
|
public static final int TYPE__mp3 = 0x2e6d7033;
|
||||||
|
|
||||||
@ -274,9 +277,6 @@ import java.util.List;
|
|||||||
@SuppressWarnings("ConstantCaseForConstants")
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
public static final int TYPE_TTML = 0x54544d4c;
|
public static final int TYPE_TTML = 0x54544d4c;
|
||||||
|
|
||||||
@SuppressWarnings("ConstantCaseForConstants")
|
|
||||||
public static final int TYPE_vmhd = 0x766d6864;
|
|
||||||
|
|
||||||
@SuppressWarnings("ConstantCaseForConstants")
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
public static final int TYPE_mp4v = 0x6d703476;
|
public static final int TYPE_mp4v = 0x6d703476;
|
||||||
|
|
||||||
@ -358,6 +358,9 @@ import java.util.List;
|
|||||||
@SuppressWarnings("ConstantCaseForConstants")
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
public static final int TYPE_camm = 0x63616d6d;
|
public static final int TYPE_camm = 0x63616d6d;
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
|
public static final int TYPE_mett = 0x6d657474;
|
||||||
|
|
||||||
@SuppressWarnings("ConstantCaseForConstants")
|
@SuppressWarnings("ConstantCaseForConstants")
|
||||||
public static final int TYPE_alac = 0x616c6163;
|
public static final int TYPE_alac = 0x616c6163;
|
||||||
|
|
||||||
|
@ -384,9 +384,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fixed sample size raw audio may need to be rechunked.
|
// Fixed sample size raw audio may need to be rechunked.
|
||||||
boolean isFixedSampleSizeRawAudio =
|
int fixedSampleSize = sampleSizeBox.getFixedSampleSize();
|
||||||
sampleSizeBox.isFixedSampleSize()
|
@Nullable String sampleMimeType = track.format.sampleMimeType;
|
||||||
&& MimeTypes.AUDIO_RAW.equals(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
|
&& remainingTimestampDeltaChanges == 0
|
||||||
&& remainingTimestampOffsetChanges == 0
|
&& remainingTimestampOffsetChanges == 0
|
||||||
&& remainingSynchronizationSamples == 0;
|
&& remainingSynchronizationSamples == 0;
|
||||||
@ -399,15 +403,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
long timestampTimeUnits = 0;
|
long timestampTimeUnits = 0;
|
||||||
long duration;
|
long duration;
|
||||||
|
|
||||||
if (isFixedSampleSizeRawAudio) {
|
if (rechunkFixedSizeSamples) {
|
||||||
long[] chunkOffsetsBytes = new long[chunkIterator.length];
|
long[] chunkOffsetsBytes = new long[chunkIterator.length];
|
||||||
int[] chunkSampleCounts = new int[chunkIterator.length];
|
int[] chunkSampleCounts = new int[chunkIterator.length];
|
||||||
while (chunkIterator.moveNext()) {
|
while (chunkIterator.moveNext()) {
|
||||||
chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset;
|
chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset;
|
||||||
chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples;
|
chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples;
|
||||||
}
|
}
|
||||||
int fixedSampleSize =
|
|
||||||
Util.getPcmFrameSize(track.format.pcmEncoding, track.format.channelCount);
|
|
||||||
FixedSampleSizeRechunker.Results rechunkedResults =
|
FixedSampleSizeRechunker.Results rechunkedResults =
|
||||||
FixedSampleSizeRechunker.rechunk(
|
FixedSampleSizeRechunker.rechunk(
|
||||||
fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
|
fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
|
||||||
@ -878,6 +880,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
|| childAtomType == Atom.TYPE_lpcm
|
|| childAtomType == Atom.TYPE_lpcm
|
||||||
|| childAtomType == Atom.TYPE_sowt
|
|| childAtomType == Atom.TYPE_sowt
|
||||||
|| childAtomType == Atom.TYPE_twos
|
|| childAtomType == Atom.TYPE_twos
|
||||||
|
|| childAtomType == Atom.TYPE__mp2
|
||||||
|| childAtomType == Atom.TYPE__mp3
|
|| childAtomType == Atom.TYPE__mp3
|
||||||
|| childAtomType == Atom.TYPE_alac
|
|| childAtomType == Atom.TYPE_alac
|
||||||
|| childAtomType == Atom.TYPE_alaw
|
|| childAtomType == Atom.TYPE_alaw
|
||||||
@ -891,6 +894,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
|| childAtomType == Atom.TYPE_c608) {
|
|| childAtomType == Atom.TYPE_c608) {
|
||||||
parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
|
parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
|
||||||
language, out);
|
language, out);
|
||||||
|
} else if (childAtomType == Atom.TYPE_mett) {
|
||||||
|
parseMetaDataSampleEntry(stsd, childAtomType, childStartPosition, trackId, out);
|
||||||
} else if (childAtomType == Atom.TYPE_camm) {
|
} else if (childAtomType == Atom.TYPE_camm) {
|
||||||
out.format =
|
out.format =
|
||||||
new Format.Builder()
|
new Format.Builder()
|
||||||
@ -1097,6 +1102,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
.build();
|
.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).
|
* 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) {
|
} else if (atomType == Atom.TYPE_twos) {
|
||||||
mimeType = MimeTypes.AUDIO_RAW;
|
mimeType = MimeTypes.AUDIO_RAW;
|
||||||
pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN;
|
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;
|
mimeType = MimeTypes.AUDIO_MPEG;
|
||||||
} else if (atomType == Atom.TYPE_alac) {
|
} else if (atomType == Atom.TYPE_alac) {
|
||||||
mimeType = MimeTypes.AUDIO_ALAC;
|
mimeType = MimeTypes.AUDIO_ALAC;
|
||||||
@ -1646,16 +1663,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
*/
|
*/
|
||||||
int getSampleCount();
|
int getSampleCount();
|
||||||
|
|
||||||
/**
|
/** Returns the size of each sample if fixed, or {@link C#LENGTH_UNSET} otherwise. */
|
||||||
* Returns the size for the next sample.
|
int getFixedSampleSize();
|
||||||
*/
|
|
||||||
|
/** Returns the size for the next sample. */
|
||||||
int readNextSampleSize();
|
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) {
|
public StszSampleSizeBox(Atom.LeafAtom stszAtom) {
|
||||||
data = stszAtom.data;
|
data = stszAtom.data;
|
||||||
data.setPosition(Atom.FULL_HEADER_SIZE);
|
data.setPosition(Atom.FULL_HEADER_SIZE);
|
||||||
fixedSampleSize = data.readUnsignedIntToInt();
|
int fixedSampleSize = data.readUnsignedIntToInt();
|
||||||
|
this.fixedSampleSize = fixedSampleSize == 0 ? C.LENGTH_UNSET : fixedSampleSize;
|
||||||
sampleCount = data.readUnsignedIntToInt();
|
sampleCount = data.readUnsignedIntToInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1680,15 +1693,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int readNextSampleSize() {
|
public int getFixedSampleSize() {
|
||||||
return fixedSampleSize == 0 ? data.readUnsignedIntToInt() : fixedSampleSize;
|
return fixedSampleSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isFixedSampleSize() {
|
public int readNextSampleSize() {
|
||||||
return fixedSampleSize != 0;
|
return fixedSampleSize == C.LENGTH_UNSET ? data.readUnsignedIntToInt() : fixedSampleSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1716,6 +1728,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
return sampleCount;
|
return sampleCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getFixedSampleSize() {
|
||||||
|
return C.LENGTH_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int readNextSampleSize() {
|
public int readNextSampleSize() {
|
||||||
if (fieldSize == 8) {
|
if (fieldSize == 8) {
|
||||||
@ -1735,12 +1752,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isFixedSampleSize() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,9 @@ import java.util.Arrays;
|
|||||||
int segmentIndex = currentSegmentIndex + segmentCount;
|
int segmentIndex = currentSegmentIndex + segmentCount;
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
if (packetArray.capacity() < packetArray.limit() + size) {
|
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);
|
input.readFully(packetArray.getData(), packetArray.limit(), size);
|
||||||
packetArray.setLimit(packetArray.limit() + size);
|
packetArray.setLimit(packetArray.limit() + size);
|
||||||
@ -131,7 +133,8 @@ import java.util.Arrays;
|
|||||||
}
|
}
|
||||||
packetArray.reset(
|
packetArray.reset(
|
||||||
Arrays.copyOf(
|
Arrays.copyOf(
|
||||||
packetArray.getData(), max(OggPageHeader.MAX_PAGE_PAYLOAD, packetArray.limit())));
|
packetArray.getData(), max(OggPageHeader.MAX_PAGE_PAYLOAD, packetArray.limit())),
|
||||||
|
/* limit= */ packetArray.limit());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,6 +25,7 @@ import com.google.android.exoplayer2.util.MimeTypes;
|
|||||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link StreamReader} to extract Vorbis data out of Ogg byte stream.
|
* {@link StreamReader} to extract Vorbis data out of Ogg byte stream.
|
||||||
@ -160,8 +161,11 @@ import java.util.ArrayList;
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
/* package */ static void appendNumberOfSamples(
|
/* package */ static void appendNumberOfSamples(
|
||||||
ParsableByteArray buffer, long packetSampleCount) {
|
ParsableByteArray buffer, long packetSampleCount) {
|
||||||
|
if (buffer.capacity() < buffer.limit() + 4) {
|
||||||
buffer.setLimit(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
|
// The vorbis decoder expects the number of samples in the packet
|
||||||
// to be appended to the audio data as an int32
|
// to be appended to the audio data as an int32
|
||||||
byte[] data = buffer.getData();
|
byte[] data = buffer.getData();
|
||||||
|
@ -97,11 +97,11 @@ public final class PesReader implements TsPayloadReader {
|
|||||||
Log.w(TAG, "Unexpected start indicator reading extended header");
|
Log.w(TAG, "Unexpected start indicator reading extended header");
|
||||||
break;
|
break;
|
||||||
case STATE_READING_BODY:
|
case STATE_READING_BODY:
|
||||||
// If payloadSize == -1 then the length of the previous packet was unspecified, and so
|
// 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
|
// we only know that it's finished now that we've seen the start of the next one. This is
|
||||||
// is expected. If payloadSize != -1, then the length of the previous packet was known,
|
// expected. If payloadSize is set, then the length of the previous packet was known, but
|
||||||
// but we didn't receive that amount of data. This is not expected.
|
// we didn't receive that amount of data. This is not expected.
|
||||||
if (payloadSize != -1) {
|
if (payloadSize != C.LENGTH_UNSET) {
|
||||||
Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes");
|
Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes");
|
||||||
}
|
}
|
||||||
// Either way, notify the reader that it has now finished.
|
// Either way, notify the reader that it has now finished.
|
||||||
@ -136,13 +136,13 @@ public final class PesReader implements TsPayloadReader {
|
|||||||
break;
|
break;
|
||||||
case STATE_READING_BODY:
|
case STATE_READING_BODY:
|
||||||
readLength = data.bytesLeft();
|
readLength = data.bytesLeft();
|
||||||
int padding = payloadSize == -1 ? 0 : readLength - payloadSize;
|
int padding = payloadSize == C.LENGTH_UNSET ? 0 : readLength - payloadSize;
|
||||||
if (padding > 0) {
|
if (padding > 0) {
|
||||||
readLength -= padding;
|
readLength -= padding;
|
||||||
data.setLimit(data.getPosition() + readLength);
|
data.setLimit(data.getPosition() + readLength);
|
||||||
}
|
}
|
||||||
reader.consume(data);
|
reader.consume(data);
|
||||||
if (payloadSize != -1) {
|
if (payloadSize != C.LENGTH_UNSET) {
|
||||||
payloadSize -= readLength;
|
payloadSize -= readLength;
|
||||||
if (payloadSize == 0) {
|
if (payloadSize == 0) {
|
||||||
reader.packetFinished();
|
reader.packetFinished();
|
||||||
@ -191,7 +191,7 @@ public final class PesReader implements TsPayloadReader {
|
|||||||
int startCodePrefix = pesScratch.readBits(24);
|
int startCodePrefix = pesScratch.readBits(24);
|
||||||
if (startCodePrefix != 0x000001) {
|
if (startCodePrefix != 0x000001) {
|
||||||
Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix);
|
Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix);
|
||||||
payloadSize = -1;
|
payloadSize = C.LENGTH_UNSET;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,10 +208,14 @@ public final class PesReader implements TsPayloadReader {
|
|||||||
extendedHeaderLength = pesScratch.readBits(8);
|
extendedHeaderLength = pesScratch.readBits(8);
|
||||||
|
|
||||||
if (packetLength == 0) {
|
if (packetLength == 0) {
|
||||||
payloadSize = -1;
|
payloadSize = C.LENGTH_UNSET;
|
||||||
} else {
|
} else {
|
||||||
payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */
|
payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */
|
||||||
- HEADER_SIZE - extendedHeaderLength;
|
- HEADER_SIZE - extendedHeaderLength;
|
||||||
|
if (payloadSize < 0) {
|
||||||
|
Log.w(TAG, "Found negative packet payload size: " + payloadSize);
|
||||||
|
payloadSize = C.LENGTH_UNSET;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -37,13 +37,16 @@ import java.io.IOException;
|
|||||||
|
|
||||||
private static final long SEEK_TOLERANCE_US = 100_000;
|
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 MINIMUM_SEARCH_RANGE_BYTES = 5 * TsExtractor.TS_PACKET_SIZE;
|
||||||
private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE;
|
|
||||||
|
|
||||||
public TsBinarySearchSeeker(
|
public TsBinarySearchSeeker(
|
||||||
TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) {
|
TimestampAdjuster pcrTimestampAdjuster,
|
||||||
|
long streamDurationUs,
|
||||||
|
long inputLength,
|
||||||
|
int pcrPid,
|
||||||
|
int timestampSearchBytes) {
|
||||||
super(
|
super(
|
||||||
new DefaultSeekTimestampConverter(),
|
new DefaultSeekTimestampConverter(),
|
||||||
new TsPcrSeeker(pcrPid, pcrTimestampAdjuster),
|
new TsPcrSeeker(pcrPid, pcrTimestampAdjuster, timestampSearchBytes),
|
||||||
streamDurationUs,
|
streamDurationUs,
|
||||||
/* floorTimePosition= */ 0,
|
/* floorTimePosition= */ 0,
|
||||||
/* ceilingTimePosition= */ streamDurationUs + 1,
|
/* ceilingTimePosition= */ streamDurationUs + 1,
|
||||||
@ -58,7 +61,7 @@ import java.io.IOException;
|
|||||||
* position in a TS stream.
|
* position in a TS stream.
|
||||||
*
|
*
|
||||||
* <p>Given a PCR timestamp, and a position within a TS stream, this seeker will peek up to {@link
|
* <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
|
* PCR_PID, and then compare the PCR timestamps (if available) of these packets to the target
|
||||||
* timestamp.
|
* timestamp.
|
||||||
*/
|
*/
|
||||||
@ -67,10 +70,13 @@ import java.io.IOException;
|
|||||||
private final TimestampAdjuster pcrTimestampAdjuster;
|
private final TimestampAdjuster pcrTimestampAdjuster;
|
||||||
private final ParsableByteArray packetBuffer;
|
private final ParsableByteArray packetBuffer;
|
||||||
private final int pcrPid;
|
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.pcrPid = pcrPid;
|
||||||
this.pcrTimestampAdjuster = pcrTimestampAdjuster;
|
this.pcrTimestampAdjuster = pcrTimestampAdjuster;
|
||||||
|
this.timestampSearchBytes = timestampSearchBytes;
|
||||||
packetBuffer = new ParsableByteArray();
|
packetBuffer = new ParsableByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +84,7 @@ import java.io.IOException;
|
|||||||
public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
|
public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
long inputPosition = input.getPosition();
|
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);
|
packetBuffer.reset(bytesToSearch);
|
||||||
input.peekFully(packetBuffer.getData(), /* offset= */ 0, bytesToSearch);
|
input.peekFully(packetBuffer.getData(), /* offset= */ 0, bytesToSearch);
|
||||||
|
@ -38,8 +38,7 @@ import java.io.IOException;
|
|||||||
*/
|
*/
|
||||||
/* package */ final class TsDurationReader {
|
/* 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 TimestampAdjuster pcrTimestampAdjuster;
|
||||||
private final ParsableByteArray packetBuffer;
|
private final ParsableByteArray packetBuffer;
|
||||||
|
|
||||||
@ -51,7 +50,8 @@ import java.io.IOException;
|
|||||||
private long lastPcrValue;
|
private long lastPcrValue;
|
||||||
private long durationUs;
|
private long durationUs;
|
||||||
|
|
||||||
/* package */ TsDurationReader() {
|
/* package */ TsDurationReader(int timestampSearchBytes) {
|
||||||
|
this.timestampSearchBytes = timestampSearchBytes;
|
||||||
pcrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);
|
pcrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);
|
||||||
firstPcrValue = C.TIME_UNSET;
|
firstPcrValue = C.TIME_UNSET;
|
||||||
lastPcrValue = C.TIME_UNSET;
|
lastPcrValue = C.TIME_UNSET;
|
||||||
@ -125,7 +125,7 @@ import java.io.IOException;
|
|||||||
|
|
||||||
private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
|
private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, input.getLength());
|
int bytesToSearch = (int) min(timestampSearchBytes, input.getLength());
|
||||||
int searchStartPosition = 0;
|
int searchStartPosition = 0;
|
||||||
if (input.getPosition() != searchStartPosition) {
|
if (input.getPosition() != searchStartPosition) {
|
||||||
seekPositionHolder.position = searchStartPosition;
|
seekPositionHolder.position = searchStartPosition;
|
||||||
@ -161,7 +161,7 @@ import java.io.IOException;
|
|||||||
private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
|
private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
long inputLength = input.getLength();
|
long inputLength = input.getLength();
|
||||||
int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, inputLength);
|
int bytesToSearch = (int) min(timestampSearchBytes, inputLength);
|
||||||
long searchStartPosition = inputLength - bytesToSearch;
|
long searchStartPosition = inputLength - bytesToSearch;
|
||||||
if (input.getPosition() != searchStartPosition) {
|
if (input.getPosition() != searchStartPosition) {
|
||||||
seekPositionHolder.position = searchStartPosition;
|
seekPositionHolder.position = searchStartPosition;
|
||||||
|
@ -80,6 +80,9 @@ public final class TsExtractor implements Extractor {
|
|||||||
*/
|
*/
|
||||||
public static final int MODE_HLS = 2;
|
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 = 0x03;
|
||||||
public static final int TS_STREAM_TYPE_MPA_LSF = 0x04;
|
public static final int TS_STREAM_TYPE_MPA_LSF = 0x04;
|
||||||
public static final int TS_STREAM_TYPE_AAC_ADTS = 0x0F;
|
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.
|
// 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_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.
|
public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
|
||||||
|
|
||||||
private static final int TS_PAT_PID = 0;
|
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 static final int SNIFF_TS_PACKET_COUNT = 5;
|
||||||
|
|
||||||
private final @Mode int mode;
|
private final @Mode int mode;
|
||||||
|
private final int timestampSearchBytes;
|
||||||
private final List<TimestampAdjuster> timestampAdjusters;
|
private final List<TimestampAdjuster> timestampAdjusters;
|
||||||
private final ParsableByteArray tsPacketBuffer;
|
private final ParsableByteArray tsPacketBuffer;
|
||||||
private final SparseIntArray continuityCounters;
|
private final SparseIntArray continuityCounters;
|
||||||
@ -136,7 +139,7 @@ public final class TsExtractor implements Extractor {
|
|||||||
private int pcrPid;
|
private int pcrPid;
|
||||||
|
|
||||||
public TsExtractor() {
|
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.
|
* {@code FLAG_*} values that control the behavior of the payload readers.
|
||||||
*/
|
*/
|
||||||
public TsExtractor(@Flags int defaultTsPayloadReaderFlags) {
|
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}.
|
* and {@link #MODE_HLS}.
|
||||||
* @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory}
|
* @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory}
|
||||||
* {@code FLAG_*} values that control the behavior of the payload readers.
|
* {@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(
|
this(
|
||||||
mode,
|
mode,
|
||||||
new TimestampAdjuster(0),
|
new TimestampAdjuster(0),
|
||||||
new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags));
|
new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags),
|
||||||
|
timestampSearchBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -170,7 +183,30 @@ public final class TsExtractor implements Extractor {
|
|||||||
@Mode int mode,
|
@Mode int mode,
|
||||||
TimestampAdjuster timestampAdjuster,
|
TimestampAdjuster timestampAdjuster,
|
||||||
TsPayloadReader.Factory payloadReaderFactory) {
|
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.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory);
|
||||||
|
this.timestampSearchBytes = timestampSearchBytes;
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) {
|
if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) {
|
||||||
timestampAdjusters = Collections.singletonList(timestampAdjuster);
|
timestampAdjusters = Collections.singletonList(timestampAdjuster);
|
||||||
@ -183,7 +219,7 @@ public final class TsExtractor implements Extractor {
|
|||||||
trackPids = new SparseBooleanArray();
|
trackPids = new SparseBooleanArray();
|
||||||
tsPayloadReaders = new SparseArray<>();
|
tsPayloadReaders = new SparseArray<>();
|
||||||
continuityCounters = new SparseIntArray();
|
continuityCounters = new SparseIntArray();
|
||||||
durationReader = new TsDurationReader();
|
durationReader = new TsDurationReader(timestampSearchBytes);
|
||||||
pcrPid = -1;
|
pcrPid = -1;
|
||||||
resetPayloadReaders();
|
resetPayloadReaders();
|
||||||
}
|
}
|
||||||
@ -365,7 +401,8 @@ public final class TsExtractor implements Extractor {
|
|||||||
durationReader.getPcrTimestampAdjuster(),
|
durationReader.getPcrTimestampAdjuster(),
|
||||||
durationReader.getDurationUs(),
|
durationReader.getDurationUs(),
|
||||||
inputLength,
|
inputLength,
|
||||||
pcrPid);
|
pcrPid,
|
||||||
|
timestampSearchBytes);
|
||||||
output.seekMap(tsBinarySearchSeeker.getSeekMap());
|
output.seekMap(tsBinarySearchSeeker.getSeekMap());
|
||||||
} else {
|
} else {
|
||||||
output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));
|
output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs()));
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -38,4 +38,10 @@ public final class FlvExtractorTest {
|
|||||||
public void sample() throws Exception {
|
public void sample() throws Exception {
|
||||||
ExtractorAsserts.assertBehavior(FlvExtractor::new, "media/flv/sample.flv", simulationConfig);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp4;
|
|||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
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.ParsableByteArray;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -64,7 +65,7 @@ public final class AtomParsersTest {
|
|||||||
private static void verifyStz2Parsing(Atom.LeafAtom stz2Atom) {
|
private static void verifyStz2Parsing(Atom.LeafAtom stz2Atom) {
|
||||||
AtomParsers.Stz2SampleSizeBox box = new AtomParsers.Stz2SampleSizeBox(stz2Atom);
|
AtomParsers.Stz2SampleSizeBox box = new AtomParsers.Stz2SampleSizeBox(stz2Atom);
|
||||||
assertThat(box.getSampleCount()).isEqualTo(4);
|
assertThat(box.getSampleCount()).isEqualTo(4);
|
||||||
assertThat(box.isFixedSampleSize()).isFalse();
|
assertThat(box.getFixedSampleSize()).isEqualTo(C.LENGTH_UNSET);
|
||||||
for (int i = 0; i < box.getSampleCount(); i++) {
|
for (int i = 0; i < box.getSampleCount(); i++) {
|
||||||
assertThat(box.readNextSampleSize()).isEqualTo(i + 1);
|
assertThat(box.readNextSampleSize()).isEqualTo(i + 1);
|
||||||
}
|
}
|
||||||
|
@ -60,11 +60,27 @@ public final class OggExtractorParameterizedTest {
|
|||||||
OggExtractor::new, "media/ogg/bear_vorbis.ogg", simulationConfig);
|
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
|
@Test
|
||||||
public void vorbisWithGapBeforeSecondPage() throws Exception {
|
public void vorbisWithGapBeforeSecondPage() throws Exception {
|
||||||
ExtractorAsserts.assertBehavior(
|
ExtractorAsserts.assertBehavior(
|
||||||
OggExtractor::new, "media/ogg/bear_vorbis_gap.ogg", simulationConfig);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
package com.google.android.exoplayer2.extractor.ts;
|
package com.google.android.exoplayer2.extractor.ts;
|
||||||
|
|
||||||
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.ExtractorAsserts.AssertionConfig;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
@ -49,7 +50,10 @@ public final class AdtsExtractorTest {
|
|||||||
public void sample_withSeeking() throws Exception {
|
public void sample_withSeeking() throws Exception {
|
||||||
ExtractorAsserts.assertBehavior(
|
ExtractorAsserts.assertBehavior(
|
||||||
() -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING),
|
() -> 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);
|
simulationConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ public final class TsDurationReaderTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
tsDurationReader = new TsDurationReader();
|
tsDurationReader = new TsDurationReader(TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES);
|
||||||
seekPositionHolder = new PositionHolder();
|
seekPositionHolder = new PositionHolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,13 +25,6 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
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-qual:' + checkerframeworkVersion
|
||||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
|
@ -592,7 +592,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
|
|
||||||
public InitializationTrackSelection(TrackGroup group, int[] tracks) {
|
public InitializationTrackSelection(TrackGroup group, int[] tracks) {
|
||||||
super(group, 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
|
@Override
|
||||||
|
@ -20,13 +20,6 @@ dependencies {
|
|||||||
api 'androidx.media:media:' + androidxMediaVersion
|
api 'androidx.media:media:' + androidxMediaVersion
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion
|
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.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
|
@ -227,13 +227,22 @@ public class DefaultTimeBar extends View implements TimeBar {
|
|||||||
this(context, attrs, defStyleAttr, attrs);
|
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.
|
// Suppress warnings due to usage of View methods in the constructor.
|
||||||
@SuppressWarnings("nullness:method.invocation.invalid")
|
@SuppressWarnings("nullness:method.invocation.invalid")
|
||||||
public DefaultTimeBar(
|
public DefaultTimeBar(
|
||||||
Context context,
|
Context context,
|
||||||
@Nullable AttributeSet attrs,
|
@Nullable AttributeSet attrs,
|
||||||
int defStyleAttr,
|
int defStyleAttr,
|
||||||
@Nullable AttributeSet timebarAttrs) {
|
@Nullable AttributeSet timebarAttrs,
|
||||||
|
int defStyleRes) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
seekBounds = new Rect();
|
seekBounds = new Rect();
|
||||||
progressBar = 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);
|
int defaultScrubberDraggedSize = dpToPx(density, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP);
|
||||||
if (timebarAttrs != null) {
|
if (timebarAttrs != null) {
|
||||||
TypedArray a =
|
TypedArray a =
|
||||||
context.getTheme().obtainStyledAttributes(timebarAttrs, R.styleable.DefaultTimeBar, 0, 0);
|
context
|
||||||
|
.getTheme()
|
||||||
|
.obtainStyledAttributes(
|
||||||
|
timebarAttrs, R.styleable.DefaultTimeBar, defStyleAttr, defStyleRes);
|
||||||
try {
|
try {
|
||||||
scrubberDrawable = a.getDrawable(R.styleable.DefaultTimeBar_scrubber_drawable);
|
scrubberDrawable = a.getDrawable(R.styleable.DefaultTimeBar_scrubber_drawable);
|
||||||
if (scrubberDrawable != null) {
|
if (scrubberDrawable != null) {
|
||||||
|
@ -989,7 +989,6 @@ public class PlayerNotificationManager {
|
|||||||
Notification notification = builder.build();
|
Notification notification = builder.build();
|
||||||
notificationManager.notify(notificationId, notification);
|
notificationManager.notify(notificationId, notification);
|
||||||
if (!isNotificationStarted) {
|
if (!isNotificationStarted) {
|
||||||
isNotificationStarted = true;
|
|
||||||
context.registerReceiver(notificationBroadcastReceiver, intentFilter);
|
context.registerReceiver(notificationBroadcastReceiver, intentFilter);
|
||||||
if (notificationListener != null) {
|
if (notificationListener != null) {
|
||||||
notificationListener.onNotificationStarted(notificationId, notification);
|
notificationListener.onNotificationStarted(notificationId, notification);
|
||||||
@ -997,8 +996,12 @@ public class PlayerNotificationManager {
|
|||||||
}
|
}
|
||||||
@Nullable NotificationListener listener = notificationListener;
|
@Nullable NotificationListener listener = notificationListener;
|
||||||
if (listener != null) {
|
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.
|
// We're calling a deprecated listener method that we still want to notify.
|
||||||
|
@ -459,6 +459,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
|
|
||||||
@SuppressWarnings({
|
@SuppressWarnings({
|
||||||
"nullness:argument.type.incompatible",
|
"nullness:argument.type.incompatible",
|
||||||
|
"nullness:assignment.type.incompatible",
|
||||||
"nullness:method.invocation.invalid",
|
"nullness:method.invocation.invalid",
|
||||||
"nullness:methodref.receiver.bound.invalid"
|
"nullness:methodref.receiver.bound.invalid"
|
||||||
})
|
})
|
||||||
@ -526,8 +527,11 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
a.recycle();
|
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<>();
|
visibilityListeners = new CopyOnWriteArrayList<>();
|
||||||
period = new Timeline.Period();
|
period = new Timeline.Period();
|
||||||
window = new Timeline.Window();
|
window = new Timeline.Window();
|
||||||
@ -537,13 +541,9 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
playedAdGroups = new boolean[0];
|
playedAdGroups = new boolean[0];
|
||||||
extraAdGroupTimesMs = new long[0];
|
extraAdGroupTimesMs = new long[0];
|
||||||
extraPlayedAdGroups = new boolean[0];
|
extraPlayedAdGroups = new boolean[0];
|
||||||
componentListener = new ComponentListener();
|
|
||||||
controlDispatcher = new DefaultControlDispatcher(fastForwardMs, rewindMs);
|
controlDispatcher = new DefaultControlDispatcher(fastForwardMs, rewindMs);
|
||||||
updateProgressAction = this::updateProgress;
|
updateProgressAction = this::updateProgress;
|
||||||
|
|
||||||
LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this);
|
|
||||||
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
|
|
||||||
|
|
||||||
// Relating to Bottom Bar Left View
|
// Relating to Bottom Bar Left View
|
||||||
durationView = findViewById(R.id.exo_duration);
|
durationView = findViewById(R.id.exo_duration);
|
||||||
positionView = findViewById(R.id.exo_position);
|
positionView = findViewById(R.id.exo_position);
|
||||||
@ -570,7 +570,8 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
} else if (timeBarPlaceholder != null) {
|
} else if (timeBarPlaceholder != null) {
|
||||||
// Propagate attrs as timebarAttrs so that DefaultTimeBar's custom attributes are transferred,
|
// Propagate attrs as timebarAttrs so that DefaultTimeBar's custom attributes are transferred,
|
||||||
// but standard attributes (e.g. background) are not.
|
// 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.setId(R.id.exo_progress);
|
||||||
defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams());
|
defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams());
|
||||||
ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent());
|
ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent());
|
||||||
@ -581,10 +582,10 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
} else {
|
} else {
|
||||||
timeBar = null;
|
timeBar = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeBar != null) {
|
if (timeBar != null) {
|
||||||
timeBar.addListener(componentListener);
|
timeBar.addListener(componentListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
playPauseButton = findViewById(R.id.exo_play_pause);
|
playPauseButton = findViewById(R.id.exo_play_pause);
|
||||||
if (playPauseButton != null) {
|
if (playPauseButton != null) {
|
||||||
playPauseButton.setOnClickListener(componentListener);
|
playPauseButton.setOnClickListener(componentListener);
|
||||||
@ -626,7 +627,6 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resources = context.getResources();
|
resources = context.getResources();
|
||||||
|
|
||||||
buttonAlphaEnabled =
|
buttonAlphaEnabled =
|
||||||
(float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_enabled) / 100;
|
(float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_enabled) / 100;
|
||||||
buttonAlphaDisabled =
|
buttonAlphaDisabled =
|
||||||
@ -634,10 +634,12 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
|
|
||||||
vrButton = findViewById(R.id.exo_vr);
|
vrButton = findViewById(R.id.exo_vr);
|
||||||
if (vrButton != null) {
|
if (vrButton != null) {
|
||||||
setShowVrButton(showVrButton);
|
|
||||||
updateButton(/* enabled= */ false, vrButton);
|
updateButton(/* enabled= */ false, vrButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
controlViewLayoutManager = new StyledPlayerControlViewLayoutManager(this);
|
||||||
|
controlViewLayoutManager.setAnimationEnabled(animationEnabled);
|
||||||
|
|
||||||
// Related to Settings List View
|
// Related to Settings List View
|
||||||
String[] settingTexts = new String[2];
|
String[] settingTexts = new String[2];
|
||||||
Drawable[] settingIcons = new Drawable[2];
|
Drawable[] settingIcons = new Drawable[2];
|
||||||
@ -1071,6 +1073,11 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
controlViewLayoutManager.hide();
|
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. */
|
/** Returns whether the controller is fully visible, which means all UI controls are visible. */
|
||||||
public boolean isFullyVisible() {
|
public boolean isFullyVisible() {
|
||||||
return controlViewLayoutManager.isFullyVisible();
|
return controlViewLayoutManager.isFullyVisible();
|
||||||
@ -1159,13 +1166,14 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
if (controlDispatcher instanceof DefaultControlDispatcher) {
|
if (controlDispatcher instanceof DefaultControlDispatcher) {
|
||||||
rewindMs = ((DefaultControlDispatcher) controlDispatcher).getRewindIncrementMs();
|
rewindMs = ((DefaultControlDispatcher) controlDispatcher).getRewindIncrementMs();
|
||||||
}
|
}
|
||||||
long rewindSec = rewindMs / 1_000;
|
int rewindSec = (int) (rewindMs / 1_000);
|
||||||
if (rewindButtonTextView != null) {
|
if (rewindButtonTextView != null) {
|
||||||
rewindButtonTextView.setText(String.valueOf(rewindSec));
|
rewindButtonTextView.setText(String.valueOf(rewindSec));
|
||||||
}
|
}
|
||||||
if (rewindButton != null) {
|
if (rewindButton != null) {
|
||||||
rewindButton.setContentDescription(
|
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) {
|
if (controlDispatcher instanceof DefaultControlDispatcher) {
|
||||||
fastForwardMs = ((DefaultControlDispatcher) controlDispatcher).getFastForwardIncrementMs();
|
fastForwardMs = ((DefaultControlDispatcher) controlDispatcher).getFastForwardIncrementMs();
|
||||||
}
|
}
|
||||||
long fastForwardSec = fastForwardMs / 1_000;
|
int fastForwardSec = (int) (fastForwardMs / 1_000);
|
||||||
if (fastForwardButtonTextView != null) {
|
if (fastForwardButtonTextView != null) {
|
||||||
fastForwardButtonTextView.setText(String.valueOf(fastForwardSec));
|
fastForwardButtonTextView.setText(String.valueOf(fastForwardSec));
|
||||||
}
|
}
|
||||||
if (fastForwardButton != null) {
|
if (fastForwardButton != null) {
|
||||||
fastForwardButton.setContentDescription(
|
fastForwardButton.setContentDescription(
|
||||||
resources.getString(
|
resources.getQuantityString(
|
||||||
R.string.exo_controls_fastforward_by_amount_description, fastForwardSec));
|
R.plurals.exo_controls_fastforward_by_amount_description,
|
||||||
|
fastForwardSec,
|
||||||
|
fastForwardSec));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1604,7 +1614,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
@Override
|
@Override
|
||||||
public void onAttachedToWindow() {
|
public void onAttachedToWindow() {
|
||||||
super.onAttachedToWindow();
|
super.onAttachedToWindow();
|
||||||
controlViewLayoutManager.onViewAttached(this);
|
controlViewLayoutManager.onAttachedToWindow();
|
||||||
isAttachedToWindow = true;
|
isAttachedToWindow = true;
|
||||||
if (isFullyVisible()) {
|
if (isFullyVisible()) {
|
||||||
controlViewLayoutManager.resetHideCallbacks();
|
controlViewLayoutManager.resetHideCallbacks();
|
||||||
@ -1615,7 +1625,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
@Override
|
@Override
|
||||||
public void onDetachedFromWindow() {
|
public void onDetachedFromWindow() {
|
||||||
super.onDetachedFromWindow();
|
super.onDetachedFromWindow();
|
||||||
controlViewLayoutManager.onViewDetached(this);
|
controlViewLayoutManager.onDetachedFromWindow();
|
||||||
isAttachedToWindow = false;
|
isAttachedToWindow = false;
|
||||||
removeCallbacks(updateProgressAction);
|
removeCallbacks(updateProgressAction);
|
||||||
controlViewLayoutManager.removeHideCallbacks();
|
controlViewLayoutManager.removeHideCallbacks();
|
||||||
@ -2002,11 +2012,13 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkNotNull(subtitleButton)
|
|
||||||
.setImageDrawable(subtitleIsOn ? subtitleOnButtonDrawable : subtitleOffButtonDrawable);
|
if (subtitleButton != null) {
|
||||||
checkNotNull(subtitleButton)
|
subtitleButton.setImageDrawable(
|
||||||
.setContentDescription(
|
subtitleIsOn ? subtitleOnButtonDrawable : subtitleOffButtonDrawable);
|
||||||
subtitleIsOn ? subtitleOnContentDescription : subtitleOffContentDescription);
|
subtitleButton.setContentDescription(
|
||||||
|
subtitleIsOn ? subtitleOnContentDescription : subtitleOffContentDescription);
|
||||||
|
}
|
||||||
this.rendererIndices = rendererIndices;
|
this.rendererIndices = rendererIndices;
|
||||||
this.tracks = trackInfos;
|
this.tracks = trackInfos;
|
||||||
this.mappedTrackInfo = mappedTrackInfo;
|
this.mappedTrackInfo = mappedTrackInfo;
|
||||||
|
@ -47,6 +47,26 @@ import java.util.List;
|
|||||||
// Int for defining the UX state where the views are being animated to be shown.
|
// 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 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 showAllBarsRunnable;
|
||||||
private final Runnable hideAllBarsRunnable;
|
private final Runnable hideAllBarsRunnable;
|
||||||
private final Runnable hideProgressBarRunnable;
|
private final Runnable hideProgressBarRunnable;
|
||||||
@ -57,32 +77,16 @@ import java.util.List;
|
|||||||
private final List<View> shownButtons;
|
private final List<View> shownButtons;
|
||||||
|
|
||||||
private int uxState;
|
private int uxState;
|
||||||
private boolean initiallyHidden;
|
|
||||||
private boolean isMinimalMode;
|
private boolean isMinimalMode;
|
||||||
private boolean needToShowBars;
|
private boolean needToShowBars;
|
||||||
private boolean animationEnabled;
|
private boolean animationEnabled;
|
||||||
|
|
||||||
@Nullable private StyledPlayerControlView styledPlayerControlView;
|
@SuppressWarnings({
|
||||||
|
"nullness:method.invocation.invalid",
|
||||||
@Nullable private ViewGroup embeddedTransportControls;
|
"nullness:methodref.receiver.bound.invalid"
|
||||||
@Nullable private ViewGroup bottomBar;
|
})
|
||||||
@Nullable private ViewGroup minimalControls;
|
public StyledPlayerControlViewLayoutManager(StyledPlayerControlView styledPlayerControlView) {
|
||||||
@Nullable private ViewGroup basicControls;
|
this.styledPlayerControlView = styledPlayerControlView;
|
||||||
@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() {
|
|
||||||
showAllBarsRunnable = this::showAllBars;
|
showAllBarsRunnable = this::showAllBars;
|
||||||
hideAllBarsRunnable = this::hideAllBars;
|
hideAllBarsRunnable = this::hideAllBars;
|
||||||
hideProgressBarRunnable = this::hideProgressBar;
|
hideProgressBarRunnable = this::hideProgressBar;
|
||||||
@ -92,121 +96,48 @@ import java.util.List;
|
|||||||
animationEnabled = true;
|
animationEnabled = true;
|
||||||
uxState = UX_STATE_ALL_VISIBLE;
|
uxState = UX_STATE_ALL_VISIBLE;
|
||||||
shownButtons = new ArrayList<>();
|
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
|
// Relating to Center View
|
||||||
ViewGroup centerView = v.findViewById(R.id.exo_center_view);
|
ViewGroup centerView = styledPlayerControlView.findViewById(R.id.exo_center_view);
|
||||||
embeddedTransportControls = v.findViewById(R.id.exo_embedded_transport_controls);
|
embeddedTransportControls =
|
||||||
|
styledPlayerControlView.findViewById(R.id.exo_embedded_transport_controls);
|
||||||
|
|
||||||
// Relating to Minimal Layout
|
// 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
|
// 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
|
// Relating to Bottom Bar Left View
|
||||||
timeView = v.findViewById(R.id.exo_time);
|
timeView = styledPlayerControlView.findViewById(R.id.exo_time);
|
||||||
View timeBar = v.findViewById(R.id.exo_progress);
|
timeBar = styledPlayerControlView.findViewById(R.id.exo_progress);
|
||||||
|
|
||||||
// Relating to Bottom Bar Right View
|
// Relating to Bottom Bar Right View
|
||||||
basicControls = v.findViewById(R.id.exo_basic_controls);
|
basicControls = styledPlayerControlView.findViewById(R.id.exo_basic_controls);
|
||||||
extraControls = v.findViewById(R.id.exo_extra_controls);
|
extraControls = styledPlayerControlView.findViewById(R.id.exo_extra_controls);
|
||||||
extraControlsScrollView = v.findViewById(R.id.exo_extra_controls_scroll_view);
|
extraControlsScrollView =
|
||||||
overflowShowButton = v.findViewById(R.id.exo_overflow_show);
|
styledPlayerControlView.findViewById(R.id.exo_extra_controls_scroll_view);
|
||||||
View overflowHideButton = v.findViewById(R.id.exo_overflow_hide);
|
overflowShowButton = styledPlayerControlView.findViewById(R.id.exo_overflow_show);
|
||||||
|
View overflowHideButton = styledPlayerControlView.findViewById(R.id.exo_overflow_hide);
|
||||||
if (overflowShowButton != null && overflowHideButton != null) {
|
if (overflowShowButton != null && overflowHideButton != null) {
|
||||||
overflowShowButton.setOnClickListener(this::onOverflowButtonClick);
|
overflowShowButton.setOnClickListener(this::onOverflowButtonClick);
|
||||||
overflowHideButton.setOnClickListener(this::onOverflowButtonClick);
|
overflowHideButton.setOnClickListener(this::onOverflowButtonClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bottomBar = bottomBar;
|
Resources resources = styledPlayerControlView.getResources();
|
||||||
this.timeBar = timeBar;
|
float bottomBarHeight =
|
||||||
|
resources.getDimension(R.dimen.exo_bottom_bar_height)
|
||||||
Resources resources = v.getResources();
|
- resources.getDimension(R.dimen.exo_styled_progress_bar_height);
|
||||||
float progressBarHeight = resources.getDimension(R.dimen.exo_custom_progress_thumb_size);
|
float progressBarHeight =
|
||||||
float bottomBarHeight = resources.getDimension(R.dimen.exo_bottom_bar_height);
|
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);
|
ValueAnimator fadeOutAnimator = ValueAnimator.ofFloat(1.0f, 0.0f);
|
||||||
fadeOutAnimator.setInterpolator(new LinearInterpolator());
|
fadeOutAnimator.setInterpolator(new LinearInterpolator());
|
||||||
fadeOutAnimator.addUpdateListener(
|
fadeOutAnimator.addUpdateListener(
|
||||||
animation -> {
|
animation -> {
|
||||||
float animatedValue = (float) animation.getAnimatedValue();
|
float animatedValue = (float) animation.getAnimatedValue();
|
||||||
|
|
||||||
if (centerView != null) {
|
if (centerView != null) {
|
||||||
centerView.setAlpha(animatedValue);
|
centerView.setAlpha(animatedValue);
|
||||||
}
|
}
|
||||||
@ -239,7 +170,6 @@ import java.util.List;
|
|||||||
fadeInAnimator.addUpdateListener(
|
fadeInAnimator.addUpdateListener(
|
||||||
animation -> {
|
animation -> {
|
||||||
float animatedValue = (float) animation.getAnimatedValue();
|
float animatedValue = (float) animation.getAnimatedValue();
|
||||||
|
|
||||||
if (centerView != null) {
|
if (centerView != null) {
|
||||||
centerView.setAlpha(animatedValue);
|
centerView.setAlpha(animatedValue);
|
||||||
}
|
}
|
||||||
@ -276,9 +206,7 @@ import java.util.List;
|
|||||||
public void onAnimationEnd(Animator animation) {
|
public void onAnimationEnd(Animator animation) {
|
||||||
setUxState(UX_STATE_ONLY_PROGRESS_VISIBLE);
|
setUxState(UX_STATE_ONLY_PROGRESS_VISIBLE);
|
||||||
if (needToShowBars) {
|
if (needToShowBars) {
|
||||||
if (styledPlayerControlView != null) {
|
styledPlayerControlView.post(showAllBarsRunnable);
|
||||||
styledPlayerControlView.post(showAllBarsRunnable);
|
|
||||||
}
|
|
||||||
needToShowBars = false;
|
needToShowBars = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -301,9 +229,7 @@ import java.util.List;
|
|||||||
public void onAnimationEnd(Animator animation) {
|
public void onAnimationEnd(Animator animation) {
|
||||||
setUxState(UX_STATE_NONE_VISIBLE);
|
setUxState(UX_STATE_NONE_VISIBLE);
|
||||||
if (needToShowBars) {
|
if (needToShowBars) {
|
||||||
if (styledPlayerControlView != null) {
|
styledPlayerControlView.post(showAllBarsRunnable);
|
||||||
styledPlayerControlView.post(showAllBarsRunnable);
|
|
||||||
}
|
|
||||||
needToShowBars = false;
|
needToShowBars = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -325,9 +251,7 @@ import java.util.List;
|
|||||||
public void onAnimationEnd(Animator animation) {
|
public void onAnimationEnd(Animator animation) {
|
||||||
setUxState(UX_STATE_NONE_VISIBLE);
|
setUxState(UX_STATE_NONE_VISIBLE);
|
||||||
if (needToShowBars) {
|
if (needToShowBars) {
|
||||||
if (styledPlayerControlView != null) {
|
styledPlayerControlView.post(showAllBarsRunnable);
|
||||||
styledPlayerControlView.post(showAllBarsRunnable);
|
|
||||||
}
|
|
||||||
needToShowBars = false;
|
needToShowBars = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -420,14 +344,78 @@ import java.util.List;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onViewDetached(StyledPlayerControlView v) {
|
public void show() {
|
||||||
v.removeOnLayoutChangeListener(onLayoutChangeListener);
|
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() {
|
public boolean isFullyVisible() {
|
||||||
if (styledPlayerControlView == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return uxState == UX_STATE_ALL_VISIBLE && styledPlayerControlView.isVisible();
|
return uxState == UX_STATE_ALL_VISIBLE && styledPlayerControlView.isVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,18 +443,15 @@ import java.util.List;
|
|||||||
private void setUxState(int uxState) {
|
private void setUxState(int uxState) {
|
||||||
int prevUxState = this.uxState;
|
int prevUxState = this.uxState;
|
||||||
this.uxState = uxState;
|
this.uxState = uxState;
|
||||||
if (styledPlayerControlView != null) {
|
if (uxState == UX_STATE_NONE_VISIBLE) {
|
||||||
StyledPlayerControlView styledPlayerControlView = this.styledPlayerControlView;
|
styledPlayerControlView.setVisibility(View.GONE);
|
||||||
if (uxState == UX_STATE_NONE_VISIBLE) {
|
} else if (prevUxState == UX_STATE_NONE_VISIBLE) {
|
||||||
styledPlayerControlView.setVisibility(View.GONE);
|
styledPlayerControlView.setVisibility(View.VISIBLE);
|
||||||
} else if (prevUxState == UX_STATE_NONE_VISIBLE) {
|
}
|
||||||
styledPlayerControlView.setVisibility(View.VISIBLE);
|
// TODO(insun): Notify specific uxState. Currently reuses legacy visibility listener for API
|
||||||
}
|
// compatibility.
|
||||||
// TODO(insun): Notify specific uxState. Currently reuses legacy visibility listener for API
|
if (prevUxState != uxState) {
|
||||||
// compatibility.
|
styledPlayerControlView.notifyOnVisibilityChange();
|
||||||
if (prevUxState != uxState) {
|
|
||||||
styledPlayerControlView.notifyOnVisibilityChange();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,9 +479,9 @@ import java.util.List;
|
|||||||
|
|
||||||
private void onOverflowButtonClick(View v) {
|
private void onOverflowButtonClick(View v) {
|
||||||
resetHideCallbacks();
|
resetHideCallbacks();
|
||||||
if (v.getId() == R.id.exo_overflow_show && overflowShowAnimator != null) {
|
if (v.getId() == R.id.exo_overflow_show) {
|
||||||
overflowShowAnimator.start();
|
overflowShowAnimator.start();
|
||||||
} else if (v.getId() == R.id.exo_overflow_hide && overflowHideAnimator != null) {
|
} else if (v.getId() == R.id.exo_overflow_hide) {
|
||||||
overflowHideAnimator.start();
|
overflowHideAnimator.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -510,14 +495,10 @@ import java.util.List;
|
|||||||
|
|
||||||
switch (uxState) {
|
switch (uxState) {
|
||||||
case UX_STATE_NONE_VISIBLE:
|
case UX_STATE_NONE_VISIBLE:
|
||||||
if (showAllBarsAnimator != null) {
|
showAllBarsAnimator.start();
|
||||||
showAllBarsAnimator.start();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case UX_STATE_ONLY_PROGRESS_VISIBLE:
|
case UX_STATE_ONLY_PROGRESS_VISIBLE:
|
||||||
if (showMainBarsAnimator != null) {
|
showMainBarsAnimator.start();
|
||||||
showMainBarsAnimator.start();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case UX_STATE_ANIMATING_HIDE:
|
case UX_STATE_ANIMATING_HIDE:
|
||||||
needToShowBars = true;
|
needToShowBars = true;
|
||||||
@ -531,23 +512,14 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void hideAllBars() {
|
private void hideAllBars() {
|
||||||
if (hideAllBarsAnimator == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
hideAllBarsAnimator.start();
|
hideAllBarsAnimator.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideProgressBar() {
|
private void hideProgressBar() {
|
||||||
if (hideProgressBarAnimator == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
hideProgressBarAnimator.start();
|
hideProgressBarAnimator.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideMainBars() {
|
private void hideMainBars() {
|
||||||
if (hideMainBarsAnimator == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
hideMainBarsAnimator.start();
|
hideMainBarsAnimator.start();
|
||||||
postDelayedRunnable(hideProgressBarRunnable, ANIMATION_INTERVAL_MS);
|
postDelayedRunnable(hideProgressBarRunnable, ANIMATION_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
@ -561,7 +533,7 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void postDelayedRunnable(Runnable runnable, long interval) {
|
private void postDelayedRunnable(Runnable runnable, long interval) {
|
||||||
if (styledPlayerControlView != null && interval >= 0) {
|
if (interval >= 0) {
|
||||||
styledPlayerControlView.postDelayed(runnable, interval);
|
styledPlayerControlView.postDelayed(runnable, interval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -582,19 +554,14 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldBeMinimalMode() {
|
private boolean shouldBeMinimalMode() {
|
||||||
if (this.styledPlayerControlView == null) {
|
|
||||||
return isMinimalMode;
|
|
||||||
}
|
|
||||||
ViewGroup playerControlView = this.styledPlayerControlView;
|
|
||||||
|
|
||||||
int width =
|
int width =
|
||||||
playerControlView.getWidth()
|
styledPlayerControlView.getWidth()
|
||||||
- playerControlView.getPaddingLeft()
|
- styledPlayerControlView.getPaddingLeft()
|
||||||
- playerControlView.getPaddingRight();
|
- styledPlayerControlView.getPaddingRight();
|
||||||
int height =
|
int height =
|
||||||
playerControlView.getHeight()
|
styledPlayerControlView.getHeight()
|
||||||
- playerControlView.getPaddingBottom()
|
- styledPlayerControlView.getPaddingBottom()
|
||||||
- playerControlView.getPaddingTop();
|
- styledPlayerControlView.getPaddingTop();
|
||||||
int defaultModeWidth =
|
int defaultModeWidth =
|
||||||
Math.max(
|
Math.max(
|
||||||
getWidth(embeddedTransportControls), getWidth(timeView) + getWidth(overflowShowButton));
|
getWidth(embeddedTransportControls), getWidth(timeView) + getWidth(overflowShowButton));
|
||||||
@ -605,16 +572,11 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateLayoutForSizeChange() {
|
private void updateLayoutForSizeChange() {
|
||||||
if (this.styledPlayerControlView == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
StyledPlayerControlView playerControlView = this.styledPlayerControlView;
|
|
||||||
|
|
||||||
if (minimalControls != null) {
|
if (minimalControls != null) {
|
||||||
minimalControls.setVisibility(isMinimalMode ? View.VISIBLE : View.INVISIBLE);
|
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) {
|
if (fullScreenButton != null) {
|
||||||
ViewGroup parent = (ViewGroup) fullScreenButton.getParent();
|
ViewGroup parent = (ViewGroup) fullScreenButton.getParent();
|
||||||
parent.removeView(fullScreenButton);
|
parent.removeView(fullScreenButton);
|
||||||
@ -629,12 +591,11 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (timeBar != null) {
|
if (timeBar != null) {
|
||||||
View timeBar = this.timeBar;
|
|
||||||
MarginLayoutParams timeBarParams = (MarginLayoutParams) timeBar.getLayoutParams();
|
MarginLayoutParams timeBarParams = (MarginLayoutParams) timeBar.getLayoutParams();
|
||||||
int timeBarMarginBottom =
|
int timeBarMarginBottom =
|
||||||
playerControlView
|
styledPlayerControlView
|
||||||
.getResources()
|
.getResources()
|
||||||
.getDimensionPixelSize(R.dimen.exo_custom_progress_margin_bottom);
|
.getDimensionPixelSize(R.dimen.exo_styled_progress_margin_bottom);
|
||||||
timeBarParams.bottomMargin = (isMinimalMode ? 0 : timeBarMarginBottom);
|
timeBarParams.bottomMargin = (isMinimalMode ? 0 : timeBarMarginBottom);
|
||||||
timeBar.setLayoutParams(timeBarParams);
|
timeBar.setLayoutParams(timeBarParams);
|
||||||
if (timeBar instanceof DefaultTimeBar
|
if (timeBar instanceof DefaultTimeBar
|
||||||
@ -668,18 +629,14 @@ import java.util.List;
|
|||||||
if (basicControls == null || extraControls == null) {
|
if (basicControls == null || extraControls == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ViewGroup basicControls = this.basicControls;
|
|
||||||
ViewGroup extraControls = this.extraControls;
|
|
||||||
|
|
||||||
int width =
|
int width =
|
||||||
(styledPlayerControlView != null
|
styledPlayerControlView.getWidth()
|
||||||
? styledPlayerControlView.getWidth()
|
- styledPlayerControlView.getPaddingLeft()
|
||||||
- styledPlayerControlView.getPaddingLeft()
|
- styledPlayerControlView.getPaddingRight();
|
||||||
- styledPlayerControlView.getPaddingRight()
|
int bottomBarWidth = getWidth(timeView);
|
||||||
: 0);
|
|
||||||
int basicBottomBarWidth = getWidth(timeView);
|
|
||||||
for (int i = 0; i < basicControls.getChildCount(); ++i) {
|
for (int i = 0; i < basicControls.getChildCount(); ++i) {
|
||||||
basicBottomBarWidth += basicControls.getChildAt(i).getWidth();
|
bottomBarWidth += basicControls.getChildAt(i).getWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
// BasicControls keeps overflow button at least.
|
// BasicControls keeps overflow button at least.
|
||||||
@ -687,7 +644,7 @@ import java.util.List;
|
|||||||
// ExtraControls keeps overflow button and settings button at least.
|
// ExtraControls keeps overflow button and settings button at least.
|
||||||
int minExtraControlsChildCount = 2;
|
int minExtraControlsChildCount = 2;
|
||||||
|
|
||||||
if (basicBottomBarWidth > width) {
|
if (bottomBarWidth > width) {
|
||||||
// move control views from basicControls to extraControls
|
// move control views from basicControls to extraControls
|
||||||
ArrayList<View> movingChildren = new ArrayList<>();
|
ArrayList<View> movingChildren = new ArrayList<>();
|
||||||
int movingWidth = 0;
|
int movingWidth = 0;
|
||||||
@ -696,7 +653,7 @@ import java.util.List;
|
|||||||
View child = basicControls.getChildAt(index);
|
View child = basicControls.getChildAt(index);
|
||||||
movingWidth += child.getWidth();
|
movingWidth += child.getWidth();
|
||||||
movingChildren.add(child);
|
movingChildren.add(child);
|
||||||
if (basicBottomBarWidth - movingWidth <= width) {
|
if (bottomBarWidth - movingWidth <= width) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -711,14 +668,14 @@ import java.util.List;
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} 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<>();
|
ArrayList<View> movingChildren = new ArrayList<>();
|
||||||
int movingWidth = 0;
|
int movingWidth = 0;
|
||||||
int startIndex = extraControls.getChildCount() - minExtraControlsChildCount - 1;
|
int startIndex = extraControls.getChildCount() - minExtraControlsChildCount - 1;
|
||||||
for (int index = startIndex; index >= 0; index--) {
|
for (int index = startIndex; index >= 0; index--) {
|
||||||
View child = extraControls.getChildAt(index);
|
View child = extraControls.getChildAt(index);
|
||||||
movingWidth += child.getWidth();
|
movingWidth += child.getWidth();
|
||||||
if (basicBottomBarWidth + movingWidth > width) {
|
if (bottomBarWidth + movingWidth > width) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
movingChildren.add(child);
|
movingChildren.add(child);
|
||||||
|
@ -511,11 +511,11 @@ public class StyledPlayerView extends FrameLayout implements AdsLoader.AdViewPro
|
|||||||
this.controllerAutoShow = controllerAutoShow;
|
this.controllerAutoShow = controllerAutoShow;
|
||||||
this.controllerHideDuringAds = controllerHideDuringAds;
|
this.controllerHideDuringAds = controllerHideDuringAds;
|
||||||
this.useController = useController && controller != null;
|
this.useController = useController && controller != null;
|
||||||
hideController();
|
|
||||||
updateContentDescription();
|
|
||||||
if (controller != null) {
|
if (controller != null) {
|
||||||
|
controller.hideImmediately();
|
||||||
controller.addVisibilityListener(/* listener= */ componentListener);
|
controller.addVisibilityListener(/* listener= */ componentListener);
|
||||||
}
|
}
|
||||||
|
updateContentDescription();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -414,5 +414,4 @@ public final class SubtitleView extends FrameLayout implements TextOutput {
|
|||||||
return cue;
|
return cue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import android.view.LayoutInflater;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StyleRes;
|
import androidx.annotation.StyleRes;
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
|
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 com.google.android.exoplayer2.trackselection.TrackSelectionUtil;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Builder for a dialog with a {@link TrackSelectionView}. */
|
/** Builder for a dialog with a {@link TrackSelectionView}. */
|
||||||
@ -62,6 +64,7 @@ public final class TrackSelectionDialogBuilder {
|
|||||||
@Nullable private TrackNameProvider trackNameProvider;
|
@Nullable private TrackNameProvider trackNameProvider;
|
||||||
private boolean isDisabled;
|
private boolean isDisabled;
|
||||||
private List<SelectionOverride> overrides;
|
private List<SelectionOverride> overrides;
|
||||||
|
@Nullable private Comparator<Format> trackFormatComparator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a builder for a track selection dialog.
|
* Creates a builder for a track selection dialog.
|
||||||
@ -208,6 +211,16 @@ public final class TrackSelectionDialogBuilder {
|
|||||||
return this;
|
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
|
* 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.
|
* updates the view with track names queried from the specified provider.
|
||||||
@ -287,7 +300,13 @@ public final class TrackSelectionDialogBuilder {
|
|||||||
if (trackNameProvider != null) {
|
if (trackNameProvider != null) {
|
||||||
selectionView.setTrackNameProvider(trackNameProvider);
|
selectionView.setTrackNameProvider(trackNameProvider);
|
||||||
}
|
}
|
||||||
selectionView.init(mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ null);
|
selectionView.init(
|
||||||
|
mappedTrackInfo,
|
||||||
|
rendererIndex,
|
||||||
|
isDisabled,
|
||||||
|
overrides,
|
||||||
|
trackFormatComparator,
|
||||||
|
/* listener= */ null);
|
||||||
return (dialog, which) ->
|
return (dialog, which) ->
|
||||||
callback.onTracksSelected(selectionView.getIsDisabled(), selectionView.getOverrides());
|
callback.onTracksSelected(selectionView.getIsDisabled(), selectionView.getOverrides());
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ui;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Pair;
|
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -26,6 +25,7 @@ import android.widget.CheckedTextView;
|
|||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import androidx.annotation.AttrRes;
|
import androidx.annotation.AttrRes;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.RendererCapabilities;
|
import com.google.android.exoplayer2.RendererCapabilities;
|
||||||
import com.google.android.exoplayer2.source.TrackGroup;
|
import com.google.android.exoplayer2.source.TrackGroup;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
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 com.google.android.exoplayer2.util.Assertions;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
@ -71,6 +72,7 @@ public class TrackSelectionView extends LinearLayout {
|
|||||||
private int rendererIndex;
|
private int rendererIndex;
|
||||||
private TrackGroupArray trackGroups;
|
private TrackGroupArray trackGroups;
|
||||||
private boolean isDisabled;
|
private boolean isDisabled;
|
||||||
|
@Nullable private Comparator<TrackInfo> trackInfoComparator;
|
||||||
@Nullable private TrackSelectionListener listener;
|
@Nullable private TrackSelectionListener listener;
|
||||||
|
|
||||||
/** Creates a track selection view. */
|
/** 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
|
* @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
|
* one override for each track group. If {@link #setAllowMultipleOverrides(boolean)} hasn't
|
||||||
* been set to {@code true}, only the first override is used.
|
* 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.
|
* @param listener An optional listener for track selection updates.
|
||||||
*/
|
*/
|
||||||
public void init(
|
public void init(
|
||||||
@ -203,10 +207,15 @@ public class TrackSelectionView extends LinearLayout {
|
|||||||
int rendererIndex,
|
int rendererIndex,
|
||||||
boolean isDisabled,
|
boolean isDisabled,
|
||||||
List<SelectionOverride> overrides,
|
List<SelectionOverride> overrides,
|
||||||
|
@Nullable Comparator<Format> trackFormatComparator,
|
||||||
@Nullable TrackSelectionListener listener) {
|
@Nullable TrackSelectionListener listener) {
|
||||||
this.mappedTrackInfo = mappedTrackInfo;
|
this.mappedTrackInfo = mappedTrackInfo;
|
||||||
this.rendererIndex = rendererIndex;
|
this.rendererIndex = rendererIndex;
|
||||||
this.isDisabled = isDisabled;
|
this.isDisabled = isDisabled;
|
||||||
|
this.trackInfoComparator =
|
||||||
|
trackFormatComparator == null
|
||||||
|
? null
|
||||||
|
: (o1, o2) -> trackFormatComparator.compare(o1.format, o2.format);
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
int maxOverrides = allowMultipleOverrides ? overrides.size() : Math.min(overrides.size(), 1);
|
int maxOverrides = allowMultipleOverrides ? overrides.size() : Math.min(overrides.size(), 1);
|
||||||
for (int i = 0; i < maxOverrides; i++) {
|
for (int i = 0; i < maxOverrides; i++) {
|
||||||
@ -259,7 +268,16 @@ public class TrackSelectionView extends LinearLayout {
|
|||||||
TrackGroup group = trackGroups.get(groupIndex);
|
TrackGroup group = trackGroups.get(groupIndex);
|
||||||
boolean enableMultipleChoiceForAdaptiveSelections = shouldEnableAdaptiveSelection(groupIndex);
|
boolean enableMultipleChoiceForAdaptiveSelections = shouldEnableAdaptiveSelection(groupIndex);
|
||||||
trackViews[groupIndex] = new CheckedTextView[group.length];
|
trackViews[groupIndex] = new CheckedTextView[group.length];
|
||||||
|
|
||||||
|
TrackInfo[] trackInfos = new TrackInfo[group.length];
|
||||||
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
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) {
|
if (trackIndex == 0) {
|
||||||
addView(inflater.inflate(R.layout.exo_list_divider, this, false));
|
addView(inflater.inflate(R.layout.exo_list_divider, this, false));
|
||||||
}
|
}
|
||||||
@ -270,11 +288,11 @@ public class TrackSelectionView extends LinearLayout {
|
|||||||
CheckedTextView trackView =
|
CheckedTextView trackView =
|
||||||
(CheckedTextView) inflater.inflate(trackViewLayoutId, this, false);
|
(CheckedTextView) inflater.inflate(trackViewLayoutId, this, false);
|
||||||
trackView.setBackgroundResource(selectableItemBackgroundResourceId);
|
trackView.setBackgroundResource(selectableItemBackgroundResourceId);
|
||||||
trackView.setText(trackNameProvider.getTrackName(group.getFormat(trackIndex)));
|
trackView.setText(trackNameProvider.getTrackName(trackInfos[trackIndex].format));
|
||||||
if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
|
if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
|
||||||
== RendererCapabilities.FORMAT_HANDLED) {
|
== RendererCapabilities.FORMAT_HANDLED) {
|
||||||
trackView.setFocusable(true);
|
trackView.setFocusable(true);
|
||||||
trackView.setTag(Pair.create(groupIndex, trackIndex));
|
trackView.setTag(trackInfos[trackIndex]);
|
||||||
trackView.setOnClickListener(componentListener);
|
trackView.setOnClickListener(componentListener);
|
||||||
} else {
|
} else {
|
||||||
trackView.setFocusable(false);
|
trackView.setFocusable(false);
|
||||||
@ -294,7 +312,12 @@ public class TrackSelectionView extends LinearLayout {
|
|||||||
for (int i = 0; i < trackViews.length; i++) {
|
for (int i = 0; i < trackViews.length; i++) {
|
||||||
SelectionOverride override = overrides.get(i);
|
SelectionOverride override = overrides.get(i);
|
||||||
for (int j = 0; j < trackViews[i].length; j++) {
|
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) {
|
private void onTrackViewClicked(View view) {
|
||||||
isDisabled = false;
|
isDisabled = false;
|
||||||
@SuppressWarnings("unchecked")
|
TrackInfo trackInfo = (TrackInfo) Assertions.checkNotNull(view.getTag());
|
||||||
Pair<Integer, Integer> tag = (Pair<Integer, Integer>) Assertions.checkNotNull(view.getTag());
|
int groupIndex = trackInfo.groupIndex;
|
||||||
int groupIndex = tag.first;
|
int trackIndex = trackInfo.trackIndex;
|
||||||
int trackIndex = tag.second;
|
|
||||||
SelectionOverride override = overrides.get(groupIndex);
|
SelectionOverride override = overrides.get(groupIndex);
|
||||||
Assertions.checkNotNull(mappedTrackInfo);
|
Assertions.checkNotNull(mappedTrackInfo);
|
||||||
if (override == null) {
|
if (override == null) {
|
||||||
@ -406,4 +428,16 @@ public class TrackSelectionView extends LinearLayout {
|
|||||||
TrackSelectionView.this.onClick(view);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user