introduce NewPlayer Service
This commit is contained in:
parent
ba03e17088
commit
0744d43b20
|
@ -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
|
||||
|
||||
|
|
@ -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" }
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
|
||||
|
||||
package net.newpipe.newplayer.playerInternals
|
||||
package net.newpipe.newplayer.model
|
||||
|
||||
import net.newpipe.newplayer.utils.Thumbnail
|
||||
|
|
@ -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,
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue