introduce NewPlayer Service

This commit is contained in:
Christian Schabesberger 2024-09-09 18:38:04 +02:00
parent ba03e17088
commit 0744d43b20
14 changed files with 506 additions and 318 deletions

View File

@ -0,0 +1,91 @@
kotlin version: 2.0.20-Beta2
error message: java.lang.IllegalStateException: Storage for [/home/schabi/Projects/NewPlayer/new-player/build/kspCaches/debug/symbolLookups/file-to-id.tab] is already registered
at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.<init>(PagedFileStorage.java:72)
at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.<init>(ResizeableMappedFile.java:55)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.<init>(PersistentBTreeEnumerator.java:128)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:165)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:140)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:45)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:71)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:60)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:57)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:78)
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.set(PersistentStorage.kt:94)
at org.jetbrains.kotlin.incremental.LookupStorage.addFileIfNeeded(LookupStorage.kt:164)
at org.jetbrains.kotlin.incremental.LookupStorage.addAll$lambda$4(LookupStorage.kt:117)
at org.jetbrains.kotlin.utils.CollectionsKt.keysToMap(collections.kt:117)
at org.jetbrains.kotlin.incremental.LookupStorage.addAll(LookupStorage.kt:117)
at org.jetbrains.kotlin.incremental.BuildUtilKt.update(buildUtil.kt:134)
at com.google.devtools.ksp.LookupStorageWrapperImpl.update(IncrementalContext.kt:231)
at com.google.devtools.ksp.common.IncrementalContextBase.updateLookupCache(IncrementalContextBase.kt:133)
at com.google.devtools.ksp.common.IncrementalContextBase.updateCaches(IncrementalContextBase.kt:364)
at com.google.devtools.ksp.common.IncrementalContextBase.updateCachesAndOutputs(IncrementalContextBase.kt:470)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:362)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.repeatAnalysisIfNeeded(KotlinToJVMBytecodeCompiler.kt:282)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.repeatAnalysisIfNeeded(KotlinToJVMBytecodeCompiler.kt:282)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.runFrontendAndGenerateIrUsingClassicFrontend(KotlinToJVMBytecodeCompiler.kt:195)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:106)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:170)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1556)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:840)
Suppressed: java.lang.Exception: Storage[/home/schabi/Projects/NewPlayer/new-player/build/kspCaches/debug/symbolLookups/file-to-id.tab] registration stack trace
at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.<init>(PagedFileStorage.java:72)
at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.<init>(ResizeableMappedFile.java:55)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.<init>(PersistentBTreeEnumerator.java:128)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:165)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:140)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:45)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:71)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:60)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageIfExists(LazyStorage.kt:51)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.get(LazyStorage.kt:74)
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.get(PersistentStorage.kt:90)
at org.jetbrains.kotlin.incremental.LookupStorage.removeLookupsFrom(LookupStorage.kt:131)
at com.google.devtools.ksp.LookupStorageWrapperImpl.removeLookupsFrom(IncrementalContext.kt:234)
at com.google.devtools.ksp.common.IncrementalContextBase.updateFromRemovedOutputs(IncrementalContextBase.kt:122)
at com.google.devtools.ksp.common.IncrementalContextBase.calcDirtyFiles(IncrementalContextBase.kt:277)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:196)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:189)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.handleException(KotlinSymbolProcessingExtension.kt:414)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:189)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
... 25 more

View File

@ -44,6 +44,7 @@ newplayer = "master-SNAPSHOT"
okhttpAndroid = "5.0.0-alpha.14" 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"
[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" }
@ -79,6 +80,7 @@ newplayer = { group = "com.github.theScrabi.NewPlayer", name = "new-player", ver
okhttp-android = { group = "com.squareup.okhttp3", name = "okhttp-android", version.ref = "okhttpAndroid" } okhttp-android = { group = "com.squareup.okhttp3", name = "okhttp-android", version.ref = "okhttpAndroid" }
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" }

View File

@ -68,6 +68,7 @@ dependencies {
implementation(libs.androidx.media3.common) implementation(libs.androidx.media3.common)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.reorderable) implementation(libs.reorderable)
implementation(libs.androidx.media3.session)
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler) ksp(libs.androidx.hilt.compiler)

View File

@ -1,11 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<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_MEDIA_PLAYBACK" />
<application> <application>
<activity <service
android:name=".internal.VideoPlayerActivity" android:name=".service.NewPlayerService"
android:exported="false" android:foregroundServiceType="mediaPlayback"
android:label="@string/video_player_fullscreen_activity" /> android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
</application> </application>
</manifest> </manifest>

View File

@ -37,13 +37,11 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.newpipe.newplayer.model.UIModeState import net.newpipe.newplayer.model.PlaylistItem
import net.newpipe.newplayer.playerInternals.PlaylistItem import net.newpipe.newplayer.model.fetchPlaylistItem
import net.newpipe.newplayer.playerInternals.fetchPlaylistItem import net.newpipe.newplayer.model.getPlaylistItemsFromExoplayer
import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromExoplayer
import kotlin.Exception import kotlin.Exception
import kotlin.random.Random import kotlin.random.Random
@ -62,8 +60,6 @@ enum class RepeatMode {
REPEAT_ONE REPEAT_ONE
} }
private val TAG = "NewPlayer"
interface NewPlayer { interface NewPlayer {
// preferences // preferences
val preferredStreamVariants: List<String> val preferredStreamVariants: List<String>
@ -72,7 +68,6 @@ interface NewPlayer {
var playWhenReady: Boolean var playWhenReady: Boolean
val duration: Long val duration: Long
val bufferedPercentage: Int val bufferedPercentage: Int
val sharingLinkWithOffsetPossible: Boolean
var currentPosition: Long var currentPosition: Long
var fastSeekAmountSec: Int var fastSeekAmountSec: Int
val playBackMode: MutableStateFlow<PlayMode> val playBackMode: MutableStateFlow<PlayMode>
@ -100,11 +95,11 @@ interface NewPlayer {
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: String, playMode: PlayMode)
fun release()
data class Builder(val app: Application, val repository: MediaRepository) { data class Builder(val app: Application, val repository: MediaRepository) {
private var mediaSourceFactory: MediaSource.Factory? = null private var mediaSourceFactory: MediaSource.Factory? = null
private var preferredStreamVariants: List<String> = emptyList() private var preferredStreamVariants: List<String> = emptyList()
private var sharingLinkWithOffsetPossible = false
fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory): Builder { fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory): Builder {
this.mediaSourceFactory = mediaSourceFactory this.mediaSourceFactory = mediaSourceFactory
@ -116,11 +111,6 @@ interface NewPlayer {
return this return this
} }
fun setSharingLinkWithOffsetPossible(possible: Boolean): Builder {
this.sharingLinkWithOffsetPossible = false
return this
}
fun build(): NewPlayer { fun build(): NewPlayer {
val exoPlayerBuilder = ExoPlayer.Builder(app) val exoPlayerBuilder = ExoPlayer.Builder(app)
mediaSourceFactory?.let { mediaSourceFactory?.let {
@ -131,284 +121,8 @@ interface NewPlayer {
internalPlayer = exoPlayerBuilder.build(), internalPlayer = exoPlayerBuilder.build(),
repository = repository, repository = repository,
preferredStreamVariants = preferredStreamVariants, preferredStreamVariants = preferredStreamVariants,
sharingLinkWithOffsetPossible = sharingLinkWithOffsetPossible
) )
} }
} }
} }
class NewPlayerImpl(
val app: Application,
override val internalPlayer: Player,
override val preferredStreamVariants: List<String>,
private val repository: MediaRepository,
override val sharingLinkWithOffsetPossible: Boolean
) : NewPlayer {
private var uniqueIdToIdLookup = HashMap<Long, String>()
var mutableErrorFlow = MutableSharedFlow<Exception>()
override val errorFlow = mutableErrorFlow.asSharedFlow()
override val bufferedPercentage: Int
get() = internalPlayer.bufferedPercentage
override var currentPosition: Long
get() = internalPlayer.currentPosition
set(value) {
internalPlayer.seekTo(value)
}
override var fastSeekAmountSec: Int = 10
private var playerScope = CoroutineScope(Dispatchers.Main + Job())
override var playBackMode = MutableStateFlow(PlayMode.IDLE)
override var shuffle: Boolean
get() = internalPlayer.shuffleModeEnabled
set(value) {
internalPlayer.shuffleModeEnabled = value
}
override var repeatMode: RepeatMode
get() = when (internalPlayer.repeatMode) {
Player.REPEAT_MODE_OFF -> RepeatMode.DONT_REPEAT
Player.REPEAT_MODE_ALL -> RepeatMode.REPEAT_ALL
Player.REPEAT_MODE_ONE -> RepeatMode.REPEAT_ONE
else -> throw NewPlayerException("Unknown Repeatmode option returned by ExoPlayer: ${internalPlayer.repeatMode}")
}
set(value) {
when (value) {
RepeatMode.DONT_REPEAT -> internalPlayer.repeatMode = Player.REPEAT_MODE_OFF
RepeatMode.REPEAT_ALL -> internalPlayer.repeatMode = Player.REPEAT_MODE_ALL
RepeatMode.REPEAT_ONE -> internalPlayer.repeatMode = Player.REPEAT_MODE_ONE
}
}
private var mutableOnEvent = MutableSharedFlow<Pair<Player, Player.Events>>()
override val onExoPlayerEvent: SharedFlow<Pair<Player, Player.Events>> =
mutableOnEvent.asSharedFlow()
override var playWhenReady: Boolean
set(value) {
internalPlayer.playWhenReady = value
}
get() = internalPlayer.playWhenReady
override val duration: Long
get() = internalPlayer.duration
private val mutablePlaylist = MutableStateFlow<List<PlaylistItem>>(emptyList())
override val playlist: StateFlow<List<PlaylistItem>> =
mutablePlaylist.asStateFlow()
private val mutableCurrentlyPlaying = MutableStateFlow<PlaylistItem?>(null)
override val currentlyPlaying: StateFlow<PlaylistItem?> = mutableCurrentlyPlaying.asStateFlow()
private val mutableCurrentChapter = MutableStateFlow<List<Chapter>>(emptyList())
override val currentChapters: StateFlow<List<Chapter>> = mutableCurrentChapter.asStateFlow()
override var currentlyPlayingPlaylistItem: Int
get() = internalPlayer.currentMediaItemIndex
set(value) {
assert(value in 0..<playlist.value.size) {
throw NewPlayerException("Playlist item selection out of bound: selected item index: $value, available chapters: ${playlist.value.size}")
}
internalPlayer.seekTo(value, 0)
}
init {
internalPlayer.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
launchJobAndCollectError {
val item = internalPlayer.currentMediaItem?.mediaId
val newUri = repository.tryAndRescueError(item, exception = error)
if (newUri != null) {
TODO("Implement handing new uri on fixed error")
} else {
mutableErrorFlow.emit(error)
}
}
}
override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
launchJobAndCollectError {
mutableOnEvent.emit(Pair(player, events))
}
}
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
super.onTimelineChanged(timeline, reason)
updatePlaylistItems()
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
mediaItem?.let {
val playlistItem = getPlaylistItem(mediaItem.mediaId.toLong())
if (playlistItem != null) {
mutableCurrentlyPlaying.update {
playlistItem
}
} else {
launchJobAndCollectError {
val item = fetchPlaylistItem(
uniqueId = mediaItem.mediaId.toLong(),
mediaRepo = repository,
idLookupTable = uniqueIdToIdLookup
)
mutableCurrentlyPlaying.update { item }
}
}
}
}
})
playerScope.launch {
currentlyPlaying.collect { playing ->
playing?.let {
try {
val chapters = repository.getChapters(playing.id)
mutableCurrentChapter.update { chapters }
} catch (e: Exception) {
mutableErrorFlow.emit(e)
}
}
}
}
}
private fun updatePlaylistItems() {
if (internalPlayer.mediaItemCount == 0) {
playBackMode.update {
PlayMode.IDLE
}
}
playerScope.launch {
val playlist =
getPlaylistItemsFromExoplayer(internalPlayer, repository, uniqueIdToIdLookup)
var playlistDuration = 0
for (item in playlist) {
playlistDuration += item.lengthInS
}
mutablePlaylist.update {
playlist
}
}
}
private fun getPlaylistItem(uniqueId: Long): PlaylistItem? {
for (item in playlist.value) {
if (item.uniqueId == uniqueId) {
return item
}
}
return null
}
override fun prepare() {
internalPlayer.prepare()
}
override fun play() {
if (internalPlayer.currentMediaItem != null) {
internalPlayer.play()
} else {
Log.i(TAG, "Tried to start playing but no media Item was cued")
}
}
override fun pause() {
internalPlayer.pause()
}
override fun addToPlaylist(item: String) {
launchJobAndCollectError {
val mediaItem = toMediaItem(item)
internalPlayer.addMediaItem(mediaItem)
}
}
override fun movePlaylistItem(fromIndex: Int, toIndex: Int) {
internalPlayer.moveMediaItem(fromIndex, toIndex)
}
override fun removePlaylistItem(uniqueId: Long) {
for(i in 0..<internalPlayer.mediaItemCount) {
val id = internalPlayer.getMediaItemAt(i).mediaId.toLong()
if(id == uniqueId) {
internalPlayer.removeMediaItem(i)
break
}
}
}
override fun playStream(item: String, playMode: PlayMode) {
launchJobAndCollectError {
val mediaItem = toMediaItem(item)
internalPlayStream(mediaItem, playMode)
}
}
override fun playStream(item: String, streamVariant: String, playMode: PlayMode) {
launchJobAndCollectError {
val stream = toMediaItem(item, streamVariant)
internalPlayStream(stream, playMode)
}
}
override fun selectChapter(index: Int) {
val chapters = currentChapters.value
assert(index in 0..<chapters.size) {
throw NewPlayerException("Chapter selection out of bound: selected chapter index: $index, available chapters: ${chapters.size}")
}
val chapter = chapters[index]
currentPosition = chapter.chapterStartInMs
}
private fun internalPlayStream(mediaItem: MediaItem, playMode: PlayMode) {
if (internalPlayer.playbackState == Player.STATE_IDLE) {
internalPlayer.prepare()
}
this.playBackMode.update { playMode }
this.internalPlayer.setMediaItem(mediaItem)
this.internalPlayer.play()
}
private suspend fun toMediaItem(item: String, streamVariant: String): MediaItem {
val dataStream = repository.getStream(item, streamVariant)
val uniqueId = Random.nextLong()
uniqueIdToIdLookup[uniqueId] = item
val mediaItem = MediaItem.Builder().setMediaId(uniqueId.toString()).setUri(dataStream)
return mediaItem.build()
}
private suspend fun toMediaItem(item: String): MediaItem {
val availableStream = repository.getAvailableStreamVariants(item)
var selectedStream = availableStream[availableStream.size / 2]
for (preferredStream in preferredStreamVariants) {
if (preferredStream in availableStream) {
selectedStream = preferredStream
break;
}
}
return toMediaItem(item, selectedStream)
}
private fun launchJobAndCollectError(task: suspend () -> Unit) =
playerScope.launch {
try {
task()
} catch (e: Exception) {
mutableErrorFlow.emit(e)
}
}
}

View File

@ -0,0 +1,341 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* NewPlayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPlayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*/
package net.newpipe.newplayer
import android.app.Application
import android.content.ComponentName
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.MoreExecutors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.newpipe.newplayer.model.PlaylistItem
import net.newpipe.newplayer.model.fetchPlaylistItem
import net.newpipe.newplayer.model.getPlaylistItemsFromExoplayer
import net.newpipe.newplayer.service.NewPlayerService
import kotlin.random.Random
private const val TAG = "NewPlayerImpl"
class NewPlayerImpl(
val app: Application,
override val internalPlayer: Player,
override val preferredStreamVariants: List<String>,
private val repository: MediaRepository
) : NewPlayer {
private var uniqueIdToIdLookup = HashMap<Long, String>()
// this is used to take care of the NewPlayerService
private var mediaController: MediaController? = null
var mutableErrorFlow = MutableSharedFlow<Exception>()
override val errorFlow = mutableErrorFlow.asSharedFlow()
override val bufferedPercentage: Int
get() = internalPlayer.bufferedPercentage
override var currentPosition: Long
get() = internalPlayer.currentPosition
set(value) {
internalPlayer.seekTo(value)
}
override var fastSeekAmountSec: Int = 10
private var playerScope = CoroutineScope(Dispatchers.Main + Job())
override var playBackMode = MutableStateFlow(PlayMode.IDLE)
override var shuffle: Boolean
get() = internalPlayer.shuffleModeEnabled
set(value) {
internalPlayer.shuffleModeEnabled = value
}
override var repeatMode: RepeatMode
get() = when (internalPlayer.repeatMode) {
Player.REPEAT_MODE_OFF -> RepeatMode.DONT_REPEAT
Player.REPEAT_MODE_ALL -> RepeatMode.REPEAT_ALL
Player.REPEAT_MODE_ONE -> RepeatMode.REPEAT_ONE
else -> throw NewPlayerException("Unknown Repeatmode option returned by ExoPlayer: ${internalPlayer.repeatMode}")
}
set(value) {
when (value) {
RepeatMode.DONT_REPEAT -> internalPlayer.repeatMode = Player.REPEAT_MODE_OFF
RepeatMode.REPEAT_ALL -> internalPlayer.repeatMode = Player.REPEAT_MODE_ALL
RepeatMode.REPEAT_ONE -> internalPlayer.repeatMode = Player.REPEAT_MODE_ONE
}
}
private var mutableOnEvent = MutableSharedFlow<Pair<Player, Player.Events>>()
override val onExoPlayerEvent: SharedFlow<Pair<Player, Player.Events>> =
mutableOnEvent.asSharedFlow()
override var playWhenReady: Boolean
set(value) {
internalPlayer.playWhenReady = value
}
get() = internalPlayer.playWhenReady
override val duration: Long
get() = internalPlayer.duration
private val mutablePlaylist = MutableStateFlow<List<PlaylistItem>>(emptyList())
override val playlist: StateFlow<List<PlaylistItem>> =
mutablePlaylist.asStateFlow()
private val mutableCurrentlyPlaying = MutableStateFlow<PlaylistItem?>(null)
override val currentlyPlaying: StateFlow<PlaylistItem?> = mutableCurrentlyPlaying.asStateFlow()
private val mutableCurrentChapter = MutableStateFlow<List<Chapter>>(emptyList())
override val currentChapters: StateFlow<List<Chapter>> = mutableCurrentChapter.asStateFlow()
override var currentlyPlayingPlaylistItem: Int
get() = internalPlayer.currentMediaItemIndex
set(value) {
assert(value in 0..<playlist.value.size) {
throw NewPlayerException("Playlist item selection out of bound: selected item index: $value, available chapters: ${playlist.value.size}")
}
internalPlayer.seekTo(value, 0)
}
init {
internalPlayer.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
launchJobAndCollectError {
val item = internalPlayer.currentMediaItem?.mediaId
val newUri = repository.tryAndRescueError(item, exception = error)
if (newUri != null) {
TODO("Implement handing new uri on fixed error")
} else {
mutableErrorFlow.emit(error)
}
}
}
override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
launchJobAndCollectError {
mutableOnEvent.emit(Pair(player, events))
}
}
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
super.onTimelineChanged(timeline, reason)
updatePlaylistItems()
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
mediaItem?.let {
val playlistItem = getPlaylistItem(mediaItem.mediaId.toLong())
if (playlistItem != null) {
mutableCurrentlyPlaying.update {
playlistItem
}
} else {
launchJobAndCollectError {
val item = fetchPlaylistItem(
uniqueId = mediaItem.mediaId.toLong(),
mediaRepo = repository,
idLookupTable = uniqueIdToIdLookup
)
mutableCurrentlyPlaying.update { item }
}
}
}
}
})
playerScope.launch {
currentlyPlaying.collect { playing ->
playing?.let {
try {
val chapters = repository.getChapters(playing.id)
mutableCurrentChapter.update { chapters }
} catch (e: Exception) {
mutableErrorFlow.emit(e)
}
}
}
}
}
private fun updatePlaylistItems() {
if (internalPlayer.mediaItemCount == 0) {
playBackMode.update {
PlayMode.IDLE
}
}
playerScope.launch {
val playlist =
getPlaylistItemsFromExoplayer(internalPlayer, repository, uniqueIdToIdLookup)
var playlistDuration = 0
for (item in playlist) {
playlistDuration += item.lengthInS
}
mutablePlaylist.update {
playlist
}
}
}
private fun getPlaylistItem(uniqueId: Long): PlaylistItem? {
for (item in playlist.value) {
if (item.uniqueId == uniqueId) {
return item
}
}
return null
}
override fun prepare() {
internalPlayer.prepare()
if (mediaController == null) {
val sessionToken = SessionToken(app, ComponentName(app, NewPlayerService::class.java))
val mediaControllerFuture = MediaController.Builder(app, sessionToken).buildAsync()
mediaControllerFuture.addListener({
mediaController = mediaControllerFuture.get()
}, MoreExecutors.directExecutor())
}
}
override fun play() {
if (internalPlayer.currentMediaItem != null) {
internalPlayer.play()
} else {
Log.i(TAG, "Tried to start playing but no media Item was cued")
}
}
override fun pause() {
internalPlayer.pause()
}
override fun addToPlaylist(item: String) {
launchJobAndCollectError {
val mediaItem = toMediaItem(item)
internalPlayer.addMediaItem(mediaItem)
}
}
override fun movePlaylistItem(fromIndex: Int, toIndex: Int) {
internalPlayer.moveMediaItem(fromIndex, toIndex)
}
override fun removePlaylistItem(uniqueId: Long) {
for (i in 0..<internalPlayer.mediaItemCount) {
val id = internalPlayer.getMediaItemAt(i).mediaId.toLong()
if (id == uniqueId) {
internalPlayer.removeMediaItem(i)
break
}
}
}
override fun playStream(item: String, playMode: PlayMode) {
launchJobAndCollectError {
val mediaItem = toMediaItem(item)
internalPlayStream(mediaItem, playMode)
}
}
override fun playStream(item: String, streamVariant: String, playMode: PlayMode) {
launchJobAndCollectError {
val stream = toMediaItem(item, streamVariant)
internalPlayStream(stream, playMode)
}
}
override fun selectChapter(index: Int) {
val chapters = currentChapters.value
assert(index in 0..<chapters.size) {
throw NewPlayerException("Chapter selection out of bound: selected chapter index: $index, available chapters: ${chapters.size}")
}
val chapter = chapters[index]
currentPosition = chapter.chapterStartInMs
}
override fun release() {
internalPlayer.release()
}
private fun internalPlayStream(mediaItem: MediaItem, playMode: PlayMode) {
if (internalPlayer.playbackState == Player.STATE_IDLE) {
prepare()
}
this.playBackMode.update { playMode }
this.internalPlayer.setMediaItem(mediaItem)
this.internalPlayer.play()
}
private suspend fun toMediaItem(item: String, streamVariant: String): MediaItem {
val dataStream = repository.getStream(item, streamVariant)
val uniqueId = Random.nextLong()
uniqueIdToIdLookup[uniqueId] = item
val mediaItem = MediaItem.Builder().setMediaId(uniqueId.toString()).setUri(dataStream)
return mediaItem.build()
}
private suspend fun toMediaItem(item: String): MediaItem {
val availableStream = repository.getAvailableStreamVariants(item)
var selectedStream = availableStream[availableStream.size / 2]
for (preferredStream in preferredStreamVariants) {
if (preferredStream in availableStream) {
selectedStream = preferredStream
break;
}
}
return toMediaItem(item, selectedStream)
}
private fun launchJobAndCollectError(task: suspend () -> Unit) =
playerScope.launch {
try {
task()
} catch (e: Exception) {
mutableErrorFlow.emit(e)
}
}
}

View File

@ -21,7 +21,7 @@
package net.newpipe.newplayer.playerInternals package net.newpipe.newplayer.model
import net.newpipe.newplayer.utils.Thumbnail import net.newpipe.newplayer.utils.Thumbnail

View File

@ -19,9 +19,8 @@
* *
*/ */
package net.newpipe.newplayer.playerInternals package net.newpipe.newplayer.model
import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -29,7 +28,6 @@ import net.newpipe.newplayer.MediaRepository
import net.newpipe.newplayer.NewPlayerException import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.utils.Thumbnail import net.newpipe.newplayer.utils.Thumbnail
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
import kotlin.random.Random
data class PlaylistItem( data class PlaylistItem(
val title: String, val title: String,

View File

@ -22,7 +22,6 @@ package net.newpipe.newplayer.model
import net.newpipe.newplayer.Chapter import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.RepeatMode import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
data class VideoPlayerUIState( data class VideoPlayerUIState(

View File

@ -45,7 +45,6 @@ import kotlinx.coroutines.launch
import net.newpipe.newplayer.utils.VideoSize import net.newpipe.newplayer.utils.VideoSize
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.RepeatMode import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
import java.util.LinkedList import java.util.LinkedList

View File

@ -0,0 +1,52 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* NewPlayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPlayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*/
package net.newpipe.newplayer.service
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import dagger.hilt.android.AndroidEntryPoint
import net.newpipe.newplayer.NewPlayer
import javax.inject.Inject
@AndroidEntryPoint
class NewPlayerService : MediaSessionService() {
private var mediaSession: MediaSession? = null
@Inject
lateinit var newPlayer: NewPlayer
override fun onDestroy() {
super.onDestroy()
newPlayer.release()
mediaSession?.release()
mediaSession = null
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
println("gurken get session")
if (mediaSession == null) {
mediaSession = MediaSession.Builder(this, newPlayer.internalPlayer).build()
}
return mediaSession
}
}

View File

@ -41,8 +41,7 @@ import net.newpipe.newplayer.model.VideoPlayerUIState
import net.newpipe.newplayer.model.VideoPlayerViewModel import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.Chapter import net.newpipe.newplayer.model.PlaylistItem
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.ui.STREAMSELECT_UI_BACKGROUND_COLOR import net.newpipe.newplayer.ui.STREAMSELECT_UI_BACKGROUND_COLOR
import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterItem import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterItem
import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterSelectTopBar import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterSelectTopBar

View File

@ -20,13 +20,9 @@
package net.newpipe.newplayer.ui.videoplayer.streamselect package net.newpipe.newplayer.ui.videoplayer.streamselect
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -40,7 +36,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -53,33 +48,25 @@ import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.playerInternals.PlaylistItem import net.newpipe.newplayer.model.PlaylistItem
import net.newpipe.newplayer.ui.CONTROLLER_UI_BACKGROUND_COLOR import net.newpipe.newplayer.ui.CONTROLLER_UI_BACKGROUND_COLOR
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.ui.videoplayer.ITEM_CORNER_SHAPE import net.newpipe.newplayer.ui.videoplayer.ITEM_CORNER_SHAPE
import net.newpipe.newplayer.ui.videoplayer.gesture_ui.SEEK_ANIMATION_FADE_IN
import net.newpipe.newplayer.ui.videoplayer.gesture_ui.SEEK_ANIMATION_FADE_OUT
import net.newpipe.newplayer.utils.BitmapThumbnail
import net.newpipe.newplayer.utils.OnlineThumbnail
import net.newpipe.newplayer.utils.ReorderHapticFeedback import net.newpipe.newplayer.utils.ReorderHapticFeedback
import net.newpipe.newplayer.utils.ReorderHapticFeedbackType import net.newpipe.newplayer.utils.ReorderHapticFeedbackType
import net.newpipe.newplayer.utils.Thumbnail import net.newpipe.newplayer.utils.Thumbnail
import net.newpipe.newplayer.utils.VectorThumbnail
import net.newpipe.newplayer.utils.getLocale import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs import net.newpipe.newplayer.utils.getTimeStringFromMs
import sh.calvin.reorderable.ReorderableCollectionItemScope import sh.calvin.reorderable.ReorderableCollectionItemScope

View File

@ -47,7 +47,7 @@ import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.model.VideoPlayerUIState import net.newpipe.newplayer.model.VideoPlayerUIState
import net.newpipe.newplayer.model.VideoPlayerViewModel import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
import net.newpipe.newplayer.playerInternals.getPlaylistDurationInS import net.newpipe.newplayer.model.getPlaylistDurationInS
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.utils.getLocale import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs import net.newpipe.newplayer.utils.getTimeStringFromMs