mirror of
https://github.com/samsonjs/media.git
synced 2026-03-25 09:25:53 +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:
|
||||
* Add luma and chroma bitdepth to `ColorInfo`
|
||||
[#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:
|
||||
* Add support for flattening H.265/HEVC SEF slow motion videos.
|
||||
* Track Selection:
|
||||
|
|
@ -62,6 +68,9 @@
|
|||
* Remove deprecated `DownloadNotificationHelper.buildProgressNotification`
|
||||
method, use a non deprecated method that takes a `notMetRequirements`
|
||||
parameter instead.
|
||||
* Demo app:
|
||||
* Add a shortform demo module to demo the usage of `PreloadMediaSource`
|
||||
with the short-form content use case.
|
||||
|
||||
## 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