mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
libmpv init macos
This commit is contained in:
parent
9f7f465a25
commit
5f1370d89b
6 changed files with 347 additions and 25 deletions
|
|
@ -213,6 +213,7 @@ kotlin {
|
|||
implementation(compose.desktop.currentOs)
|
||||
implementation(libs.ktor.client.java)
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
implementation(libs.jna)
|
||||
}
|
||||
}
|
||||
androidMain.dependencies {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,25 @@ interface PlayerEngineController {
|
|||
fun clearExternalSubtitle()
|
||||
fun clearExternalSubtitleAndSelect(trackIndex: Int)
|
||||
fun applySubtitleStyle(style: SubtitleStyleState) {}
|
||||
fun setMetadata(
|
||||
title: String,
|
||||
streamTitle: String,
|
||||
providerName: String,
|
||||
seasonNumber: Int? = null,
|
||||
episodeNumber: Int? = null,
|
||||
episodeTitle: String? = null,
|
||||
) {}
|
||||
fun showSkipButton(type: String, endTimeMs: Long) {}
|
||||
fun hideSkipButton() {}
|
||||
fun showNextEpisode(
|
||||
season: Int,
|
||||
episode: Int,
|
||||
title: String,
|
||||
thumbnail: String? = null,
|
||||
hasAired: Boolean = true,
|
||||
) {}
|
||||
fun hideNextEpisode() {}
|
||||
fun setOnCloseCallback(callback: () -> Unit) {}
|
||||
}
|
||||
|
||||
internal fun sanitizePlaybackHeaders(headers: Map<String, String>?): Map<String, String> {
|
||||
|
|
|
|||
|
|
@ -1180,6 +1180,30 @@ fun PlayerScreen(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(activeSkipInterval, skipIntervalDismissed) {
|
||||
val interval = activeSkipInterval
|
||||
if (interval != null && !skipIntervalDismissed) {
|
||||
playerController?.showSkipButton(interval.type, (interval.endTime * 1000).toLong())
|
||||
} else {
|
||||
playerController?.hideSkipButton()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(showNextEpisodeCard, nextEpisodeInfo) {
|
||||
val info = nextEpisodeInfo
|
||||
if (showNextEpisodeCard && info != null) {
|
||||
playerController?.showNextEpisode(
|
||||
season = info.season,
|
||||
episode = info.episode,
|
||||
title = info.title ?: "",
|
||||
thumbnail = info.thumbnail,
|
||||
hasAired = info.hasAired,
|
||||
)
|
||||
} else {
|
||||
playerController?.hideNextEpisode()
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve next episode info when episodes list or current episode changes
|
||||
LaunchedEffect(allEpisodes, activeSeasonNumber, activeEpisodeNumber) {
|
||||
if (!isSeries || allEpisodes.isEmpty()) {
|
||||
|
|
@ -1407,6 +1431,15 @@ fun PlayerScreen(
|
|||
onControllerReady = { controller ->
|
||||
playerController = controller
|
||||
playerControllerSourceUrl = activeSourceUrl
|
||||
controller.setMetadata(
|
||||
title = title,
|
||||
streamTitle = activeStreamTitle,
|
||||
providerName = activeProviderName,
|
||||
seasonNumber = activeSeasonNumber,
|
||||
episodeNumber = activeEpisodeNumber,
|
||||
episodeTitle = activeEpisodeTitle,
|
||||
)
|
||||
controller.setOnCloseCallback { onBackWithProgress() }
|
||||
},
|
||||
onSnapshot = { snapshot ->
|
||||
playbackSnapshot = snapshot
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
package com.nuvio.app.features.player
|
||||
|
||||
import com.sun.jna.Library
|
||||
import com.sun.jna.Native
|
||||
import com.sun.jna.Pointer
|
||||
|
||||
internal interface DesktopMPVBridgeLib : Library {
|
||||
companion object {
|
||||
val INSTANCE: DesktopMPVBridgeLib by lazy {
|
||||
val libPath = resolveLibraryPath()
|
||||
if (libPath != null) {
|
||||
System.setProperty(
|
||||
"jna.library.path",
|
||||
(System.getProperty("jna.library.path") ?: "") + ":" + libPath,
|
||||
)
|
||||
}
|
||||
Native.load("DesktopMPVBridge", DesktopMPVBridgeLib::class.java)
|
||||
}
|
||||
|
||||
private fun resolveLibraryPath(): String? {
|
||||
val candidates = listOf(
|
||||
"MPVKit/.build/arm64-apple-macosx/release",
|
||||
"MPVKit/.build/arm64-apple-macosx/debug",
|
||||
"../MPVKit/.build/arm64-apple-macosx/release",
|
||||
"../MPVKit/.build/arm64-apple-macosx/debug",
|
||||
)
|
||||
val userDir = System.getProperty("user.dir") ?: return null
|
||||
for (candidate in candidates) {
|
||||
val dir = java.io.File(userDir, candidate)
|
||||
if (dir.exists() && dir.isDirectory) {
|
||||
val dylib = java.io.File(dir, "libDesktopMPVBridge.dylib")
|
||||
if (dylib.exists()) return dir.absolutePath
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun nuvio_player_create(): Pointer
|
||||
fun nuvio_player_destroy(player: Pointer)
|
||||
fun nuvio_player_show(player: Pointer)
|
||||
|
||||
fun nuvio_player_set_metadata(
|
||||
player: Pointer,
|
||||
title: String,
|
||||
streamTitle: String,
|
||||
providerName: String,
|
||||
season: Int,
|
||||
episode: Int,
|
||||
episodeTitle: String?,
|
||||
)
|
||||
|
||||
fun nuvio_player_load_file(
|
||||
player: Pointer,
|
||||
url: String,
|
||||
audioUrl: String?,
|
||||
headersJson: String?,
|
||||
)
|
||||
|
||||
fun nuvio_player_play(player: Pointer)
|
||||
fun nuvio_player_pause(player: Pointer)
|
||||
fun nuvio_player_seek_to(player: Pointer, positionMs: Long)
|
||||
fun nuvio_player_seek_by(player: Pointer, offsetMs: Long)
|
||||
fun nuvio_player_set_speed(player: Pointer, speed: Float)
|
||||
fun nuvio_player_set_resize_mode(player: Pointer, mode: Int)
|
||||
fun nuvio_player_retry(player: Pointer)
|
||||
|
||||
fun nuvio_player_refresh_state(player: Pointer)
|
||||
fun nuvio_player_is_loading(player: Pointer): Boolean
|
||||
fun nuvio_player_is_playing(player: Pointer): Boolean
|
||||
fun nuvio_player_is_ended(player: Pointer): Boolean
|
||||
fun nuvio_player_get_position_ms(player: Pointer): Long
|
||||
fun nuvio_player_get_duration_ms(player: Pointer): Long
|
||||
fun nuvio_player_get_buffered_ms(player: Pointer): Long
|
||||
fun nuvio_player_get_speed(player: Pointer): Float
|
||||
fun nuvio_player_get_error(player: Pointer): String?
|
||||
|
||||
fun nuvio_player_get_audio_track_count(player: Pointer): Int
|
||||
fun nuvio_player_get_audio_track_id(player: Pointer, index: Int): Int
|
||||
fun nuvio_player_get_audio_track_label(player: Pointer, index: Int): String?
|
||||
fun nuvio_player_get_audio_track_lang(player: Pointer, index: Int): String?
|
||||
fun nuvio_player_is_audio_track_selected(player: Pointer, index: Int): Boolean
|
||||
fun nuvio_player_select_audio_track(player: Pointer, trackId: Int)
|
||||
|
||||
fun nuvio_player_get_subtitle_track_count(player: Pointer): Int
|
||||
fun nuvio_player_get_subtitle_track_id(player: Pointer, index: Int): Int
|
||||
fun nuvio_player_get_subtitle_track_label(player: Pointer, index: Int): String?
|
||||
fun nuvio_player_get_subtitle_track_lang(player: Pointer, index: Int): String?
|
||||
fun nuvio_player_is_subtitle_track_selected(player: Pointer, index: Int): Boolean
|
||||
fun nuvio_player_select_subtitle_track(player: Pointer, trackId: Int)
|
||||
|
||||
fun nuvio_player_set_subtitle_url(player: Pointer, url: String)
|
||||
fun nuvio_player_clear_external_subtitle(player: Pointer)
|
||||
fun nuvio_player_clear_external_subtitle_and_select(player: Pointer, trackId: Int)
|
||||
fun nuvio_player_apply_subtitle_style(
|
||||
player: Pointer,
|
||||
textColor: String,
|
||||
outlineSize: Float,
|
||||
fontSize: Float,
|
||||
subPos: Int,
|
||||
)
|
||||
|
||||
fun nuvio_player_show_skip_button(player: Pointer, type: String, endTimeMs: Long)
|
||||
fun nuvio_player_hide_skip_button(player: Pointer)
|
||||
|
||||
fun nuvio_player_show_next_episode(
|
||||
player: Pointer,
|
||||
season: Int,
|
||||
episode: Int,
|
||||
title: String,
|
||||
thumbnail: String?,
|
||||
hasAired: Boolean,
|
||||
)
|
||||
fun nuvio_player_hide_next_episode(player: Pointer)
|
||||
|
||||
fun nuvio_player_is_closed(player: Pointer): Boolean
|
||||
fun nuvio_player_pop_next_episode_pressed(player: Pointer): Boolean
|
||||
}
|
||||
|
|
@ -3,13 +3,16 @@ package com.nuvio.app.features.player
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.material3.Text
|
||||
import com.nuvio.app.core.storage.ProfileScopedKey
|
||||
import com.nuvio.app.core.sync.decodeSyncBoolean
|
||||
import com.nuvio.app.core.sync.decodeSyncFloat
|
||||
|
|
@ -22,7 +25,9 @@ import com.nuvio.app.core.sync.encodeSyncInt
|
|||
import com.nuvio.app.core.sync.encodeSyncString
|
||||
import com.nuvio.app.core.sync.encodeSyncStringSet
|
||||
import com.nuvio.app.desktop.DesktopPreferences
|
||||
import com.sun.jna.Pointer
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
|
@ -42,53 +47,197 @@ actual fun PlatformPlayerSurface(
|
|||
onSnapshot: (PlayerPlaybackSnapshot) -> Unit,
|
||||
onError: (String?) -> Unit,
|
||||
) {
|
||||
val controller = remember {
|
||||
val bridge = remember { DesktopMPVBridgeLib.INSTANCE }
|
||||
val playerPtr = remember { bridge.nuvio_player_create() }
|
||||
var onCloseCallback by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||
|
||||
DisposableEffect(playerPtr) {
|
||||
bridge.nuvio_player_show(playerPtr)
|
||||
onDispose {
|
||||
bridge.nuvio_player_destroy(playerPtr)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(sourceUrl, sourceAudioUrl) {
|
||||
val headersJson = if (sourceHeaders.isNotEmpty()) {
|
||||
buildJsonObject {
|
||||
sourceHeaders.forEach { (k, v) -> put(k, v) }
|
||||
}.toString()
|
||||
} else null
|
||||
bridge.nuvio_player_load_file(playerPtr, sourceUrl, sourceAudioUrl, headersJson)
|
||||
if (playWhenReady) {
|
||||
bridge.nuvio_player_play(playerPtr)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(resizeMode) {
|
||||
val mode = when (resizeMode) {
|
||||
PlayerResizeMode.Fit -> 0
|
||||
PlayerResizeMode.Fill -> 1
|
||||
PlayerResizeMode.Zoom -> 2
|
||||
}
|
||||
bridge.nuvio_player_set_resize_mode(playerPtr, mode)
|
||||
}
|
||||
|
||||
val controller = remember(playerPtr) {
|
||||
object : PlayerEngineController {
|
||||
override fun play() = Unit
|
||||
override fun play() = bridge.nuvio_player_play(playerPtr)
|
||||
override fun pause() = bridge.nuvio_player_pause(playerPtr)
|
||||
override fun seekTo(positionMs: Long) = bridge.nuvio_player_seek_to(playerPtr, positionMs)
|
||||
override fun seekBy(offsetMs: Long) = bridge.nuvio_player_seek_by(playerPtr, offsetMs)
|
||||
override fun retry() = bridge.nuvio_player_retry(playerPtr)
|
||||
override fun setPlaybackSpeed(speed: Float) = bridge.nuvio_player_set_speed(playerPtr, speed)
|
||||
|
||||
override fun pause() = Unit
|
||||
override fun getAudioTracks(): List<AudioTrack> {
|
||||
val count = bridge.nuvio_player_get_audio_track_count(playerPtr)
|
||||
return (0 until count).map { i ->
|
||||
AudioTrack(
|
||||
index = i,
|
||||
id = bridge.nuvio_player_get_audio_track_id(playerPtr, i).toString(),
|
||||
label = bridge.nuvio_player_get_audio_track_label(playerPtr, i) ?: "",
|
||||
language = bridge.nuvio_player_get_audio_track_lang(playerPtr, i),
|
||||
isSelected = bridge.nuvio_player_is_audio_track_selected(playerPtr, i),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun seekTo(positionMs: Long) = Unit
|
||||
override fun getSubtitleTracks(): List<SubtitleTrack> {
|
||||
val count = bridge.nuvio_player_get_subtitle_track_count(playerPtr)
|
||||
return (0 until count).map { i ->
|
||||
SubtitleTrack(
|
||||
index = i,
|
||||
id = bridge.nuvio_player_get_subtitle_track_id(playerPtr, i).toString(),
|
||||
label = bridge.nuvio_player_get_subtitle_track_label(playerPtr, i) ?: "",
|
||||
language = bridge.nuvio_player_get_subtitle_track_lang(playerPtr, i),
|
||||
isSelected = bridge.nuvio_player_is_subtitle_track_selected(playerPtr, i),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun seekBy(offsetMs: Long) = Unit
|
||||
override fun selectAudioTrack(index: Int) {
|
||||
val count = bridge.nuvio_player_get_audio_track_count(playerPtr)
|
||||
if (index in 0 until count) {
|
||||
val trackId = bridge.nuvio_player_get_audio_track_id(playerPtr, index)
|
||||
bridge.nuvio_player_select_audio_track(playerPtr, trackId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun retry() = Unit
|
||||
override fun selectSubtitleTrack(index: Int) {
|
||||
if (index < 0) {
|
||||
bridge.nuvio_player_select_subtitle_track(playerPtr, -1)
|
||||
return
|
||||
}
|
||||
val count = bridge.nuvio_player_get_subtitle_track_count(playerPtr)
|
||||
if (index in 0 until count) {
|
||||
val trackId = bridge.nuvio_player_get_subtitle_track_id(playerPtr, index)
|
||||
bridge.nuvio_player_select_subtitle_track(playerPtr, trackId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPlaybackSpeed(speed: Float) = Unit
|
||||
override fun setSubtitleUri(url: String) =
|
||||
bridge.nuvio_player_set_subtitle_url(playerPtr, url)
|
||||
|
||||
override fun getAudioTracks(): List<AudioTrack> = emptyList()
|
||||
override fun clearExternalSubtitle() =
|
||||
bridge.nuvio_player_clear_external_subtitle(playerPtr)
|
||||
|
||||
override fun getSubtitleTracks(): List<SubtitleTrack> = emptyList()
|
||||
override fun clearExternalSubtitleAndSelect(trackIndex: Int) {
|
||||
val trackId = if (trackIndex >= 0) {
|
||||
val count = bridge.nuvio_player_get_subtitle_track_count(playerPtr)
|
||||
if (trackIndex < count) bridge.nuvio_player_get_subtitle_track_id(playerPtr, trackIndex) else -1
|
||||
} else -1
|
||||
bridge.nuvio_player_clear_external_subtitle_and_select(playerPtr, trackId)
|
||||
}
|
||||
|
||||
override fun selectAudioTrack(index: Int) = Unit
|
||||
override fun applySubtitleStyle(style: SubtitleStyleState) {
|
||||
val colorHex = style.textColor.toMpvColorString()
|
||||
val outline = if (style.outlineEnabled) 2.0f else 0.0f
|
||||
bridge.nuvio_player_apply_subtitle_style(
|
||||
playerPtr, colorHex, outline, style.fontSizeSp.toFloat(), style.bottomOffset,
|
||||
)
|
||||
}
|
||||
|
||||
override fun selectSubtitleTrack(index: Int) = Unit
|
||||
override fun setMetadata(
|
||||
title: String,
|
||||
streamTitle: String,
|
||||
providerName: String,
|
||||
seasonNumber: Int?,
|
||||
episodeNumber: Int?,
|
||||
episodeTitle: String?,
|
||||
) {
|
||||
bridge.nuvio_player_set_metadata(
|
||||
playerPtr, title, streamTitle, providerName,
|
||||
seasonNumber ?: 0, episodeNumber ?: 0, episodeTitle,
|
||||
)
|
||||
}
|
||||
|
||||
override fun setSubtitleUri(url: String) = Unit
|
||||
override fun showSkipButton(type: String, endTimeMs: Long) {
|
||||
bridge.nuvio_player_show_skip_button(playerPtr, type, endTimeMs)
|
||||
}
|
||||
|
||||
override fun clearExternalSubtitle() = Unit
|
||||
override fun hideSkipButton() {
|
||||
bridge.nuvio_player_hide_skip_button(playerPtr)
|
||||
}
|
||||
|
||||
override fun clearExternalSubtitleAndSelect(trackIndex: Int) = Unit
|
||||
override fun showNextEpisode(
|
||||
season: Int,
|
||||
episode: Int,
|
||||
title: String,
|
||||
thumbnail: String?,
|
||||
hasAired: Boolean,
|
||||
) {
|
||||
bridge.nuvio_player_show_next_episode(playerPtr, season, episode, title, thumbnail, hasAired)
|
||||
}
|
||||
|
||||
override fun hideNextEpisode() {
|
||||
bridge.nuvio_player_hide_next_episode(playerPtr)
|
||||
}
|
||||
|
||||
override fun setOnCloseCallback(callback: () -> Unit) {
|
||||
onCloseCallback = callback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(controller) {
|
||||
onControllerReady(controller)
|
||||
onSnapshot(PlayerPlaybackSnapshot(isLoading = false))
|
||||
onError(null)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier.background(Color.Black),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "Desktop playback is not implemented yet.",
|
||||
color = Color.White,
|
||||
)
|
||||
LaunchedEffect(playerPtr) {
|
||||
while (true) {
|
||||
delay(250)
|
||||
if (bridge.nuvio_player_is_closed(playerPtr)) {
|
||||
onCloseCallback?.invoke()
|
||||
break
|
||||
}
|
||||
bridge.nuvio_player_refresh_state(playerPtr)
|
||||
val snapshot = PlayerPlaybackSnapshot(
|
||||
isLoading = bridge.nuvio_player_is_loading(playerPtr),
|
||||
isPlaying = bridge.nuvio_player_is_playing(playerPtr),
|
||||
isEnded = bridge.nuvio_player_is_ended(playerPtr),
|
||||
positionMs = bridge.nuvio_player_get_position_ms(playerPtr),
|
||||
durationMs = bridge.nuvio_player_get_duration_ms(playerPtr),
|
||||
bufferedPositionMs = bridge.nuvio_player_get_buffered_ms(playerPtr),
|
||||
playbackSpeed = bridge.nuvio_player_get_speed(playerPtr),
|
||||
)
|
||||
onSnapshot(snapshot)
|
||||
val error = bridge.nuvio_player_get_error(playerPtr)
|
||||
onError(error)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier.background(Color.Black))
|
||||
}
|
||||
|
||||
private fun androidx.compose.ui.graphics.Color.toMpvColorString(): String {
|
||||
val r = (red * 255).toInt().coerceIn(0, 255)
|
||||
val g = (green * 255).toInt().coerceIn(0, 255)
|
||||
val b = (blue * 255).toInt().coerceIn(0, 255)
|
||||
val a = (alpha * 255).toInt().coerceIn(0, 255)
|
||||
return "#${r.hex()}${g.hex()}${b.hex()}${a.hex()}"
|
||||
}
|
||||
|
||||
private fun Int.hex(): String = toString(16).padStart(2, '0').uppercase()
|
||||
|
||||
internal actual object DeviceLanguagePreferences {
|
||||
actual fun preferredLanguageCodes(): List<String> =
|
||||
listOfNotNull(Locale.getDefault().toLanguageTag().takeIf { it.isNotBlank() })
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ supabase = "3.4.1"
|
|||
quickjsKt = "1.0.1"
|
||||
ksoup = "0.2.6"
|
||||
reorderable = "3.0.0"
|
||||
jna = "5.14.0"
|
||||
|
||||
[libraries]
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
|
|
@ -76,6 +77,7 @@ supabase-functions = { module = "io.github.jan-tennert.supabase:functions-kt", v
|
|||
quickjs-kt = { module = "io.github.dokar3:quickjs-kt", version.ref = "quickjsKt" }
|
||||
ksoup = { module = "com.fleeksoft.ksoup:ksoup", version.ref = "ksoup" }
|
||||
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||
|
||||
[plugins]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
|
|
|
|||
Loading…
Reference in a new issue