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"
coil = "2.7.0"
reorderable = "2.4.0-alpha02"
media3Session = "1.4.1"
[libraries]
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" }
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
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.coil.compose)
implementation(libs.reorderable)
implementation(libs.androidx.media3.session)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)

View File

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

View File

@ -37,13 +37,11 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.newpipe.newplayer.model.UIModeState
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.playerInternals.fetchPlaylistItem
import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromExoplayer
import net.newpipe.newplayer.model.PlaylistItem
import net.newpipe.newplayer.model.fetchPlaylistItem
import net.newpipe.newplayer.model.getPlaylistItemsFromExoplayer
import kotlin.Exception
import kotlin.random.Random
@ -62,8 +60,6 @@ enum class RepeatMode {
REPEAT_ONE
}
private val TAG = "NewPlayer"
interface NewPlayer {
// preferences
val preferredStreamVariants: List<String>
@ -72,7 +68,6 @@ interface NewPlayer {
var playWhenReady: Boolean
val duration: Long
val bufferedPercentage: Int
val sharingLinkWithOffsetPossible: Boolean
var currentPosition: Long
var fastSeekAmountSec: Int
val playBackMode: MutableStateFlow<PlayMode>
@ -100,11 +95,11 @@ interface NewPlayer {
fun playStream(item: String, playMode: PlayMode)
fun selectChapter(index: Int)
fun playStream(item: String, streamVariant: String, playMode: PlayMode)
fun release()
data class Builder(val app: Application, val repository: MediaRepository) {
private var mediaSourceFactory: MediaSource.Factory? = null
private var preferredStreamVariants: List<String> = emptyList()
private var sharingLinkWithOffsetPossible = false
fun setMediaSourceFactory(mediaSourceFactory: MediaSource.Factory): Builder {
this.mediaSourceFactory = mediaSourceFactory
@ -116,11 +111,6 @@ interface NewPlayer {
return this
}
fun setSharingLinkWithOffsetPossible(possible: Boolean): Builder {
this.sharingLinkWithOffsetPossible = false
return this
}
fun build(): NewPlayer {
val exoPlayerBuilder = ExoPlayer.Builder(app)
mediaSourceFactory?.let {
@ -131,284 +121,8 @@ interface NewPlayer {
internalPlayer = exoPlayerBuilder.build(),
repository = repository,
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

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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
@ -29,7 +28,6 @@ import net.newpipe.newplayer.MediaRepository
import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.utils.Thumbnail
import kotlin.coroutines.coroutineContext
import kotlin.random.Random
data class PlaylistItem(
val title: String,

View File

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

View File

@ -45,7 +45,6 @@ import kotlinx.coroutines.launch
import net.newpipe.newplayer.utils.VideoSize
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.RepeatMode
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.ui.ContentScale
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.VideoPlayerViewModelDummy
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.model.PlaylistItem
import net.newpipe.newplayer.ui.STREAMSELECT_UI_BACKGROUND_COLOR
import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterItem
import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterSelectTopBar

View File

@ -20,13 +20,9 @@
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.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material3.ExperimentalMaterial3Api
@ -53,33 +48,25 @@ import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
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.theme.VideoPlayerTheme
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.ReorderHapticFeedbackType
import net.newpipe.newplayer.utils.Thumbnail
import net.newpipe.newplayer.utils.VectorThumbnail
import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs
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.VideoPlayerViewModel
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.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs