introduce datasource
This commit is contained in:
parent
f80e4c8e5f
commit
b50e63077b
|
@ -45,6 +45,7 @@ okhttpAndroid = "5.0.0-alpha.14"
|
||||||
coil = "2.7.0"
|
coil = "2.7.0"
|
||||||
reorderable = "2.4.0-alpha02"
|
reorderable = "2.4.0-alpha02"
|
||||||
media3Session = "1.4.1"
|
media3Session = "1.4.1"
|
||||||
|
media3ExoplayerDash = "1.4.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
@ -81,6 +82,7 @@ okhttp-android = { group = "com.squareup.okhttp3", name = "okhttp-android", vers
|
||||||
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
||||||
reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderable" }
|
reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderable" }
|
||||||
androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3Session" }
|
androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3Session" }
|
||||||
|
androidx-media3-exoplayer-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "media3ExoplayerDash" }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,7 @@ dependencies {
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.reorderable)
|
implementation(libs.reorderable)
|
||||||
implementation(libs.androidx.media3.session)
|
implementation(libs.androidx.media3.session)
|
||||||
|
implementation(libs.androidx.media3.exoplayer.dash)
|
||||||
|
|
||||||
ksp(libs.hilt.android.compiler)
|
ksp(libs.hilt.android.compiler)
|
||||||
ksp(libs.androidx.hilt.compiler)
|
ksp(libs.androidx.hilt.compiler)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<service
|
<service
|
||||||
|
|
|
@ -27,12 +27,32 @@ import net.newpipe.newplayer.utils.Thumbnail
|
||||||
|
|
||||||
data class Chapter(val chapterStartInMs: Long, val chapterTitle: String?, val thumbnail: Uri?)
|
data class Chapter(val chapterStartInMs: Long, val chapterTitle: String?, val thumbnail: Uri?)
|
||||||
|
|
||||||
|
enum class StreamType {
|
||||||
|
VIDEO,
|
||||||
|
AUDIO,
|
||||||
|
AUDIO_AND_VIDEO,
|
||||||
|
DYNAMIC
|
||||||
|
}
|
||||||
|
|
||||||
|
data class StreamVariant(
|
||||||
|
val streamType: StreamType,
|
||||||
|
val language: String?,
|
||||||
|
val streamVariantIdentifier: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RepoMetaInfo(
|
||||||
|
val canHandleTimestampedLinks: Boolean,
|
||||||
|
val pullsDataFromNetwrok: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
interface MediaRepository {
|
interface MediaRepository {
|
||||||
|
|
||||||
|
fun getRepoInfo() : RepoMetaInfo
|
||||||
|
|
||||||
suspend fun getMetaInfo(item: String): MediaMetadata
|
suspend fun getMetaInfo(item: String): MediaMetadata
|
||||||
|
|
||||||
suspend fun getAvailableStreamVariants(item: String): List<String>
|
suspend fun getAvailableStreamVariants(item: String): List<StreamVariant>
|
||||||
suspend fun getStream(item: String, streamSelector: String): Uri
|
suspend fun getStream(item: String, streamVariantSelector: StreamVariant): Uri
|
||||||
|
|
||||||
suspend fun getAvailableSubtitleVariants(item: String): List<String>
|
suspend fun getAvailableSubtitleVariants(item: String): List<String>
|
||||||
suspend fun getSubtitle(item: String, variant: String): Uri
|
suspend fun getSubtitle(item: String, variant: String): Uri
|
||||||
|
|
|
@ -25,6 +25,7 @@ import androidx.media3.common.Player
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import java.util.stream.Stream
|
||||||
import kotlin.Exception
|
import kotlin.Exception
|
||||||
|
|
||||||
enum class PlayMode {
|
enum class PlayMode {
|
||||||
|
@ -45,6 +46,7 @@ enum class RepeatMode {
|
||||||
interface NewPlayer {
|
interface NewPlayer {
|
||||||
// preferences
|
// preferences
|
||||||
val preferredStreamVariants: List<String>
|
val preferredStreamVariants: List<String>
|
||||||
|
val preferredStreamLanguage: List<String>
|
||||||
|
|
||||||
val exoPlayer: StateFlow<Player?>
|
val exoPlayer: StateFlow<Player?>
|
||||||
var playWhenReady: Boolean
|
var playWhenReady: Boolean
|
||||||
|
@ -76,7 +78,7 @@ interface NewPlayer {
|
||||||
fun removePlaylistItem(uniqueId: Long)
|
fun removePlaylistItem(uniqueId: Long)
|
||||||
fun playStream(item: String, playMode: PlayMode)
|
fun playStream(item: String, playMode: PlayMode)
|
||||||
fun selectChapter(index: Int)
|
fun selectChapter(index: Int)
|
||||||
fun playStream(item: String, streamVariant: String, playMode: PlayMode)
|
fun playStream(item: String, streamVariant: StreamVariant, playMode: PlayMode)
|
||||||
fun release()
|
fun release()
|
||||||
fun getItemLinkOfMediaItem(mediaItem: MediaItem) : String
|
fun getItemLinkOfMediaItem(mediaItem: MediaItem) : String
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,11 +23,18 @@ package net.newpipe.newplayer
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.AudioAttributes
|
||||||
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Timeline
|
import androidx.media3.common.Timeline
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.datasource.DefaultHttpDataSource
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.exoplayer.source.MediaSource
|
||||||
|
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.MediaController
|
||||||
import androidx.media3.session.SessionToken
|
import androidx.media3.session.SessionToken
|
||||||
import com.google.common.util.concurrent.MoreExecutors
|
import com.google.common.util.concurrent.MoreExecutors
|
||||||
|
@ -50,7 +57,8 @@ private const val TAG = "NewPlayerImpl"
|
||||||
class NewPlayerImpl(
|
class NewPlayerImpl(
|
||||||
val app: Application,
|
val app: Application,
|
||||||
private val repository: MediaRepository,
|
private val repository: MediaRepository,
|
||||||
override val preferredStreamVariants: List<String> = emptyList()
|
override val preferredStreamVariants: List<String> = emptyList(),
|
||||||
|
override val preferredStreamLanguage: List<String> = emptyList()
|
||||||
) : NewPlayer {
|
) : NewPlayer {
|
||||||
|
|
||||||
private val mutableExoPlayer = MutableStateFlow<ExoPlayer?>(null)
|
private val mutableExoPlayer = MutableStateFlow<ExoPlayer?>(null)
|
||||||
|
@ -134,7 +142,11 @@ class NewPlayerImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupNewExoplayer() {
|
private fun setupNewExoplayer() {
|
||||||
val newExoPlayer = ExoPlayer.Builder(app).build()
|
val newExoPlayer = ExoPlayer.Builder(app)
|
||||||
|
.setAudioAttributes(AudioAttributes.DEFAULT, true)
|
||||||
|
.setHandleAudioBecomingNoisy(true)
|
||||||
|
.setWakeMode(if (repository.getRepoInfo().pullsDataFromNetwrok) C.WAKE_MODE_NETWORK else C.WAKE_MODE_LOCAL)
|
||||||
|
.build()
|
||||||
newExoPlayer.addListener(object : Player.Listener {
|
newExoPlayer.addListener(object : Player.Listener {
|
||||||
override fun onPlayerError(error: PlaybackException) {
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
launchJobAndCollectError {
|
launchJobAndCollectError {
|
||||||
|
@ -219,13 +231,14 @@ class NewPlayerImpl(
|
||||||
exoPlayer.value?.pause()
|
exoPlayer.value?.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
override fun addToPlaylist(item: String) {
|
override fun addToPlaylist(item: String) {
|
||||||
if (exoPlayer.value == null) {
|
if (exoPlayer.value == null) {
|
||||||
prepare()
|
prepare()
|
||||||
}
|
}
|
||||||
launchJobAndCollectError {
|
launchJobAndCollectError {
|
||||||
val mediaItem = toMediaItem(item)
|
val mediaSource = toMediaSource(item, playBackMode.value)
|
||||||
exoPlayer.value?.addMediaItem(mediaItem)
|
exoPlayer.value?.addMediaSource(mediaSource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,22 +258,23 @@ class NewPlayerImpl(
|
||||||
|
|
||||||
override fun playStream(item: String, playMode: PlayMode) {
|
override fun playStream(item: String, playMode: PlayMode) {
|
||||||
launchJobAndCollectError {
|
launchJobAndCollectError {
|
||||||
val mediaItem = toMediaItem(item)
|
val mediaItem = toMediaSource(item, playMode)
|
||||||
internalPlayStream(mediaItem, playMode)
|
internalPlayStream(mediaItem, playMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun playStream(
|
override fun playStream(
|
||||||
item: String,
|
item: String,
|
||||||
streamVariant: String,
|
streamVariant: StreamVariant,
|
||||||
playMode: PlayMode
|
playMode: PlayMode
|
||||||
) {
|
) {
|
||||||
launchJobAndCollectError {
|
launchJobAndCollectError {
|
||||||
val stream = toMediaItem(item, streamVariant)
|
val stream = toMediaSource(item, streamVariant)
|
||||||
internalPlayStream(stream, playMode)
|
internalPlayStream(stream, playMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
override fun selectChapter(index: Int) {
|
override fun selectChapter(index: Int) {
|
||||||
val chapters = currentChapters.value
|
val chapters = currentChapters.value
|
||||||
assert(index in 0..<chapters.size) {
|
assert(index in 0..<chapters.size) {
|
||||||
|
@ -287,18 +301,20 @@ class NewPlayerImpl(
|
||||||
uniqueIdToIdLookup[mediaItem.mediaId.toLong()]
|
uniqueIdToIdLookup[mediaItem.mediaId.toLong()]
|
||||||
?: throw NewPlayerException("Could not find Media item with mediaId: ${mediaItem.mediaId}")
|
?: throw NewPlayerException("Could not find Media item with mediaId: ${mediaItem.mediaId}")
|
||||||
|
|
||||||
private fun internalPlayStream(mediaItem: MediaItem, playMode: PlayMode) {
|
@OptIn(UnstableApi::class)
|
||||||
|
private fun internalPlayStream(mediaSource: MediaSource, playMode: PlayMode) {
|
||||||
if (exoPlayer.value?.playbackState == Player.STATE_IDLE || exoPlayer.value == null) {
|
if (exoPlayer.value?.playbackState == Player.STATE_IDLE || exoPlayer.value == null) {
|
||||||
prepare()
|
prepare()
|
||||||
}
|
}
|
||||||
this.playBackMode.update { playMode }
|
this.playBackMode.update { playMode }
|
||||||
println("gurken: playervalue: ${this.exoPlayer.value}")
|
|
||||||
this.exoPlayer.value?.setMediaItem(mediaItem)
|
this.exoPlayer.value?.setMediaSource(mediaSource)
|
||||||
this.exoPlayer.value?.play()
|
this.exoPlayer.value?.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
private suspend
|
private suspend
|
||||||
fun toMediaItem(item: String, streamVariant: String): MediaItem {
|
fun toMediaSource(item: String, streamVariant: StreamVariant): MediaSource {
|
||||||
val dataStream = repository.getStream(item, streamVariant)
|
val dataStream = repository.getStream(item, streamVariant)
|
||||||
|
|
||||||
val uniqueId = Random.nextLong()
|
val uniqueId = Random.nextLong()
|
||||||
|
@ -314,21 +330,25 @@ class NewPlayerImpl(
|
||||||
mutableErrorFlow.emit(e)
|
mutableErrorFlow.emit(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mediaItemBuilder.build()
|
val mediaItem = mediaItemBuilder.build()
|
||||||
|
|
||||||
|
return ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory())
|
||||||
|
.createMediaSource(mediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend
|
private suspend
|
||||||
fun toMediaItem(item: String): MediaItem {
|
fun toMediaSource(item: String, playMode: PlayMode): MediaSource {
|
||||||
val availableStream = repository.getAvailableStreamVariants(item)
|
val availableStreams = repository.getAvailableStreamVariants(item)
|
||||||
var selectedStream = availableStream[availableStream.size / 2]
|
var selectedStream = availableStreams[availableStreams.size / 2]
|
||||||
for (preferredStream in preferredStreamVariants) {
|
for (preferredStream in preferredStreamVariants) {
|
||||||
if (preferredStream in availableStream) {
|
for (availableStream in availableStreams) {
|
||||||
selectedStream = preferredStream
|
if (preferredStream == availableStream.streamVariantIdentifier) {
|
||||||
break;
|
selectedStream = availableStream
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return toMediaItem(item, selectedStream)
|
return toMediaSource(item, selectedStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchJobAndCollectError(task: suspend () -> Unit) =
|
private fun launchJobAndCollectError(task: suspend () -> Unit) =
|
||||||
|
|
|
@ -8,6 +8,9 @@ import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import net.newpipe.newplayer.Chapter
|
import net.newpipe.newplayer.Chapter
|
||||||
import net.newpipe.newplayer.MediaRepository
|
import net.newpipe.newplayer.MediaRepository
|
||||||
|
import net.newpipe.newplayer.RepoMetaInfo
|
||||||
|
import net.newpipe.newplayer.StreamType
|
||||||
|
import net.newpipe.newplayer.StreamVariant
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
@ -22,6 +25,9 @@ class TestMediaRepository(val context: Context) : MediaRepository {
|
||||||
return client.newCall(request).execute()
|
return client.newCall(request).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getRepoInfo() =
|
||||||
|
RepoMetaInfo(canHandleTimestampedLinks = true, pullsDataFromNetwrok = true)
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
override suspend fun getMetaInfo(item: String): MediaMetadata =
|
override suspend fun getMetaInfo(item: String): MediaMetadata =
|
||||||
when (item) {
|
when (item) {
|
||||||
|
@ -55,11 +61,37 @@ class TestMediaRepository(val context: Context) : MediaRepository {
|
||||||
else -> throw Exception("Unknown stream: $item")
|
else -> throw Exception("Unknown stream: $item")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAvailableStreamVariants(item: String): List<String> =
|
override suspend fun getAvailableStreamVariants(item: String): List<StreamVariant> =
|
||||||
when (item) {
|
when (item) {
|
||||||
"6502" -> listOf("576p")
|
"6502" -> listOf(
|
||||||
"portrait" -> listOf("720p")
|
StreamVariant(
|
||||||
"imu" -> listOf("1080p", "576p")
|
streamType = StreamType.AUDIO_AND_VIDEO,
|
||||||
|
language = "Deutsch",
|
||||||
|
streamVariantIdentifier = "576p",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
"portrait" -> listOf(
|
||||||
|
StreamVariant(
|
||||||
|
streamType = StreamType.AUDIO_AND_VIDEO,
|
||||||
|
language = null,
|
||||||
|
streamVariantIdentifier = "720p",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
"imu" -> listOf(
|
||||||
|
StreamVariant(
|
||||||
|
streamType = StreamType.AUDIO_AND_VIDEO,
|
||||||
|
language = "Deutsch",
|
||||||
|
streamVariantIdentifier = "1080p",
|
||||||
|
),
|
||||||
|
StreamVariant(
|
||||||
|
streamType = StreamType.AUDIO_AND_VIDEO,
|
||||||
|
language = "Deutsch",
|
||||||
|
streamVariantIdentifier = "576p",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
else -> throw Exception("Unknown stream: $item")
|
else -> throw Exception("Unknown stream: $item")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,15 +100,15 @@ class TestMediaRepository(val context: Context) : MediaRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override suspend fun getStream(item: String, streamSelector: String) =
|
override suspend fun getStream(item: String, streamVariantSelector: StreamVariant) =
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
when (item) {
|
when (item) {
|
||||||
"6502" -> context.getString(R.string.ccc_6502_video)
|
"6502" -> context.getString(R.string.ccc_6502_video)
|
||||||
"portrait" -> context.getString(R.string.portrait_video_example)
|
"portrait" -> context.getString(R.string.portrait_video_example)
|
||||||
"imu" -> when (streamSelector) {
|
"imu" -> when (streamVariantSelector.streamVariantIdentifier) {
|
||||||
"1080p" -> context.getString(R.string.ccc_imu_1080_mp4)
|
"1080p" -> context.getString(R.string.ccc_imu_1080_mp4)
|
||||||
"576p" -> context.getString(R.string.ccc_imu_576_mp4)
|
"576p" -> context.getString(R.string.ccc_imu_576_mp4)
|
||||||
else -> throw Exception("Unknown stream selector for $item: $streamSelector")
|
else -> throw Exception("Unknown stream selector for $item: $streamVariantSelector")
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> throw Exception("Unknown stream: $item")
|
else -> throw Exception("Unknown stream: $item")
|
||||||
|
|
Loading…
Reference in New Issue