mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Add PreloadMediaSource and PreloadMediaPeriod
The `PreloadMediaSource` has below two new public methods that suppose to be called by the app: * `preload(long)` allows the apps to preload the source at the passed start position before playback. The preload efforts include preparing the source for a `Timeline`, creating and caching a `MediaPeriod`, preparing the period, selecting tracks on the period and continuing loading the data on the period. * `releasePreloadMediaSource()` allows the apps to release the preloaded progress. The `PreloadMediaPeriod` is designed to facilitate the `PreloadMediaSource` for the preloading work. It has a new package-private method `selectTracksForPreloading` which will cache the `SampleStream` that corresponds to the track selection made during the preloading, and when the `PreloadMediaPeriod.selectTracks` is called for playback, it will uses the preloaded streams if the new selection is equal to the selection made during the preloading. Also add a shortform demo module to demo the usage of `PreloadMediaSource` with the short-form content use case. PiperOrigin-RevId: 574439529
This commit is contained in:
parent
ff4ff76990
commit
43e6882fb4
30 changed files with 3593 additions and 0 deletions
|
|
@ -6,6 +6,12 @@
|
||||||
* ExoPlayer:
|
* ExoPlayer:
|
||||||
* Add luma and chroma bitdepth to `ColorInfo`
|
* Add luma and chroma bitdepth to `ColorInfo`
|
||||||
[#491](https://github.com/androidx/media/pull/491).
|
[#491](https://github.com/androidx/media/pull/491).
|
||||||
|
* Add `PreloadMediaSource` and `PreloadMediaPeriod` that allows apps to
|
||||||
|
preload the media source at a specific start position before playback,
|
||||||
|
where the efforts include preparing the source for a `Timeline`,
|
||||||
|
preparing and caching the period, selecting tracks and loading the data
|
||||||
|
on the period. Apps are able to control the preload progress by
|
||||||
|
implementing `PreloadMediaSource.PreloadControl`.
|
||||||
* Transformer:
|
* Transformer:
|
||||||
* Add support for flattening H.265/HEVC SEF slow motion videos.
|
* Add support for flattening H.265/HEVC SEF slow motion videos.
|
||||||
* Track Selection:
|
* Track Selection:
|
||||||
|
|
@ -62,6 +68,9 @@
|
||||||
* Remove deprecated `DownloadNotificationHelper.buildProgressNotification`
|
* Remove deprecated `DownloadNotificationHelper.buildProgressNotification`
|
||||||
method, use a non deprecated method that takes a `notMetRequirements`
|
method, use a non deprecated method that takes a `notMetRequirements`
|
||||||
parameter instead.
|
parameter instead.
|
||||||
|
* Demo app:
|
||||||
|
* Add a shortform demo module to demo the usage of `PreloadMediaSource`
|
||||||
|
with the short-form content use case.
|
||||||
|
|
||||||
## 1.2
|
## 1.2
|
||||||
|
|
||||||
|
|
|
||||||
6
demos/shortform/README.md
Normal file
6
demos/shortform/README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Short form content demo
|
||||||
|
|
||||||
|
This app demonstrates usage of ExoPlayer in common short form content UI setups.
|
||||||
|
|
||||||
|
See the [demos README](../README.md) for instructions on how to build and run
|
||||||
|
this demo.
|
||||||
96
demos/shortform/build.gradle
Normal file
96
demos/shortform/build.gradle
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright 2023 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.
|
||||||
|
apply from: '../../constants.gradle'
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'androidx.media3.demo.shortform'
|
||||||
|
|
||||||
|
compileSdkVersion project.ext.compileSdkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
versionName project.ext.releaseVersion
|
||||||
|
versionCode project.ext.releaseVersionCode
|
||||||
|
minSdkVersion project.ext.minSdkVersion
|
||||||
|
targetSdkVersion project.ext.appTargetSdkVersion
|
||||||
|
multiDexEnabled true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
shrinkResources true
|
||||||
|
minifyEnabled true
|
||||||
|
proguardFiles = [
|
||||||
|
'proguard-rules.txt',
|
||||||
|
getDefaultProguardFile('proguard-android-optimize.txt')
|
||||||
|
]
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
jniDebuggable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
// The demo app isn't indexed, and doesn't have translations.
|
||||||
|
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
java {
|
||||||
|
srcDirs 'src/main/java'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test {
|
||||||
|
java {
|
||||||
|
srcDirs 'src/test/java'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'com.google.android.material:material:1.9.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
|
||||||
|
implementation 'androidx.core:core-ktx:' + androidxCoreVersion
|
||||||
|
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
|
||||||
|
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||||
|
implementation 'com.google.android.material:material:' + androidxMaterialVersion
|
||||||
|
implementation project(modulePrefix + 'lib-exoplayer')
|
||||||
|
implementation project(modulePrefix + 'lib-exoplayer-dash')
|
||||||
|
implementation project(modulePrefix + 'lib-exoplayer-hls')
|
||||||
|
implementation project(modulePrefix + 'lib-ui')
|
||||||
|
|
||||||
|
testImplementation 'androidx.test:core:' + androidxTestCoreVersion
|
||||||
|
testImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
|
||||||
|
testImplementation 'junit:junit:' + junitVersion
|
||||||
|
testImplementation 'com.google.truth:truth:' + truthVersion
|
||||||
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
|
}
|
||||||
2
demos/shortform/proguard-rules.txt
Normal file
2
demos/shortform/proguard-rules.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Proguard rules specific to the media3 short form content demo app.
|
||||||
|
|
||||||
46
demos/shortform/src/main/AndroidManifest.xml
Normal file
46
demos/shortform/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright 2023 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="androidx.media3.demo.shortform">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:name="androidx.multidex.MultiDexApplication"
|
||||||
|
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"
|
||||||
|
android:taskAffinity=""
|
||||||
|
tools:replace="android:name">
|
||||||
|
<activity
|
||||||
|
android:exported="true"
|
||||||
|
android:name=".MainActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/title_activity_view_pager"
|
||||||
|
android:name=".viewpager.ViewPagerActivity"/>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<uses-sdk />
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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 androidx.media3.demo.shortform
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.demo.shortform.viewpager.ViewPagerActivity
|
||||||
|
import java.lang.Integer.max
|
||||||
|
import java.lang.Integer.min
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
@androidx.annotation.OptIn(UnstableApi::class)
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
var numberOfPlayers = 3
|
||||||
|
val numPlayersFieldView = findViewById<EditText>(R.id.num_players_field)
|
||||||
|
numPlayersFieldView.addTextChangedListener(
|
||||||
|
object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit
|
||||||
|
|
||||||
|
override fun afterTextChanged(s: Editable) {
|
||||||
|
val newText = numPlayersFieldView.text.toString()
|
||||||
|
if (newText != "") {
|
||||||
|
numberOfPlayers = max(1, min(newText.toInt(), 5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var mediaItemsBackwardCacheSize = 2
|
||||||
|
val mediaItemsBCacheSizeView = findViewById<EditText>(R.id.media_items_b_cache_size)
|
||||||
|
mediaItemsBCacheSizeView.addTextChangedListener(
|
||||||
|
object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit
|
||||||
|
|
||||||
|
override fun afterTextChanged(s: Editable) {
|
||||||
|
val newText = mediaItemsBCacheSizeView.text.toString()
|
||||||
|
if (newText != "") {
|
||||||
|
mediaItemsBackwardCacheSize = max(1, min(newText.toInt(), 20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var mediaItemsForwardCacheSize = 3
|
||||||
|
val mediaItemsFCacheSizeView = findViewById<EditText>(R.id.media_items_f_cache_size)
|
||||||
|
mediaItemsFCacheSizeView.addTextChangedListener(
|
||||||
|
object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit
|
||||||
|
|
||||||
|
override fun afterTextChanged(s: Editable) {
|
||||||
|
val newText = mediaItemsFCacheSizeView.text.toString()
|
||||||
|
if (newText != "") {
|
||||||
|
mediaItemsForwardCacheSize = max(1, min(newText.toInt(), 20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
findViewById<View>(R.id.view_pager_button).setOnClickListener {
|
||||||
|
startActivity(
|
||||||
|
Intent(this, ViewPagerActivity::class.java)
|
||||||
|
.putExtra(NUM_PLAYERS_EXTRA, numberOfPlayers)
|
||||||
|
.putExtra(MEDIA_ITEMS_BACKWARD_CACHE_SIZE, mediaItemsBackwardCacheSize)
|
||||||
|
.putExtra(MEDIA_ITEMS_FORWARD_CACHE_SIZE, mediaItemsForwardCacheSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MEDIA_ITEMS_BACKWARD_CACHE_SIZE = "media_items_backward_cache_size"
|
||||||
|
const val MEDIA_ITEMS_FORWARD_CACHE_SIZE = "media_items_forward_cache_size"
|
||||||
|
const val NUM_PLAYERS_EXTRA = "number_of_players"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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
|
||||||
|
*
|
||||||
|
* https://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 androidx.media3.demo.shortform
|
||||||
|
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.util.Log
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
class MediaItemDatabase() {
|
||||||
|
|
||||||
|
var lCacheSize: Int = 2
|
||||||
|
var rCacheSize: Int = 7
|
||||||
|
private val mediaItems =
|
||||||
|
mutableListOf(
|
||||||
|
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_1.mp4"),
|
||||||
|
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_2.mp4"),
|
||||||
|
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_3.mp4"),
|
||||||
|
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_4.mp4"),
|
||||||
|
MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-0/shortform_5.mp4")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Effective sliding window of size = lCacheSize + 1 + rCacheSize
|
||||||
|
private val slidingWindowCache = HashMap<Int, MediaItem>()
|
||||||
|
|
||||||
|
private fun getRaw(index: Int): MediaItem {
|
||||||
|
return mediaItems[index.mod(mediaItems.size)]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCached(index: Int): MediaItem {
|
||||||
|
var mediaItem = slidingWindowCache[index]
|
||||||
|
if (mediaItem == null) {
|
||||||
|
mediaItem = getRaw(index)
|
||||||
|
slidingWindowCache[index] = mediaItem
|
||||||
|
Log.d("viewpager", "Put URL ${mediaItem.localConfiguration?.uri} into sliding cache")
|
||||||
|
slidingWindowCache.remove(index - lCacheSize - 1)
|
||||||
|
slidingWindowCache.remove(index + rCacheSize + 1)
|
||||||
|
}
|
||||||
|
return mediaItem
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(index: Int): MediaItem {
|
||||||
|
return getCached(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(fromIndex: Int, toIndex: Int): List<MediaItem> {
|
||||||
|
val result: MutableList<MediaItem> = mutableListOf()
|
||||||
|
for (i in fromIndex..toIndex) {
|
||||||
|
result.add(get(i))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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
|
||||||
|
*
|
||||||
|
* https://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 androidx.media3.demo.shortform
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.HandlerThread
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.Process
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.Metadata
|
||||||
|
import androidx.media3.common.text.CueGroup
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.common.util.Util
|
||||||
|
import androidx.media3.exoplayer.RendererCapabilities
|
||||||
|
import androidx.media3.exoplayer.RenderersFactory
|
||||||
|
import androidx.media3.exoplayer.analytics.PlayerId
|
||||||
|
import androidx.media3.exoplayer.audio.AudioRendererEventListener
|
||||||
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
|
import androidx.media3.exoplayer.source.PreloadMediaSource
|
||||||
|
import androidx.media3.exoplayer.trackselection.TrackSelector
|
||||||
|
import androidx.media3.exoplayer.upstream.Allocator
|
||||||
|
import androidx.media3.exoplayer.upstream.BandwidthMeter
|
||||||
|
import androidx.media3.exoplayer.video.VideoRendererEventListener
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
class MediaSourceManager(
|
||||||
|
mediaSourceFactory: MediaSource.Factory,
|
||||||
|
preloadLooper: Looper,
|
||||||
|
allocator: Allocator,
|
||||||
|
renderersFactory: RenderersFactory,
|
||||||
|
trackSelector: TrackSelector,
|
||||||
|
bandwidthMeter: BandwidthMeter,
|
||||||
|
) {
|
||||||
|
private val mediaSourcesThread = HandlerThread("playback-thread", Process.THREAD_PRIORITY_AUDIO)
|
||||||
|
private var handler: Handler
|
||||||
|
private var sourceMap: MutableMap<MediaItem, PreloadMediaSource> = HashMap()
|
||||||
|
private var preloadMediaSourceFactory: PreloadMediaSource.Factory
|
||||||
|
|
||||||
|
init {
|
||||||
|
mediaSourcesThread.start()
|
||||||
|
handler = Handler(mediaSourcesThread.looper)
|
||||||
|
trackSelector.init({}, bandwidthMeter)
|
||||||
|
preloadMediaSourceFactory =
|
||||||
|
PreloadMediaSource.Factory(
|
||||||
|
mediaSourceFactory,
|
||||||
|
PlayerId.UNSET,
|
||||||
|
PreloadControlImpl(targetPreloadPositionUs = 5_000_000L),
|
||||||
|
trackSelector,
|
||||||
|
bandwidthMeter,
|
||||||
|
getRendererCapabilities(renderersFactory = renderersFactory),
|
||||||
|
allocator,
|
||||||
|
preloadLooper
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(mediaItem: MediaItem) {
|
||||||
|
if (!sourceMap.containsKey(mediaItem)) {
|
||||||
|
val preloadMediaSource = preloadMediaSourceFactory.createMediaSource(mediaItem)
|
||||||
|
sourceMap[mediaItem] = preloadMediaSource
|
||||||
|
handler.post { preloadMediaSource.preload(/* startPositionUs= */ 0L) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAll(mediaItems: List<MediaItem>) {
|
||||||
|
mediaItems.forEach {
|
||||||
|
if (!sourceMap.containsKey(it)) {
|
||||||
|
add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun get(mediaItem: MediaItem): MediaSource {
|
||||||
|
if (!sourceMap.containsKey(mediaItem)) {
|
||||||
|
add(mediaItem)
|
||||||
|
}
|
||||||
|
return sourceMap[mediaItem]!!
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Releases the instance. The instance can't be used after being released. */
|
||||||
|
fun release() {
|
||||||
|
sourceMap.keys.forEach { sourceMap[it]!!.releasePreloadMediaSource() }
|
||||||
|
handler.removeCallbacksAndMessages(null)
|
||||||
|
mediaSourcesThread.quit()
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
private fun getRendererCapabilities(
|
||||||
|
renderersFactory: RenderersFactory
|
||||||
|
): Array<RendererCapabilities> {
|
||||||
|
val renderers =
|
||||||
|
renderersFactory.createRenderers(
|
||||||
|
Util.createHandlerForCurrentOrMainLooper(),
|
||||||
|
object : VideoRendererEventListener {},
|
||||||
|
object : AudioRendererEventListener {},
|
||||||
|
{ _: CueGroup? -> }
|
||||||
|
) { _: Metadata ->
|
||||||
|
}
|
||||||
|
val capabilities = ArrayList<RendererCapabilities>()
|
||||||
|
for (i in renderers.indices) {
|
||||||
|
capabilities.add(renderers[i].capabilities)
|
||||||
|
}
|
||||||
|
return capabilities.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "MSManager"
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PreloadControlImpl(private val targetPreloadPositionUs: Long) :
|
||||||
|
PreloadMediaSource.PreloadControl {
|
||||||
|
|
||||||
|
override fun onTimelineRefreshed(mediaSource: PreloadMediaSource): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepared(mediaSource: PreloadMediaSource): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onContinueLoadingRequested(
|
||||||
|
mediaSource: PreloadMediaSource,
|
||||||
|
bufferedPositionUs: Long
|
||||||
|
): Boolean {
|
||||||
|
return bufferedPositionUs < targetPreloadPositionUs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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 androidx.media3.demo.shortform
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.exoplayer.LoadControl
|
||||||
|
import androidx.media3.exoplayer.RenderersFactory
|
||||||
|
import androidx.media3.exoplayer.upstream.BandwidthMeter
|
||||||
|
import androidx.media3.exoplayer.util.EventLogger
|
||||||
|
import com.google.common.collect.BiMap
|
||||||
|
import com.google.common.collect.HashBiMap
|
||||||
|
import com.google.common.collect.Maps
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.Queue
|
||||||
|
|
||||||
|
class PlayerPool(
|
||||||
|
private val numberOfPlayers: Int,
|
||||||
|
context: Context,
|
||||||
|
playbackLooper: Looper,
|
||||||
|
loadControl: LoadControl,
|
||||||
|
renderersFactory: RenderersFactory,
|
||||||
|
bandwidthMeter: BandwidthMeter
|
||||||
|
) {
|
||||||
|
|
||||||
|
/** Creates a player instance to be used by the pool. */
|
||||||
|
interface PlayerFactory {
|
||||||
|
/** Creates an [ExoPlayer] instance. */
|
||||||
|
fun createPlayer(): ExoPlayer
|
||||||
|
}
|
||||||
|
|
||||||
|
private val availablePlayerQueue: Queue<Int> = LinkedList()
|
||||||
|
private val playerMap: BiMap<Int, ExoPlayer> = Maps.synchronizedBiMap(HashBiMap.create())
|
||||||
|
private val playerRequestTokenSet: MutableSet<Int> = Collections.synchronizedSet(HashSet<Int>())
|
||||||
|
private val playerFactory: PlayerFactory =
|
||||||
|
DefaultPlayerFactory(context, playbackLooper, loadControl, renderersFactory, bandwidthMeter)
|
||||||
|
|
||||||
|
fun acquirePlayer(token: Int, callback: (ExoPlayer) -> Unit) {
|
||||||
|
synchronized(playerMap) {
|
||||||
|
if (playerMap.size < numberOfPlayers) {
|
||||||
|
val player = playerFactory.createPlayer()
|
||||||
|
playerMap[playerMap.size] = player
|
||||||
|
callback.invoke(player)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Add token to set of views requesting players
|
||||||
|
playerRequestTokenSet.add(token)
|
||||||
|
acquirePlayerInternal(token, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun acquirePlayerInternal(token: Int, callback: (ExoPlayer) -> Unit) {
|
||||||
|
synchronized(playerMap) {
|
||||||
|
if (!availablePlayerQueue.isEmpty()) {
|
||||||
|
val playerNumber = availablePlayerQueue.remove()
|
||||||
|
playerMap[playerNumber]?.let { callback.invoke(it) }
|
||||||
|
playerRequestTokenSet.remove(token)
|
||||||
|
return
|
||||||
|
} else if (playerRequestTokenSet.contains(token)) {
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({ acquirePlayerInternal(token, callback) }, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun releasePlayer(token: Int, player: ExoPlayer?) {
|
||||||
|
synchronized(playerMap) {
|
||||||
|
// Remove token from set of views requesting players & remove potential callbacks
|
||||||
|
// trying to grab the player
|
||||||
|
playerRequestTokenSet.remove(token)
|
||||||
|
// Stop the player and release into the pool for reusing, do not player.release()
|
||||||
|
player?.stop()
|
||||||
|
player?.clearMediaItems()
|
||||||
|
if (player != null) {
|
||||||
|
val playerNumber = playerMap.inverse()[player]
|
||||||
|
availablePlayerQueue.add(playerNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun destroyPlayers() {
|
||||||
|
synchronized(playerMap) {
|
||||||
|
for (i in 0 until playerMap.size) {
|
||||||
|
playerMap[i]?.release()
|
||||||
|
playerMap.remove(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
private class DefaultPlayerFactory(
|
||||||
|
private val context: Context,
|
||||||
|
private val playbackLooper: Looper,
|
||||||
|
private val loadControl: LoadControl,
|
||||||
|
private val renderersFactory: RenderersFactory,
|
||||||
|
private val bandwidthMeter: BandwidthMeter
|
||||||
|
) : PlayerFactory {
|
||||||
|
private var playerCounter = 0
|
||||||
|
|
||||||
|
override fun createPlayer(): ExoPlayer {
|
||||||
|
val player =
|
||||||
|
ExoPlayer.Builder(context)
|
||||||
|
.setPlaybackLooper(playbackLooper)
|
||||||
|
.setLoadControl(loadControl)
|
||||||
|
.setRenderersFactory(renderersFactory)
|
||||||
|
.setBandwidthMeter(bandwidthMeter)
|
||||||
|
.build()
|
||||||
|
player.addAnalyticsListener(EventLogger("player-$playerCounter"))
|
||||||
|
playerCounter++
|
||||||
|
player.repeatMode = ExoPlayer.REPEAT_MODE_ONE
|
||||||
|
return player
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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 androidx.media3.demo.shortform.viewpager
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.demo.shortform.MainActivity
|
||||||
|
import androidx.media3.demo.shortform.MediaItemDatabase
|
||||||
|
import androidx.media3.demo.shortform.R
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
class ViewPagerActivity : AppCompatActivity() {
|
||||||
|
private lateinit var viewPagerView: ViewPager2
|
||||||
|
private lateinit var adapter: ViewPagerMediaAdapter
|
||||||
|
private var numberOfPlayers = 3
|
||||||
|
private var mediaItemDatabase = MediaItemDatabase()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_view_pager)
|
||||||
|
numberOfPlayers = intent.getIntExtra(MainActivity.NUM_PLAYERS_EXTRA, numberOfPlayers)
|
||||||
|
mediaItemDatabase.lCacheSize =
|
||||||
|
intent.getIntExtra(MainActivity.MEDIA_ITEMS_BACKWARD_CACHE_SIZE, mediaItemDatabase.lCacheSize)
|
||||||
|
mediaItemDatabase.rCacheSize =
|
||||||
|
intent.getIntExtra(MainActivity.MEDIA_ITEMS_FORWARD_CACHE_SIZE, mediaItemDatabase.rCacheSize)
|
||||||
|
Log.d("viewpager", "Using a pool of $numberOfPlayers players")
|
||||||
|
Log.d("viewpager", "Backward cache is of size: ${mediaItemDatabase.lCacheSize}")
|
||||||
|
Log.d("viewpager", "Forward cache is of size: ${mediaItemDatabase.rCacheSize}")
|
||||||
|
viewPagerView = findViewById(R.id.viewPager)
|
||||||
|
viewPagerView.registerOnPageChangeCallback(
|
||||||
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
|
override fun onPageScrolled(
|
||||||
|
position: Int,
|
||||||
|
positionOffset: Float,
|
||||||
|
positionOffsetPixels: Int
|
||||||
|
) {}
|
||||||
|
|
||||||
|
override fun onPageScrollStateChanged(state: Int) {
|
||||||
|
Log.d("viewpager", "onPageScrollStateChanged: state=$state")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageSelected(position: Int) {
|
||||||
|
Log.d("viewpager", "onPageSelected: position=$position")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
adapter = ViewPagerMediaAdapter(mediaItemDatabase, numberOfPlayers, this)
|
||||||
|
viewPagerView.adapter = adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
adapter.onDestroy()
|
||||||
|
super.onStop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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 androidx.media3.demo.shortform.viewpager
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.HandlerThread
|
||||||
|
import android.os.Process
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.datasource.DefaultDataSource
|
||||||
|
import androidx.media3.demo.shortform.MediaItemDatabase
|
||||||
|
import androidx.media3.demo.shortform.MediaSourceManager
|
||||||
|
import androidx.media3.demo.shortform.PlayerPool
|
||||||
|
import androidx.media3.demo.shortform.R
|
||||||
|
import androidx.media3.exoplayer.DefaultLoadControl
|
||||||
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||||
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||||
|
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
|
||||||
|
import androidx.media3.exoplayer.util.EventLogger
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
class ViewPagerMediaAdapter(
|
||||||
|
private val mediaItemDatabase: MediaItemDatabase,
|
||||||
|
numberOfPlayers: Int,
|
||||||
|
private val context: Context
|
||||||
|
) : RecyclerView.Adapter<ViewPagerMediaHolder>() {
|
||||||
|
private val playbackThread: HandlerThread =
|
||||||
|
HandlerThread("playback-thread", Process.THREAD_PRIORITY_AUDIO)
|
||||||
|
private val mediaSourceManager: MediaSourceManager
|
||||||
|
private var viewCounter = 0
|
||||||
|
private var playerPool: PlayerPool
|
||||||
|
|
||||||
|
init {
|
||||||
|
playbackThread.start()
|
||||||
|
val loadControl = DefaultLoadControl()
|
||||||
|
val renderersFactory = DefaultRenderersFactory(context)
|
||||||
|
playerPool =
|
||||||
|
PlayerPool(
|
||||||
|
numberOfPlayers,
|
||||||
|
context,
|
||||||
|
playbackThread.looper,
|
||||||
|
loadControl,
|
||||||
|
renderersFactory,
|
||||||
|
DefaultBandwidthMeter.getSingletonInstance(context)
|
||||||
|
)
|
||||||
|
mediaSourceManager =
|
||||||
|
MediaSourceManager(
|
||||||
|
ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context)),
|
||||||
|
playbackThread.looper,
|
||||||
|
loadControl.allocator,
|
||||||
|
renderersFactory,
|
||||||
|
DefaultTrackSelector(context),
|
||||||
|
DefaultBandwidthMeter.getSingletonInstance(context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewPagerMediaHolder {
|
||||||
|
Log.d("viewpager", "onCreateViewHolder: $viewCounter")
|
||||||
|
val view =
|
||||||
|
LayoutInflater.from(parent.context).inflate(R.layout.media_item_view_pager, parent, false)
|
||||||
|
val holder = ViewPagerMediaHolder(view, viewCounter++, playerPool)
|
||||||
|
view.addOnAttachStateChangeListener(holder)
|
||||||
|
return holder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewPagerMediaHolder, position: Int) {
|
||||||
|
// TODO could give more information to the database about which item to supply
|
||||||
|
// e.g. based on how long the previous item was in view (i.e. "popularity" of content)
|
||||||
|
// need to measure how long it's been since the last onBindViewHolder call
|
||||||
|
val mediaItem = mediaItemDatabase.get(position)
|
||||||
|
Log.d("viewpager", "onBindViewHolder: Getting item at position $position")
|
||||||
|
holder.bindData(position, mediaSourceManager[mediaItem])
|
||||||
|
// We are moving to <position>, so should prepare the next couple of items
|
||||||
|
// Potentially most of those are already cached on the database side because of the sliding
|
||||||
|
// window and we would only require one more item at index=mediaItemHorizon
|
||||||
|
val mediaItemHorizon = position + mediaItemDatabase.rCacheSize
|
||||||
|
val reachableMediaItems =
|
||||||
|
mediaItemDatabase.get(fromIndex = position + 1, toIndex = mediaItemHorizon)
|
||||||
|
// Same as with the data retrieval, most items will have been converted to MediaSources and
|
||||||
|
// prepared already, but not on the first swipe
|
||||||
|
mediaSourceManager.addAll(reachableMediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
// Effectively infinite scroll
|
||||||
|
return Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewRecycled(holder: ViewPagerMediaHolder) {
|
||||||
|
super.onViewRecycled(holder)
|
||||||
|
Log.d("viewpager", "Recycling the view")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDestroy() {
|
||||||
|
playbackThread.quit()
|
||||||
|
playerPool.destroyPlayers()
|
||||||
|
mediaSourceManager.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class Factory : PlayerPool.PlayerFactory {
|
||||||
|
private var playerCounter = 0
|
||||||
|
|
||||||
|
override fun createPlayer(): ExoPlayer {
|
||||||
|
val loadControl =
|
||||||
|
DefaultLoadControl.Builder()
|
||||||
|
.setBufferDurationsMs(
|
||||||
|
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
|
||||||
|
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
|
||||||
|
/* bufferForPlaybackMs= */ 0,
|
||||||
|
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
val player = ExoPlayer.Builder(context).setLoadControl(loadControl).build()
|
||||||
|
player.addAnalyticsListener(EventLogger("player-$playerCounter"))
|
||||||
|
playerCounter++
|
||||||
|
player.repeatMode = ExoPlayer.REPEAT_MODE_ONE
|
||||||
|
return player
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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
|
||||||
|
*
|
||||||
|
* https://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 androidx.media3.demo.shortform.viewpager
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.demo.shortform.PlayerPool
|
||||||
|
import androidx.media3.demo.shortform.R
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
|
import androidx.media3.ui.PlayerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
class ViewPagerMediaHolder(
|
||||||
|
itemView: View,
|
||||||
|
private val viewCounter: Int,
|
||||||
|
private val playerPool: PlayerPool
|
||||||
|
) : RecyclerView.ViewHolder(itemView), View.OnAttachStateChangeListener {
|
||||||
|
private val playerView: PlayerView = itemView.findViewById(R.id.player_view)
|
||||||
|
private var player: ExoPlayer? = null
|
||||||
|
private var isInView: Boolean = false
|
||||||
|
private var token: Int = -1
|
||||||
|
|
||||||
|
private lateinit var mediaSource: MediaSource
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Define click listener for the ViewHolder's View
|
||||||
|
playerView.findViewById<PlayerView>(R.id.player_view).setOnClickListener {
|
||||||
|
if (it is PlayerView) {
|
||||||
|
it.player?.run { playWhenReady = !playWhenReady }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
override fun onViewAttachedToWindow(view: View) {
|
||||||
|
Log.d("viewpager", "onViewAttachedToWindow: $viewCounter")
|
||||||
|
isInView = true
|
||||||
|
if (player == null) {
|
||||||
|
playerPool.acquirePlayer(token, ::setupPlayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewDetachedFromWindow(view: View) {
|
||||||
|
Log.d("viewpager", "onViewDetachedFromWindow: $viewCounter")
|
||||||
|
isInView = false
|
||||||
|
releasePlayer(player)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bindData(token: Int, mediaSource: MediaSource) {
|
||||||
|
this.mediaSource = mediaSource
|
||||||
|
this.token = token
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
fun releasePlayer(player: ExoPlayer?) {
|
||||||
|
playerPool.releasePlayer(token, player ?: this.player)
|
||||||
|
this.player = null
|
||||||
|
playerView.player = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
fun setupPlayer(player: ExoPlayer) {
|
||||||
|
if (!isInView) {
|
||||||
|
releasePlayer(player)
|
||||||
|
} else {
|
||||||
|
if (player != this.player) {
|
||||||
|
releasePlayer(this.player)
|
||||||
|
}
|
||||||
|
|
||||||
|
player.run {
|
||||||
|
repeatMode = ExoPlayer.REPEAT_MODE_ONE
|
||||||
|
setMediaSource(mediaSource)
|
||||||
|
seekTo(currentPosition)
|
||||||
|
playWhenReady = true
|
||||||
|
this@ViewPagerMediaHolder.player = player
|
||||||
|
player.prepare()
|
||||||
|
playerView.player = player
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
demos/shortform/src/main/proguard-rules.txt
Symbolic link
1
demos/shortform/src/main/proguard-rules.txt
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../../proguard-rules.txt
|
||||||
BIN
demos/shortform/src/main/res/drawable/placeholder.png
Normal file
BIN
demos/shortform/src/main/res/drawable/placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
78
demos/shortform/src/main/res/layout/activity_main.xml
Normal file
78
demos/shortform/src/main/res/layout/activity_main.xml
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright 2023 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.
|
||||||
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<Button android:id="@+id/view_pager_button"
|
||||||
|
android:text="@string/open_view_pager_activity"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="12dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginRight="12dp"/>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/num_players_field"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginTop="150dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginLeft="12dp"
|
||||||
|
android:layout_marginRight="12dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:background="@color/purple_700"
|
||||||
|
android:gravity="center"
|
||||||
|
android:hint="@string/num_of_players"
|
||||||
|
android:inputType="numberDecimal"
|
||||||
|
android:textColorHint="@color/grey" />
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/media_items_b_cache_size"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginLeft="12dp"
|
||||||
|
android:layout_marginRight="12dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:background="@color/purple_700"
|
||||||
|
android:gravity="center"
|
||||||
|
android:hint="@string/how_many_previous_videos_cached"
|
||||||
|
android:inputType="numberDecimal"
|
||||||
|
android:textColorHint="@color/grey" />
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/media_items_f_cache_size"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginLeft="12dp"
|
||||||
|
android:layout_marginRight="12dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:background="@color/purple_700"
|
||||||
|
android:gravity="center"
|
||||||
|
android:hint="@string/how_many_future_videos_cached"
|
||||||
|
android:inputType="numberDecimal"
|
||||||
|
android:textColorHint="@color/grey" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
29
demos/shortform/src/main/res/layout/activity_view_pager.xml
Normal file
29
demos/shortform/src/main/res/layout/activity_view_pager.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright 2023 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.
|
||||||
|
-->
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context=".viewpager.ViewPagerActivity">
|
||||||
|
|
||||||
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
|
android:id="@+id/viewPager"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright 2023 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context=".viewpager.ViewPagerActivity">
|
||||||
|
|
||||||
|
<androidx.media3.ui.PlayerView android:id="@+id/player_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:use_controller="false"
|
||||||
|
app:resize_mode="fill"
|
||||||
|
app:show_shuffle_button="true"
|
||||||
|
app:show_subtitle_button="true"/>
|
||||||
|
</LinearLayout>
|
||||||
BIN
demos/shortform/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
demos/shortform/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
demos/shortform/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
demos/shortform/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
demos/shortform/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
demos/shortform/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
BIN
demos/shortform/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
demos/shortform/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
BIN
demos/shortform/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
demos/shortform/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
30
demos/shortform/src/main/res/values/colors.xml
Normal file
30
demos/shortform/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright 2021 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="grey">#FF999999</color>
|
||||||
|
<color name="background">#292929</color>
|
||||||
|
<color name="player_background">#1c1c1c</color>
|
||||||
|
<color name="playlist_item_background">#363434</color>
|
||||||
|
<color name="playlist_item_foreground">#635E5E</color>
|
||||||
|
<color name="divider">#646464</color>
|
||||||
|
</resources>
|
||||||
23
demos/shortform/src/main/res/values/strings.xml
Normal file
23
demos/shortform/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Copyright 2021 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Media3 short-form content Demo</string>
|
||||||
|
<string name="open_view_pager_activity">Open view pager activity</string>
|
||||||
|
<string name="add_view_pager">Add view pager, please!</string>
|
||||||
|
<string name="title_activity_view_pager">ViewPager activity</string>
|
||||||
|
<string name="num_of_players">How Many Players?</string>
|
||||||
|
<string name="how_many_previous_videos_cached">How Many Previous Videos Cached</string>
|
||||||
|
<string name="how_many_future_videos_cached">How Many Future Videos Cached</string>
|
||||||
|
</resources>
|
||||||
33
demos/shortform/src/main/res/values/themes.xml
Normal file
33
demos/shortform/src/main/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright 2021 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.Media3Demo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/purple_500</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor" tools:targetApi="l">
|
||||||
|
?attr/colorPrimaryVariant
|
||||||
|
</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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
|
||||||
|
*
|
||||||
|
* https://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 androidx.media3.exoplayer.source;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.util.NullableType;
|
||||||
|
import androidx.media3.exoplayer.LoadingInfo;
|
||||||
|
import androidx.media3.exoplayer.SeekParameters;
|
||||||
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
|
import androidx.media3.exoplayer.trackselection.TrackSelectorResult;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/** A {@link MediaPeriod} that has data preloaded before playback. */
|
||||||
|
/* package */ final class PreloadMediaPeriod implements MediaPeriod {
|
||||||
|
|
||||||
|
public final MediaPeriod mediaPeriod;
|
||||||
|
|
||||||
|
private boolean prepareInternalCalled;
|
||||||
|
private boolean prepared;
|
||||||
|
@Nullable private Callback callback;
|
||||||
|
@Nullable private PreloadTrackSelectionHolder preloadTrackSelectionHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the {@link PreloadMediaPeriod}.
|
||||||
|
*
|
||||||
|
* @param mediaPeriod The wrapped {@link MediaPeriod}.
|
||||||
|
*/
|
||||||
|
public PreloadMediaPeriod(MediaPeriod mediaPeriod) {
|
||||||
|
this.mediaPeriod = mediaPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ void preload(Callback callback, long positionUs) {
|
||||||
|
this.callback = callback;
|
||||||
|
if (prepared) {
|
||||||
|
callback.onPrepared(PreloadMediaPeriod.this);
|
||||||
|
}
|
||||||
|
if (!prepareInternalCalled) {
|
||||||
|
prepareInternal(positionUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepare(Callback callback, long positionUs) {
|
||||||
|
this.callback = callback;
|
||||||
|
if (prepared) {
|
||||||
|
callback.onPrepared(PreloadMediaPeriod.this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!prepareInternalCalled) {
|
||||||
|
prepareInternal(positionUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareInternal(long positionUs) {
|
||||||
|
prepareInternalCalled = true;
|
||||||
|
mediaPeriod.prepare(
|
||||||
|
new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod mediaPeriod) {
|
||||||
|
checkNotNull(callback).onContinueLoadingRequested(PreloadMediaPeriod.this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||||
|
prepared = true;
|
||||||
|
checkNotNull(callback).onPrepared(PreloadMediaPeriod.this);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
positionUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void maybeThrowPrepareError() throws IOException {
|
||||||
|
mediaPeriod.maybeThrowPrepareError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TrackGroupArray getTrackGroups() {
|
||||||
|
return mediaPeriod.getTrackGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long selectTracks(
|
||||||
|
@NullableType ExoTrackSelection[] selections,
|
||||||
|
boolean[] mayRetainStreamFlags,
|
||||||
|
@NullableType SampleStream[] streams,
|
||||||
|
boolean[] streamResetFlags,
|
||||||
|
long positionUs) {
|
||||||
|
long trackSelectionPositionUs;
|
||||||
|
if (preloadTrackSelectionHolder != null
|
||||||
|
&& Arrays.equals(selections, preloadTrackSelectionHolder.trackSelectorResult.selections)
|
||||||
|
&& positionUs == preloadTrackSelectionHolder.trackSelectionPositionUs) {
|
||||||
|
trackSelectionPositionUs = preloadTrackSelectionHolder.trackSelectionPositionUs;
|
||||||
|
System.arraycopy(
|
||||||
|
preloadTrackSelectionHolder.streams,
|
||||||
|
0,
|
||||||
|
streams,
|
||||||
|
0,
|
||||||
|
preloadTrackSelectionHolder.streams.length);
|
||||||
|
System.arraycopy(
|
||||||
|
preloadTrackSelectionHolder.streamResetFlags,
|
||||||
|
0,
|
||||||
|
streamResetFlags,
|
||||||
|
0,
|
||||||
|
preloadTrackSelectionHolder.streamResetFlags.length);
|
||||||
|
} else {
|
||||||
|
trackSelectionPositionUs =
|
||||||
|
mediaPeriod.selectTracks(
|
||||||
|
selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs);
|
||||||
|
}
|
||||||
|
preloadTrackSelectionHolder = null;
|
||||||
|
return trackSelectionPositionUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ long selectTracksForPreloading(
|
||||||
|
TrackSelectorResult trackSelectorResult, long positionUs) {
|
||||||
|
@NullableType ExoTrackSelection[] selections = trackSelectorResult.selections;
|
||||||
|
@NullableType SampleStream[] preloadedSampleStreams = new SampleStream[selections.length];
|
||||||
|
boolean[] preloadedStreamResetFlags = new boolean[selections.length];
|
||||||
|
boolean[] mayRetainStreamFlags = new boolean[selections.length];
|
||||||
|
if (preloadTrackSelectionHolder != null) {
|
||||||
|
for (int i = 0; i < trackSelectorResult.length; i++) {
|
||||||
|
mayRetainStreamFlags[i] =
|
||||||
|
trackSelectorResult.isEquivalent(
|
||||||
|
checkNotNull(preloadTrackSelectionHolder).trackSelectorResult, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long trackSelectionPositionUs =
|
||||||
|
mediaPeriod.selectTracks(
|
||||||
|
selections,
|
||||||
|
mayRetainStreamFlags,
|
||||||
|
preloadedSampleStreams,
|
||||||
|
preloadedStreamResetFlags,
|
||||||
|
positionUs);
|
||||||
|
preloadTrackSelectionHolder =
|
||||||
|
new PreloadTrackSelectionHolder(
|
||||||
|
trackSelectorResult,
|
||||||
|
preloadedSampleStreams,
|
||||||
|
preloadedStreamResetFlags,
|
||||||
|
trackSelectionPositionUs);
|
||||||
|
return trackSelectionPositionUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void discardBuffer(long positionUs, boolean toKeyframe) {
|
||||||
|
mediaPeriod.discardBuffer(positionUs, toKeyframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long readDiscontinuity() {
|
||||||
|
return mediaPeriod.readDiscontinuity();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long seekToUs(long positionUs) {
|
||||||
|
return mediaPeriod.seekToUs(positionUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
|
||||||
|
return mediaPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getBufferedPositionUs() {
|
||||||
|
return mediaPeriod.getBufferedPositionUs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getNextLoadPositionUs() {
|
||||||
|
return mediaPeriod.getNextLoadPositionUs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean continueLoading(LoadingInfo loadingInfo) {
|
||||||
|
return mediaPeriod.continueLoading(loadingInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLoading() {
|
||||||
|
return mediaPeriod.isLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reevaluateBuffer(long positionUs) {
|
||||||
|
mediaPeriod.reevaluateBuffer(positionUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PreloadTrackSelectionHolder {
|
||||||
|
public final TrackSelectorResult trackSelectorResult;
|
||||||
|
public final @NullableType SampleStream[] streams;
|
||||||
|
public final boolean[] streamResetFlags;
|
||||||
|
public final long trackSelectionPositionUs;
|
||||||
|
|
||||||
|
public PreloadTrackSelectionHolder(
|
||||||
|
TrackSelectorResult trackSelectorResult,
|
||||||
|
@NullableType SampleStream[] streams,
|
||||||
|
boolean[] streamResetFlags,
|
||||||
|
long trackSelectionPositionUs) {
|
||||||
|
this.trackSelectorResult = trackSelectorResult;
|
||||||
|
this.streams = streams;
|
||||||
|
this.streamResetFlags = streamResetFlags;
|
||||||
|
this.trackSelectionPositionUs = trackSelectionPositionUs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,434 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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
|
||||||
|
*
|
||||||
|
* https://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 androidx.media3.exoplayer.source;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.Pair;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.Timeline;
|
||||||
|
import androidx.media3.common.util.Log;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.exoplayer.ExoPlaybackException;
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
|
import androidx.media3.exoplayer.LoadControl;
|
||||||
|
import androidx.media3.exoplayer.LoadingInfo;
|
||||||
|
import androidx.media3.exoplayer.RendererCapabilities;
|
||||||
|
import androidx.media3.exoplayer.RenderersFactory;
|
||||||
|
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||||
|
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
|
||||||
|
import androidx.media3.exoplayer.trackselection.TrackSelector;
|
||||||
|
import androidx.media3.exoplayer.trackselection.TrackSelectorResult;
|
||||||
|
import androidx.media3.exoplayer.upstream.Allocator;
|
||||||
|
import androidx.media3.exoplayer.upstream.BandwidthMeter;
|
||||||
|
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
|
||||||
|
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preloads a {@link MediaSource} and provides a {@link MediaPeriod} that has data loaded before
|
||||||
|
* playback.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public final class PreloadMediaSource extends WrappingMediaSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls preloading of {@link PreloadMediaSource}.
|
||||||
|
*
|
||||||
|
* <p>The methods are called on the {@link Looper} that is passed when constructing the {@link
|
||||||
|
* PreloadMediaSource.Factory}.
|
||||||
|
*/
|
||||||
|
public interface PreloadControl {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from {@link PreloadMediaSource} when the {@link Timeline} is refreshed.
|
||||||
|
*
|
||||||
|
* @param mediaSource The {@link PreloadMediaSource} that has its {@link Timeline} refreshed.
|
||||||
|
* @return True if the {@code mediaSource} should continue preloading, false otherwise.
|
||||||
|
*/
|
||||||
|
boolean onTimelineRefreshed(PreloadMediaSource mediaSource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from {@link PreloadMediaSource} when it is prepared.
|
||||||
|
*
|
||||||
|
* @param mediaSource The {@link PreloadMediaSource} it is prepared.
|
||||||
|
* @return True if the {@code mediaSource} should continue preloading, false otherwise.
|
||||||
|
*/
|
||||||
|
boolean onPrepared(PreloadMediaSource mediaSource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from {@link PreloadMediaSource} when it requests to continue loading.
|
||||||
|
*
|
||||||
|
* @param mediaSource The {@link PreloadMediaSource} that requests to continue loading.
|
||||||
|
* @param bufferedPositionUs An estimate of the absolute position in microseconds up to which
|
||||||
|
* data is buffered, or {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
|
||||||
|
*/
|
||||||
|
boolean onContinueLoadingRequested(PreloadMediaSource mediaSource, long bufferedPositionUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Factory for {@link PreloadMediaSource}. */
|
||||||
|
public static final class Factory implements MediaSource.Factory {
|
||||||
|
private final MediaSource.Factory mediaSourceFactory;
|
||||||
|
private final PlayerId playerId;
|
||||||
|
private final Looper preloadLooper;
|
||||||
|
private final Allocator allocator;
|
||||||
|
private final TrackSelector trackSelector;
|
||||||
|
private final BandwidthMeter bandwidthMeter;
|
||||||
|
private final RendererCapabilities[] rendererCapabilities;
|
||||||
|
private final PreloadControl preloadControl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new factory for {@link PreloadMediaSource}.
|
||||||
|
*
|
||||||
|
* @param mediaSourceFactory The underlying {@link MediaSource.Factory}.
|
||||||
|
* @param playerId The {@link PlayerId} of the {@link ExoPlayer} that will play the created
|
||||||
|
* {@link PreloadMediaSource} instances.
|
||||||
|
* @param preloadControl The {@link PreloadControl} that will control the progress of preloading
|
||||||
|
* the created {@link PreloadMediaSource} instances.
|
||||||
|
* @param trackSelector The {@link TrackSelector}. The instance passed should be {@link
|
||||||
|
* TrackSelector#init(TrackSelector.InvalidationListener, BandwidthMeter) initialized}.
|
||||||
|
* @param bandwidthMeter The {@link BandwidthMeter}. It should be the same bandwidth meter of
|
||||||
|
* the {@link ExoPlayer} that is injected by {@link
|
||||||
|
* ExoPlayer.Builder#setBandwidthMeter(BandwidthMeter)}.
|
||||||
|
* @param rendererCapabilities The array of {@link RendererCapabilities}. It should be derived
|
||||||
|
* from the same {@link RenderersFactory} of the {@link ExoPlayer} that is injected by
|
||||||
|
* {@link ExoPlayer.Builder#setRenderersFactory(RenderersFactory)}.
|
||||||
|
* @param allocator The {@link Allocator}. It should be the same allocator of the {@link
|
||||||
|
* ExoPlayer} that is injected by {@link ExoPlayer.Builder#setLoadControl(LoadControl)}.
|
||||||
|
* @param preloadLooper The {@link Looper} that will be used for preloading. It should be the
|
||||||
|
* same looper with {@link ExoPlayer.Builder#setPlaybackLooper(Looper)} that will play the
|
||||||
|
* created {@link PreloadMediaSource} instances.
|
||||||
|
*/
|
||||||
|
public Factory(
|
||||||
|
MediaSource.Factory mediaSourceFactory,
|
||||||
|
PlayerId playerId,
|
||||||
|
PreloadControl preloadControl,
|
||||||
|
TrackSelector trackSelector,
|
||||||
|
BandwidthMeter bandwidthMeter,
|
||||||
|
RendererCapabilities[] rendererCapabilities,
|
||||||
|
Allocator allocator,
|
||||||
|
Looper preloadLooper) {
|
||||||
|
this.mediaSourceFactory = mediaSourceFactory;
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.preloadControl = preloadControl;
|
||||||
|
this.trackSelector = trackSelector;
|
||||||
|
this.bandwidthMeter = bandwidthMeter;
|
||||||
|
this.rendererCapabilities = Arrays.copyOf(rendererCapabilities, rendererCapabilities.length);
|
||||||
|
this.allocator = allocator;
|
||||||
|
this.preloadLooper = preloadLooper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Factory setCmcdConfigurationFactory(CmcdConfiguration.Factory cmcdConfigurationFactory) {
|
||||||
|
this.mediaSourceFactory.setCmcdConfigurationFactory(cmcdConfigurationFactory);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Factory setDrmSessionManagerProvider(
|
||||||
|
DrmSessionManagerProvider drmSessionManagerProvider) {
|
||||||
|
this.mediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
|
||||||
|
this.mediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getSupportedTypes() {
|
||||||
|
return this.mediaSourceFactory.getSupportedTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PreloadMediaSource createMediaSource(MediaItem mediaItem) {
|
||||||
|
return new PreloadMediaSource(
|
||||||
|
mediaSourceFactory.createMediaSource(mediaItem),
|
||||||
|
playerId,
|
||||||
|
preloadControl,
|
||||||
|
trackSelector,
|
||||||
|
bandwidthMeter,
|
||||||
|
rendererCapabilities,
|
||||||
|
allocator,
|
||||||
|
preloadLooper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String TAG = "PreloadMediaSource";
|
||||||
|
|
||||||
|
private final PreloadControl preloadControl;
|
||||||
|
private final TrackSelector trackSelector;
|
||||||
|
private final BandwidthMeter bandwidthMeter;
|
||||||
|
private final RendererCapabilities[] rendererCapabilities;
|
||||||
|
private final Allocator allocator;
|
||||||
|
private final Handler preloadHandler;
|
||||||
|
private boolean preloadCalled;
|
||||||
|
private boolean prepareChildSourceCalled;
|
||||||
|
private long startPositionUs;
|
||||||
|
@Nullable private Timeline timeline;
|
||||||
|
@Nullable private Pair<PreloadMediaPeriod, MediaPeriodKey> preloadingMediaPeriodAndKey;
|
||||||
|
@Nullable private Pair<PreloadMediaPeriod, MediaPeriodId> playingPreloadedMediaPeriodAndId;
|
||||||
|
|
||||||
|
private PreloadMediaSource(
|
||||||
|
MediaSource mediaSource,
|
||||||
|
PlayerId playerId,
|
||||||
|
PreloadControl preloadControl,
|
||||||
|
TrackSelector trackSelector,
|
||||||
|
BandwidthMeter bandwidthMeter,
|
||||||
|
RendererCapabilities[] rendererCapabilities,
|
||||||
|
Allocator allocator,
|
||||||
|
Looper preloadLooper) {
|
||||||
|
super(mediaSource);
|
||||||
|
this.preloadControl = preloadControl;
|
||||||
|
this.trackSelector = trackSelector;
|
||||||
|
this.bandwidthMeter = bandwidthMeter;
|
||||||
|
this.rendererCapabilities = rendererCapabilities;
|
||||||
|
this.allocator = allocator;
|
||||||
|
|
||||||
|
preloadHandler = Util.createHandler(preloadLooper, /* callback= */ null);
|
||||||
|
startPositionUs = C.TIME_UNSET;
|
||||||
|
setPlayerId(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preloads the {@link PreloadMediaSource} for an expected start position {@code startPositionUs}.
|
||||||
|
*
|
||||||
|
* <p>Can be called from any thread.
|
||||||
|
*
|
||||||
|
* @param startPositionUs The expected starting position in microseconds, or {@link C#TIME_UNSET}
|
||||||
|
* to indicate the default start position.
|
||||||
|
*/
|
||||||
|
public void preload(long startPositionUs) {
|
||||||
|
preloadHandler.post(
|
||||||
|
() -> {
|
||||||
|
preloadCalled = true;
|
||||||
|
this.startPositionUs = startPositionUs;
|
||||||
|
if (!isUsedByPlayer()) {
|
||||||
|
prepareSourceInternal(bandwidthMeter.getTransferListener());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void prepareSourceInternal() {
|
||||||
|
if (timeline != null) {
|
||||||
|
onChildSourceInfoRefreshed(timeline);
|
||||||
|
} else if (!prepareChildSourceCalled) {
|
||||||
|
prepareChildSourceCalled = true;
|
||||||
|
prepareChildSource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onChildSourceInfoRefreshed(Timeline newTimeline) {
|
||||||
|
this.timeline = newTimeline;
|
||||||
|
refreshSourceInfo(newTimeline);
|
||||||
|
if (isUsedByPlayer() || !preloadControl.onTimelineRefreshed(PreloadMediaSource.this)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Pair<Object, Long> periodPosition =
|
||||||
|
newTimeline.getPeriodPositionUs(
|
||||||
|
new Timeline.Window(),
|
||||||
|
new Timeline.Period(),
|
||||||
|
/* windowIndex= */ 0,
|
||||||
|
/* windowPositionUs= */ startPositionUs);
|
||||||
|
MediaPeriodId mediaPeriodId = new MediaPeriodId(periodPosition.first);
|
||||||
|
PreloadMediaPeriod mediaPeriod =
|
||||||
|
PreloadMediaSource.this.createPeriod(mediaPeriodId, allocator, periodPosition.second);
|
||||||
|
mediaPeriod.preload(
|
||||||
|
new PreloadMediaPeriodCallback(periodPosition.second),
|
||||||
|
/* positionUs= */ periodPosition.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PreloadMediaPeriod createPeriod(
|
||||||
|
MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||||
|
MediaPeriodKey key = new MediaPeriodKey(id, startPositionUs);
|
||||||
|
if (preloadingMediaPeriodAndKey != null && key.equals(preloadingMediaPeriodAndKey.second)) {
|
||||||
|
PreloadMediaPeriod mediaPeriod = checkNotNull(preloadingMediaPeriodAndKey).first;
|
||||||
|
if (isUsedByPlayer()) {
|
||||||
|
preloadingMediaPeriodAndKey = null;
|
||||||
|
playingPreloadedMediaPeriodAndId = new Pair<>(mediaPeriod, id);
|
||||||
|
}
|
||||||
|
return mediaPeriod;
|
||||||
|
} else if (preloadingMediaPeriodAndKey != null) {
|
||||||
|
mediaSource.releasePeriod(checkNotNull(preloadingMediaPeriodAndKey).first.mediaPeriod);
|
||||||
|
preloadingMediaPeriodAndKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PreloadMediaPeriod mediaPeriod =
|
||||||
|
new PreloadMediaPeriod(mediaSource.createPeriod(id, allocator, startPositionUs));
|
||||||
|
if (!isUsedByPlayer()) {
|
||||||
|
preloadingMediaPeriodAndKey = new Pair<>(mediaPeriod, key);
|
||||||
|
}
|
||||||
|
return mediaPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(MediaPeriodId mediaPeriodId) {
|
||||||
|
if (playingPreloadedMediaPeriodAndId != null
|
||||||
|
&& mediaPeriodIdEqualsWithoutWindowSequenceNumber(
|
||||||
|
mediaPeriodId, checkNotNull(playingPreloadedMediaPeriodAndId).second)) {
|
||||||
|
return checkNotNull(playingPreloadedMediaPeriodAndId).second;
|
||||||
|
}
|
||||||
|
return mediaPeriodId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||||
|
PreloadMediaPeriod preloadMediaPeriod = ((PreloadMediaPeriod) mediaPeriod);
|
||||||
|
if (preloadingMediaPeriodAndKey != null
|
||||||
|
&& preloadMediaPeriod == checkNotNull(preloadingMediaPeriodAndKey).first) {
|
||||||
|
preloadingMediaPeriodAndKey = null;
|
||||||
|
} else if (playingPreloadedMediaPeriodAndId != null
|
||||||
|
&& preloadMediaPeriod == checkNotNull(playingPreloadedMediaPeriodAndId).first) {
|
||||||
|
playingPreloadedMediaPeriodAndId = null;
|
||||||
|
}
|
||||||
|
MediaPeriod periodToRelease = preloadMediaPeriod.mediaPeriod;
|
||||||
|
mediaSource.releasePeriod(periodToRelease);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void releaseSourceInternal() {
|
||||||
|
if (!preloadCalled && !isUsedByPlayer()) {
|
||||||
|
timeline = null;
|
||||||
|
prepareChildSourceCalled = false;
|
||||||
|
super.releaseSourceInternal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases the preloaded resources in {@link PreloadMediaSource}.
|
||||||
|
*
|
||||||
|
* <p>Can be called from any thread.
|
||||||
|
*/
|
||||||
|
public void releasePreloadMediaSource() {
|
||||||
|
preloadHandler.post(
|
||||||
|
() -> {
|
||||||
|
preloadCalled = false;
|
||||||
|
startPositionUs = C.TIME_UNSET;
|
||||||
|
if (preloadingMediaPeriodAndKey != null) {
|
||||||
|
mediaSource.releasePeriod(preloadingMediaPeriodAndKey.first.mediaPeriod);
|
||||||
|
preloadingMediaPeriodAndKey = null;
|
||||||
|
}
|
||||||
|
releaseSourceInternal();
|
||||||
|
preloadHandler.removeCallbacksAndMessages(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PreloadMediaPeriodCallback implements MediaPeriod.Callback {
|
||||||
|
|
||||||
|
private final long periodStartPositionUs;
|
||||||
|
private boolean prepared;
|
||||||
|
|
||||||
|
public PreloadMediaPeriodCallback(long periodStartPositionUs) {
|
||||||
|
this.periodStartPositionUs = periodStartPositionUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||||
|
prepared = true;
|
||||||
|
PreloadMediaPeriod preloadMediaPeriod = (PreloadMediaPeriod) mediaPeriod;
|
||||||
|
TrackGroupArray trackGroups = preloadMediaPeriod.getTrackGroups();
|
||||||
|
@Nullable TrackSelectorResult trackSelectorResult = null;
|
||||||
|
MediaPeriodKey key = checkNotNull(preloadingMediaPeriodAndKey).second;
|
||||||
|
try {
|
||||||
|
trackSelectorResult =
|
||||||
|
trackSelector.selectTracks(
|
||||||
|
rendererCapabilities, trackGroups, key.mediaPeriodId, checkNotNull(timeline));
|
||||||
|
} catch (ExoPlaybackException e) {
|
||||||
|
Log.e(TAG, "Failed to select tracks", e);
|
||||||
|
}
|
||||||
|
if (trackSelectorResult != null) {
|
||||||
|
preloadMediaPeriod.selectTracksForPreloading(trackSelectorResult, periodStartPositionUs);
|
||||||
|
if (preloadControl.onPrepared(PreloadMediaSource.this)) {
|
||||||
|
preloadMediaPeriod.continueLoading(
|
||||||
|
new LoadingInfo.Builder().setPlaybackPositionUs(periodStartPositionUs).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod mediaPeriod) {
|
||||||
|
PreloadMediaPeriod preloadMediaPeriod = (PreloadMediaPeriod) mediaPeriod;
|
||||||
|
if (!prepared
|
||||||
|
|| preloadControl.onContinueLoadingRequested(
|
||||||
|
PreloadMediaSource.this, preloadMediaPeriod.getBufferedPositionUs())) {
|
||||||
|
preloadMediaPeriod.continueLoading(
|
||||||
|
new LoadingInfo.Builder().setPlaybackPositionUs(periodStartPositionUs).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isUsedByPlayer() {
|
||||||
|
return prepareSourceCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean mediaPeriodIdEqualsWithoutWindowSequenceNumber(
|
||||||
|
MediaPeriodId firstPeriodId, MediaPeriodId secondPeriodId) {
|
||||||
|
return firstPeriodId.periodUid.equals(secondPeriodId.periodUid)
|
||||||
|
&& firstPeriodId.adGroupIndex == secondPeriodId.adGroupIndex
|
||||||
|
&& firstPeriodId.adIndexInAdGroup == secondPeriodId.adIndexInAdGroup
|
||||||
|
&& firstPeriodId.nextAdGroupIndex == secondPeriodId.nextAdGroupIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MediaPeriodKey {
|
||||||
|
|
||||||
|
public final MediaSource.MediaPeriodId mediaPeriodId;
|
||||||
|
private final Long startPositionUs;
|
||||||
|
|
||||||
|
public MediaPeriodKey(MediaSource.MediaPeriodId mediaPeriodId, long startPositionUs) {
|
||||||
|
this.mediaPeriodId = mediaPeriodId;
|
||||||
|
this.startPositionUs = startPositionUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object other) {
|
||||||
|
if (this == other) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(other instanceof MediaPeriodKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
MediaPeriodKey mediaPeriodKey = (MediaPeriodKey) other;
|
||||||
|
// The MediaPeriodId.windowSequenceNumber is intentionally left out of equals to ensure we
|
||||||
|
// detect the "same" media even if it's used with a different sequence number.
|
||||||
|
return mediaPeriodIdEqualsWithoutWindowSequenceNumber(
|
||||||
|
this.mediaPeriodId, mediaPeriodKey.mediaPeriodId)
|
||||||
|
&& startPositionUs.equals(mediaPeriodKey.startPositionUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
// The MediaPeriodId.windowSequenceNumber is intentionally left out of hashCode to ensure we
|
||||||
|
// detect the "same" media even if it's used with a different sequence number.
|
||||||
|
int result = 17;
|
||||||
|
result = 31 * result + mediaPeriodId.periodUid.hashCode();
|
||||||
|
result = 31 * result + mediaPeriodId.adGroupIndex;
|
||||||
|
result = 31 * result + mediaPeriodId.adIndexInAdGroup;
|
||||||
|
result = 31 * result + mediaPeriodId.nextAdGroupIndex;
|
||||||
|
result = 31 * result + startPositionUs.intValue();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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
|
||||||
|
*
|
||||||
|
* https://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 androidx.media3.exoplayer.source;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Pair;
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.Timeline;
|
||||||
|
import androidx.media3.common.util.SystemClock;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.datasource.TransferListener;
|
||||||
|
import androidx.media3.exoplayer.Renderer;
|
||||||
|
import androidx.media3.exoplayer.RendererCapabilities;
|
||||||
|
import androidx.media3.exoplayer.RenderersFactory;
|
||||||
|
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||||
|
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
|
||||||
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
|
||||||
|
import androidx.media3.exoplayer.trackselection.TrackSelector;
|
||||||
|
import androidx.media3.exoplayer.upstream.Allocator;
|
||||||
|
import androidx.media3.exoplayer.upstream.BandwidthMeter;
|
||||||
|
import androidx.media3.exoplayer.upstream.DefaultAllocator;
|
||||||
|
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter;
|
||||||
|
import androidx.media3.exoplayer.video.VideoRendererEventListener;
|
||||||
|
import androidx.media3.test.utils.FakeAudioRenderer;
|
||||||
|
import androidx.media3.test.utils.FakeMediaPeriod;
|
||||||
|
import androidx.media3.test.utils.FakeMediaSource;
|
||||||
|
import androidx.media3.test.utils.FakeMediaSourceFactory;
|
||||||
|
import androidx.media3.test.utils.FakeVideoRenderer;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.shadows.ShadowLooper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the coordination behaviours when the player starts to prepare the {@link
|
||||||
|
* PreloadMediaSource} while it is in the different preload stages. For example, as long as the
|
||||||
|
* player calls {@link PreloadMediaSource#prepareSource(MediaSource.MediaSourceCaller,
|
||||||
|
* TransferListener, PlayerId)}, the rest of the preload logic shouldn't proceed.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class PreloadAndPlaybackCoordinationTest {
|
||||||
|
|
||||||
|
private final PlayerId playerId;
|
||||||
|
private final BandwidthMeter bandwidthMeter;
|
||||||
|
private final PreloadMediaSource preloadMediaSource;
|
||||||
|
private final FakeMediaSource wrappedMediaSource;
|
||||||
|
private final MediaSource.MediaSourceCaller playbackMediaSourceCaller;
|
||||||
|
|
||||||
|
private final AtomicBoolean preloadControlOnSourceInfoRefreshedCalled;
|
||||||
|
private final AtomicBoolean preloadControlOnPreparedCalled;
|
||||||
|
private final AtomicBoolean playbackSourceCallerOnSourceInfoRefreshedCalled;
|
||||||
|
private final AtomicBoolean playbackPeriodCallbackOnPreparedCalled;
|
||||||
|
|
||||||
|
public PreloadAndPlaybackCoordinationTest() {
|
||||||
|
playerId = new PlayerId();
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
|
||||||
|
FakeMediaSourceFactory mediaSourceFactory = new FakeMediaSourceFactory();
|
||||||
|
Allocator allocator =
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||||
|
TrackSelector trackSelector = new DefaultTrackSelector(context);
|
||||||
|
trackSelector.init(() -> {}, bandwidthMeter);
|
||||||
|
RenderersFactory renderersFactory =
|
||||||
|
(handler, videoListener, audioListener, textOutput, metadataOutput) ->
|
||||||
|
new Renderer[] {
|
||||||
|
new FakeVideoRenderer(
|
||||||
|
SystemClock.DEFAULT.createHandler(handler.getLooper(), /* callback= */ null),
|
||||||
|
videoListener),
|
||||||
|
new FakeAudioRenderer(
|
||||||
|
SystemClock.DEFAULT.createHandler(handler.getLooper(), /* callback= */ null),
|
||||||
|
audioListener)
|
||||||
|
};
|
||||||
|
preloadControlOnSourceInfoRefreshedCalled = new AtomicBoolean();
|
||||||
|
preloadControlOnPreparedCalled = new AtomicBoolean();
|
||||||
|
playbackSourceCallerOnSourceInfoRefreshedCalled = new AtomicBoolean();
|
||||||
|
playbackPeriodCallbackOnPreparedCalled = new AtomicBoolean();
|
||||||
|
|
||||||
|
PreloadMediaSource.PreloadControl preloadControl =
|
||||||
|
new PreloadMediaSource.PreloadControl() {
|
||||||
|
@Override
|
||||||
|
public boolean onTimelineRefreshed(PreloadMediaSource mediaSource) {
|
||||||
|
preloadControlOnSourceInfoRefreshedCalled.set(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPrepared(PreloadMediaSource mediaSource) {
|
||||||
|
preloadControlOnPreparedCalled.set(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onContinueLoadingRequested(
|
||||||
|
PreloadMediaSource mediaSource, long bufferedPositionUs) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PreloadMediaSource.Factory preloadMediaSourceFactory =
|
||||||
|
new PreloadMediaSource.Factory(
|
||||||
|
mediaSourceFactory,
|
||||||
|
playerId,
|
||||||
|
preloadControl,
|
||||||
|
trackSelector,
|
||||||
|
bandwidthMeter,
|
||||||
|
getRendererCapabilities(renderersFactory),
|
||||||
|
allocator,
|
||||||
|
/* preloadLooper= */ Util.getCurrentOrMainLooper());
|
||||||
|
preloadMediaSource =
|
||||||
|
preloadMediaSourceFactory.createMediaSource(
|
||||||
|
MediaItem.fromUri("asset://android_asset/media/mp4/sample.mp4"));
|
||||||
|
wrappedMediaSource = mediaSourceFactory.getLastCreatedSource();
|
||||||
|
|
||||||
|
MediaPeriod.Callback playbackMediaPeriodCallback =
|
||||||
|
new MediaPeriod.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||||
|
playbackPeriodCallbackOnPreparedCalled.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||||
|
};
|
||||||
|
playbackMediaSourceCaller =
|
||||||
|
(source, timeline) -> {
|
||||||
|
playbackSourceCallerOnSourceInfoRefreshedCalled.set(true);
|
||||||
|
Pair<Object, Long> periodPosition =
|
||||||
|
timeline.getPeriodPositionUs(
|
||||||
|
new Timeline.Window(),
|
||||||
|
new Timeline.Period(),
|
||||||
|
/* windowIndex= */ 0,
|
||||||
|
/* windowPositionUs= */ 0L);
|
||||||
|
MediaSource.MediaPeriodId mediaPeriodId =
|
||||||
|
new MediaSource.MediaPeriodId(periodPosition.first);
|
||||||
|
MediaPeriod mediaPeriod =
|
||||||
|
source.createPeriod(mediaPeriodId, allocator, periodPosition.second);
|
||||||
|
mediaPeriod.prepare(playbackMediaPeriodCallback, /* positionUs= */ 0L);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playbackPrepareSource_beforePreloadStart() {
|
||||||
|
preloadMediaSource.prepareSource(
|
||||||
|
playbackMediaSourceCaller, bandwidthMeter.getTransferListener(), playerId);
|
||||||
|
preloadMediaSource.preload(/* startPositionUs= */ 0L);
|
||||||
|
ShadowLooper.idleMainLooper();
|
||||||
|
|
||||||
|
assertThat(preloadControlOnSourceInfoRefreshedCalled.get()).isFalse();
|
||||||
|
assertThat(preloadControlOnPreparedCalled.get()).isFalse();
|
||||||
|
assertThat(playbackSourceCallerOnSourceInfoRefreshedCalled.get()).isTrue();
|
||||||
|
assertThat(playbackPeriodCallbackOnPreparedCalled.get()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playbackPrepareSource_betweenPreloadStartAndTimelineInfoRefreshed() {
|
||||||
|
wrappedMediaSource.setAllowPreparation(false);
|
||||||
|
|
||||||
|
preloadMediaSource.preload(/* startPositionUs= */ 0L);
|
||||||
|
ShadowLooper.idleMainLooper();
|
||||||
|
preloadMediaSource.prepareSource(
|
||||||
|
playbackMediaSourceCaller, bandwidthMeter.getTransferListener(), playerId);
|
||||||
|
wrappedMediaSource.setAllowPreparation(true);
|
||||||
|
ShadowLooper.idleMainLooper();
|
||||||
|
|
||||||
|
assertThat(preloadControlOnSourceInfoRefreshedCalled.get()).isFalse();
|
||||||
|
assertThat(preloadControlOnPreparedCalled.get()).isFalse();
|
||||||
|
assertThat(playbackSourceCallerOnSourceInfoRefreshedCalled.get()).isTrue();
|
||||||
|
assertThat(playbackPeriodCallbackOnPreparedCalled.get()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playbackPrepareSource_betweenTimelineInfoRefreshedAndPeriodPrepared() {
|
||||||
|
wrappedMediaSource.setPeriodDefersOnPreparedCallback(true);
|
||||||
|
|
||||||
|
preloadMediaSource.preload(/* startPositionUs= */ 0L);
|
||||||
|
ShadowLooper.idleMainLooper();
|
||||||
|
preloadMediaSource.prepareSource(
|
||||||
|
playbackMediaSourceCaller, bandwidthMeter.getTransferListener(), playerId);
|
||||||
|
FakeMediaPeriod lastCreatedActiveMediaPeriod =
|
||||||
|
(FakeMediaPeriod) wrappedMediaSource.getLastCreatedActiveMediaPeriod();
|
||||||
|
lastCreatedActiveMediaPeriod.setPreparationComplete();
|
||||||
|
ShadowLooper.idleMainLooper();
|
||||||
|
|
||||||
|
assertThat(preloadControlOnSourceInfoRefreshedCalled.get()).isTrue();
|
||||||
|
assertThat(preloadControlOnPreparedCalled.get()).isFalse();
|
||||||
|
assertThat(playbackSourceCallerOnSourceInfoRefreshedCalled.get()).isTrue();
|
||||||
|
assertThat(playbackPeriodCallbackOnPreparedCalled.get()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playbackPrepareSource_afterPeriodContinueLoading() {
|
||||||
|
preloadMediaSource.preload(/* startPositionUs= */ 0L);
|
||||||
|
ShadowLooper.idleMainLooper();
|
||||||
|
preloadMediaSource.prepareSource(
|
||||||
|
playbackMediaSourceCaller, bandwidthMeter.getTransferListener(), playerId);
|
||||||
|
ShadowLooper.idleMainLooper();
|
||||||
|
|
||||||
|
assertThat(preloadControlOnSourceInfoRefreshedCalled.get()).isTrue();
|
||||||
|
assertThat(preloadControlOnPreparedCalled.get()).isTrue();
|
||||||
|
assertThat(playbackSourceCallerOnSourceInfoRefreshedCalled.get()).isTrue();
|
||||||
|
assertThat(playbackPeriodCallbackOnPreparedCalled.get()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RendererCapabilities[] getRendererCapabilities(RenderersFactory renderersFactory) {
|
||||||
|
Renderer[] renderers =
|
||||||
|
renderersFactory.createRenderers(
|
||||||
|
Util.createHandlerForCurrentLooper(),
|
||||||
|
new VideoRendererEventListener() {},
|
||||||
|
new AudioRendererEventListener() {},
|
||||||
|
cueGroup -> {},
|
||||||
|
metadata -> {});
|
||||||
|
RendererCapabilities[] rendererCapabilities = new RendererCapabilities[renderers.length];
|
||||||
|
for (int i = 0; i < renderers.length; i++) {
|
||||||
|
rendererCapabilities[i] = renderers[i].getCapabilities();
|
||||||
|
}
|
||||||
|
return rendererCapabilities;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,474 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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
|
||||||
|
*
|
||||||
|
* https://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 androidx.media3.exoplayer.source;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.robolectric.Shadows.shadowOf;
|
||||||
|
|
||||||
|
import android.os.Looper;
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.Format;
|
||||||
|
import androidx.media3.common.MimeTypes;
|
||||||
|
import androidx.media3.common.TrackGroup;
|
||||||
|
import androidx.media3.common.Tracks;
|
||||||
|
import androidx.media3.exoplayer.RendererConfiguration;
|
||||||
|
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
|
||||||
|
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
||||||
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
|
import androidx.media3.exoplayer.trackselection.FixedTrackSelection;
|
||||||
|
import androidx.media3.exoplayer.trackselection.TrackSelectorResult;
|
||||||
|
import androidx.media3.exoplayer.upstream.DefaultAllocator;
|
||||||
|
import androidx.media3.test.utils.FakeMediaPeriod;
|
||||||
|
import androidx.media3.test.utils.FakeTimeline;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Unit test for {@link PreloadMediaPeriod}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public final class PreloadMediaPeriodTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preload_prepareWrappedPeriodAndInvokeCallbackOnPrepared() {
|
||||||
|
MediaSource.MediaPeriodId mediaPeriodId =
|
||||||
|
new MediaSource.MediaPeriodId(/* periodUid= */ new Object());
|
||||||
|
FakeMediaPeriod wrappedMediaPeriod =
|
||||||
|
new FakeMediaPeriod(
|
||||||
|
TrackGroupArray.EMPTY,
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
|
||||||
|
FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
|
||||||
|
new MediaSourceEventListener.EventDispatcher()
|
||||||
|
.withParameters(/* windowIndex= */ 0, mediaPeriodId));
|
||||||
|
PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod);
|
||||||
|
AtomicBoolean onPreparedCallbackCalled = new AtomicBoolean();
|
||||||
|
AtomicReference<MediaPeriod> mediaPeriodReference = new AtomicReference<>();
|
||||||
|
|
||||||
|
preloadMediaPeriod.preload(
|
||||||
|
new MediaPeriod.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||||
|
mediaPeriodReference.set(mediaPeriod);
|
||||||
|
onPreparedCallbackCalled.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||||
|
},
|
||||||
|
/* positionUs= */ 0L);
|
||||||
|
|
||||||
|
// Wrapped media period is called `prepare` for once.
|
||||||
|
assertThat(onPreparedCallbackCalled.get()).isTrue();
|
||||||
|
assertThat(mediaPeriodReference.get()).isSameInstanceAs(preloadMediaPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepareBeforePreload_prepareWrappedPeriodAndInvokeCallbackOnPrepared() {
|
||||||
|
MediaSource.MediaPeriodId mediaPeriodId =
|
||||||
|
new MediaSource.MediaPeriodId(/* periodUid= */ new Object());
|
||||||
|
FakeMediaPeriod wrappedMediaPeriod =
|
||||||
|
new FakeMediaPeriod(
|
||||||
|
TrackGroupArray.EMPTY,
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
|
||||||
|
FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
|
||||||
|
new MediaSourceEventListener.EventDispatcher()
|
||||||
|
.withParameters(/* windowIndex= */ 0, mediaPeriodId));
|
||||||
|
PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod);
|
||||||
|
AtomicBoolean onPreparedCallbackCalled = new AtomicBoolean();
|
||||||
|
AtomicReference<MediaPeriod> mediaPeriodReference = new AtomicReference<>();
|
||||||
|
|
||||||
|
preloadMediaPeriod.prepare(
|
||||||
|
new MediaPeriod.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||||
|
mediaPeriodReference.set(mediaPeriod);
|
||||||
|
onPreparedCallbackCalled.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||||
|
},
|
||||||
|
/* positionUs= */ 0L);
|
||||||
|
|
||||||
|
assertThat(onPreparedCallbackCalled.get()).isTrue();
|
||||||
|
assertThat(mediaPeriodReference.get()).isSameInstanceAs(preloadMediaPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepareBeforeWrappedPeriodPreparedByPreloading_invokeLatestCallbackOnPrepared() {
|
||||||
|
MediaSource.MediaPeriodId mediaPeriodId =
|
||||||
|
new MediaSource.MediaPeriodId(/* periodUid= */ new Object());
|
||||||
|
FakeMediaPeriod wrappedMediaPeriod =
|
||||||
|
new FakeMediaPeriod(
|
||||||
|
TrackGroupArray.EMPTY,
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
|
||||||
|
FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
|
||||||
|
new MediaSourceEventListener.EventDispatcher()
|
||||||
|
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
|
||||||
|
DrmSessionManager.DRM_UNSUPPORTED,
|
||||||
|
new DrmSessionEventListener.EventDispatcher()
|
||||||
|
.withParameters(/* windowIndex= */ 0, mediaPeriodId),
|
||||||
|
/* deferOnPrepared= */ true);
|
||||||
|
PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod);
|
||||||
|
AtomicBoolean onPreparedOfPreloadCallbackCalled = new AtomicBoolean();
|
||||||
|
AtomicBoolean onPreparedOfPrepareCallbackCalled = new AtomicBoolean();
|
||||||
|
AtomicReference<MediaPeriod> mediaPeriodReference = new AtomicReference<>();
|
||||||
|
MediaPeriod.Callback preloadCallback =
|
||||||
|
new MediaPeriod.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||||
|
onPreparedOfPreloadCallbackCalled.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||||
|
};
|
||||||
|
MediaPeriod.Callback prepareCallback =
|
||||||
|
new MediaPeriod.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||||
|
mediaPeriodReference.set(mediaPeriod);
|
||||||
|
onPreparedOfPrepareCallbackCalled.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
preloadMediaPeriod.preload(preloadCallback, /* positionUs= */ 0L);
|
||||||
|
preloadMediaPeriod.prepare(prepareCallback, /* positionUs= */ 0L);
|
||||||
|
wrappedMediaPeriod.setPreparationComplete();
|
||||||
|
shadowOf(Looper.getMainLooper()).idle();
|
||||||
|
|
||||||
|
// Should only invoke the latest callback.
|
||||||
|
assertThat(onPreparedOfPreloadCallbackCalled.get()).isFalse();
|
||||||
|
assertThat(onPreparedOfPrepareCallbackCalled.get()).isTrue();
|
||||||
|
assertThat(mediaPeriodReference.get()).isSameInstanceAs(preloadMediaPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepareAfterWrappedPeriodPreparedByPreloading_immediatelyInvokeCallbackOnPrepared() {
|
||||||
|
MediaSource.MediaPeriodId mediaPeriodId =
|
||||||
|
new MediaSource.MediaPeriodId(/* periodUid= */ new Object());
|
||||||
|
FakeMediaPeriod wrappedMediaPeriod =
|
||||||
|
new FakeMediaPeriod(
|
||||||
|
TrackGroupArray.EMPTY,
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
|
||||||
|
FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
|
||||||
|
new MediaSourceEventListener.EventDispatcher()
|
||||||
|
.withParameters(/* windowIndex= */ 0, mediaPeriodId));
|
||||||
|
PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod);
|
||||||
|
AtomicBoolean onPreparedOfPreloadCallbackCalled = new AtomicBoolean();
|
||||||
|
AtomicBoolean onPreparedOfPrepareCallbackCalled = new AtomicBoolean();
|
||||||
|
AtomicReference<MediaPeriod> mediaPeriodReference = new AtomicReference<>();
|
||||||
|
MediaPeriod.Callback preloadCallback =
|
||||||
|
new MediaPeriod.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||||
|
onPreparedOfPreloadCallbackCalled.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||||
|
};
|
||||||
|
MediaPeriod.Callback prepareCallback =
|
||||||
|
new MediaPeriod.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||||
|
mediaPeriodReference.set(mediaPeriod);
|
||||||
|
onPreparedOfPrepareCallbackCalled.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
preloadMediaPeriod.preload(preloadCallback, /* positionUs= */ 0L);
|
||||||
|
preloadMediaPeriod.prepare(prepareCallback, /* positionUs= */ 0L);
|
||||||
|
|
||||||
|
assertThat(onPreparedOfPreloadCallbackCalled.get()).isTrue();
|
||||||
|
assertThat(onPreparedOfPrepareCallbackCalled.get()).isTrue();
|
||||||
|
assertThat(mediaPeriodReference.get()).isSameInstanceAs(preloadMediaPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void selectTracks_afterPreloadingForSameSelections_usePreloadedResults() {
|
||||||
|
MediaPeriod wrappedMediaPeriod = mock(MediaPeriod.class);
|
||||||
|
ExoTrackSelection[] trackSelections =
|
||||||
|
new ExoTrackSelection[] {
|
||||||
|
new FixedTrackSelection(
|
||||||
|
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build()),
|
||||||
|
/* track= */ 0),
|
||||||
|
new FixedTrackSelection(
|
||||||
|
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()),
|
||||||
|
/* track= */ 0)
|
||||||
|
};
|
||||||
|
TrackSelectorResult trackSelectorResult =
|
||||||
|
new TrackSelectorResult(
|
||||||
|
new RendererConfiguration[] {
|
||||||
|
RendererConfiguration.DEFAULT, RendererConfiguration.DEFAULT
|
||||||
|
},
|
||||||
|
trackSelections,
|
||||||
|
Tracks.EMPTY,
|
||||||
|
/* info= */ null);
|
||||||
|
SampleStream[] preloadedStreams =
|
||||||
|
new SampleStream[] {new EmptySampleStream(), new EmptySampleStream()};
|
||||||
|
when(wrappedMediaPeriod.selectTracks(
|
||||||
|
eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L)))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
SampleStream[] streams = invocation.getArgument(2);
|
||||||
|
boolean[] streamResetFlags = invocation.getArgument(3);
|
||||||
|
for (int i = 0; i < streams.length; i++) {
|
||||||
|
streams[i] = preloadedStreams[i];
|
||||||
|
streamResetFlags[i] = true;
|
||||||
|
}
|
||||||
|
return 0L;
|
||||||
|
});
|
||||||
|
PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod);
|
||||||
|
MediaPeriod.Callback callback =
|
||||||
|
new MediaPeriod.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||||
|
};
|
||||||
|
preloadMediaPeriod.prepare(callback, /* positionUs= */ 0L);
|
||||||
|
|
||||||
|
// Select tracks for preloading.
|
||||||
|
long preloadTrackSelectionStartPositionUs =
|
||||||
|
preloadMediaPeriod.selectTracksForPreloading(trackSelectorResult, /* positionUs= */ 0L);
|
||||||
|
SampleStream[] streams = new SampleStream[2];
|
||||||
|
boolean[] streamResetFlags = new boolean[2];
|
||||||
|
// Select tracks based on the same track selections.
|
||||||
|
long trackSelectionStartPositionUs =
|
||||||
|
preloadMediaPeriod.selectTracks(
|
||||||
|
trackSelections, new boolean[2], streams, streamResetFlags, /* positionUs= */ 0L);
|
||||||
|
|
||||||
|
verify(wrappedMediaPeriod)
|
||||||
|
.selectTracks(eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L));
|
||||||
|
assertThat(trackSelectionStartPositionUs).isEqualTo(preloadTrackSelectionStartPositionUs);
|
||||||
|
assertThat(streams).isEqualTo(preloadedStreams);
|
||||||
|
assertThat(streamResetFlags).hasLength(2);
|
||||||
|
assertThat(streamResetFlags[0]).isTrue();
|
||||||
|
assertThat(streamResetFlags[1]).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void selectTracks_afterPreloadingButForDifferentSelections_callOnWrappedPeriod() {
|
||||||
|
MediaPeriod wrappedMediaPeriod = mock(MediaPeriod.class);
|
||||||
|
ExoTrackSelection[] trackSelections =
|
||||||
|
new ExoTrackSelection[] {
|
||||||
|
new FixedTrackSelection(
|
||||||
|
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build()),
|
||||||
|
/* track= */ 0),
|
||||||
|
new FixedTrackSelection(
|
||||||
|
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()),
|
||||||
|
/* track= */ 0)
|
||||||
|
};
|
||||||
|
TrackSelectorResult trackSelectorResult =
|
||||||
|
new TrackSelectorResult(
|
||||||
|
new RendererConfiguration[] {
|
||||||
|
RendererConfiguration.DEFAULT, RendererConfiguration.DEFAULT
|
||||||
|
},
|
||||||
|
trackSelections,
|
||||||
|
Tracks.EMPTY,
|
||||||
|
/* info= */ null);
|
||||||
|
SampleStream[] preloadedStreams =
|
||||||
|
new SampleStream[] {new EmptySampleStream(), new EmptySampleStream()};
|
||||||
|
when(wrappedMediaPeriod.selectTracks(
|
||||||
|
eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L)))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
SampleStream[] streams = invocation.getArgument(2);
|
||||||
|
boolean[] streamResetFlags = invocation.getArgument(3);
|
||||||
|
for (int i = 0; i < streams.length; i++) {
|
||||||
|
streams[i] = preloadedStreams[i];
|
||||||
|
streamResetFlags[i] = true;
|
||||||
|
}
|
||||||
|
return 0L;
|
||||||
|
});
|
||||||
|
PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod);
|
||||||
|
MediaPeriod.Callback callback =
|
||||||
|
new MediaPeriod.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||||
|
};
|
||||||
|
preloadMediaPeriod.prepare(callback, /* positionUs= */ 0L);
|
||||||
|
|
||||||
|
// Select tracks for preloading.
|
||||||
|
preloadMediaPeriod.selectTracksForPreloading(trackSelectorResult, /* positionUs= */ 0L);
|
||||||
|
verify(wrappedMediaPeriod)
|
||||||
|
.selectTracks(eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L));
|
||||||
|
// Create a new track selections.
|
||||||
|
ExoTrackSelection[] newTrackSelections =
|
||||||
|
new ExoTrackSelection[] {
|
||||||
|
null,
|
||||||
|
new FixedTrackSelection(
|
||||||
|
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()),
|
||||||
|
/* track= */ 0)
|
||||||
|
};
|
||||||
|
SampleStream[] newSampleStreams = new SampleStream[] {new EmptySampleStream()};
|
||||||
|
when(wrappedMediaPeriod.selectTracks(
|
||||||
|
eq(newTrackSelections), any(), any(), any(), /* positionUs= */ eq(0L)))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
SampleStream[] streams = invocation.getArgument(2);
|
||||||
|
boolean[] streamResetFlags = invocation.getArgument(3);
|
||||||
|
for (int i = 0; i < streams.length; i++) {
|
||||||
|
streams[i] = newSampleStreams[i];
|
||||||
|
streamResetFlags[i] = true;
|
||||||
|
}
|
||||||
|
return 0L;
|
||||||
|
});
|
||||||
|
SampleStream[] streams = new SampleStream[1];
|
||||||
|
boolean[] streamResetFlags = new boolean[1];
|
||||||
|
// Select tracks based on the new track selections.
|
||||||
|
long trackSelectionStartPositionUs =
|
||||||
|
preloadMediaPeriod.selectTracks(
|
||||||
|
newTrackSelections, new boolean[1], streams, streamResetFlags, /* positionUs= */ 0L);
|
||||||
|
|
||||||
|
verify(wrappedMediaPeriod)
|
||||||
|
.selectTracks(eq(newTrackSelections), any(), same(streams), same(streamResetFlags), eq(0L));
|
||||||
|
assertThat(trackSelectionStartPositionUs).isEqualTo(0L);
|
||||||
|
// Use newSampleStreams instead of preloadedSampleStreams
|
||||||
|
assertThat(streams).isEqualTo(newSampleStreams);
|
||||||
|
assertThat(streamResetFlags).hasLength(1);
|
||||||
|
assertThat(streamResetFlags[0]).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
selectTracks_afterPreloadingForSameSelectionsButAtDifferentPosition_callOnWrappedPeriod() {
|
||||||
|
MediaPeriod wrappedMediaPeriod = mock(MediaPeriod.class);
|
||||||
|
ExoTrackSelection[] trackSelections =
|
||||||
|
new ExoTrackSelection[] {
|
||||||
|
new FixedTrackSelection(
|
||||||
|
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build()),
|
||||||
|
/* track= */ 0),
|
||||||
|
new FixedTrackSelection(
|
||||||
|
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()),
|
||||||
|
/* track= */ 0)
|
||||||
|
};
|
||||||
|
TrackSelectorResult trackSelectorResult =
|
||||||
|
new TrackSelectorResult(
|
||||||
|
new RendererConfiguration[] {
|
||||||
|
RendererConfiguration.DEFAULT, RendererConfiguration.DEFAULT
|
||||||
|
},
|
||||||
|
trackSelections,
|
||||||
|
Tracks.EMPTY,
|
||||||
|
/* info= */ null);
|
||||||
|
when(wrappedMediaPeriod.selectTracks(eq(trackSelections), any(), any(), any(), anyLong()))
|
||||||
|
.thenAnswer(invocation -> invocation.getArgument(4, Long.class));
|
||||||
|
PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod);
|
||||||
|
MediaPeriod.Callback callback =
|
||||||
|
new MediaPeriod.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||||
|
};
|
||||||
|
preloadMediaPeriod.prepare(callback, /* positionUs= */ 0L);
|
||||||
|
|
||||||
|
// Select tracks for preloading.
|
||||||
|
preloadMediaPeriod.selectTracksForPreloading(trackSelectorResult, /* positionUs= */ 0L);
|
||||||
|
verify(wrappedMediaPeriod)
|
||||||
|
.selectTracks(eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L));
|
||||||
|
SampleStream[] streams = new SampleStream[2];
|
||||||
|
boolean[] streamResetFlags = new boolean[2];
|
||||||
|
// Select tracks based on the same track selections but at a different position.
|
||||||
|
long trackSelectionStartPositionUs =
|
||||||
|
preloadMediaPeriod.selectTracks(
|
||||||
|
trackSelections, new boolean[2], streams, streamResetFlags, /* positionUs= */ 10L);
|
||||||
|
|
||||||
|
verify(wrappedMediaPeriod)
|
||||||
|
.selectTracks(
|
||||||
|
eq(trackSelections),
|
||||||
|
any(),
|
||||||
|
same(streams),
|
||||||
|
same(streamResetFlags),
|
||||||
|
/* positionUs= */ eq(10L));
|
||||||
|
assertThat(trackSelectionStartPositionUs).isEqualTo(10L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void selectTracks_theSecondCallAfterPreloading_callOnWrappedPeriod() {
|
||||||
|
MediaPeriod wrappedMediaPeriod = mock(MediaPeriod.class);
|
||||||
|
ExoTrackSelection[] trackSelections =
|
||||||
|
new ExoTrackSelection[] {
|
||||||
|
new FixedTrackSelection(
|
||||||
|
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build()),
|
||||||
|
/* track= */ 0),
|
||||||
|
new FixedTrackSelection(
|
||||||
|
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build()),
|
||||||
|
/* track= */ 0)
|
||||||
|
};
|
||||||
|
TrackSelectorResult trackSelectorResult =
|
||||||
|
new TrackSelectorResult(
|
||||||
|
new RendererConfiguration[] {
|
||||||
|
RendererConfiguration.DEFAULT, RendererConfiguration.DEFAULT
|
||||||
|
},
|
||||||
|
trackSelections,
|
||||||
|
Tracks.EMPTY,
|
||||||
|
/* info= */ null);
|
||||||
|
when(wrappedMediaPeriod.selectTracks(
|
||||||
|
eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L)))
|
||||||
|
.thenReturn(0L);
|
||||||
|
PreloadMediaPeriod preloadMediaPeriod = new PreloadMediaPeriod(wrappedMediaPeriod);
|
||||||
|
MediaPeriod.Callback callback =
|
||||||
|
new MediaPeriod.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {}
|
||||||
|
};
|
||||||
|
preloadMediaPeriod.prepare(callback, /* positionUs= */ 0L);
|
||||||
|
verify(wrappedMediaPeriod).prepare(any(), /* positionUs= */ eq(0L));
|
||||||
|
|
||||||
|
// Select tracks for preloading.
|
||||||
|
preloadMediaPeriod.selectTracksForPreloading(trackSelectorResult, /* positionUs= */ 0L);
|
||||||
|
verify(wrappedMediaPeriod)
|
||||||
|
.selectTracks(eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L));
|
||||||
|
SampleStream[] streams = new SampleStream[2];
|
||||||
|
boolean[] streamResetFlags = new boolean[2];
|
||||||
|
// First `selectTracks` call based on the same track selections at the same position.
|
||||||
|
preloadMediaPeriod.selectTracks(
|
||||||
|
trackSelections, new boolean[2], streams, streamResetFlags, /* positionUs= */ 0L);
|
||||||
|
// Second `selectTracks` call based on the same track selections at the same position.
|
||||||
|
long trackSelectionStartPositionUs =
|
||||||
|
preloadMediaPeriod.selectTracks(
|
||||||
|
trackSelections, new boolean[2], streams, streamResetFlags, /* positionUs= */ 0L);
|
||||||
|
|
||||||
|
verify(wrappedMediaPeriod, times(2))
|
||||||
|
.selectTracks(eq(trackSelections), any(), any(), any(), /* positionUs= */ eq(0L));
|
||||||
|
assertThat(trackSelectionStartPositionUs).isEqualTo(0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue