mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-31 00:29:22 +00:00
feat(plugins): add support for plugin subtitles with header injection and format detection
This commit is contained in:
parent
75dc501c60
commit
9b372ea118
14 changed files with 224 additions and 19 deletions
|
|
@ -35,6 +35,9 @@ import androidx.media3.common.PlaybackException
|
|||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DataSpec
|
||||
import androidx.media3.datasource.TransferListener
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
|
|
@ -68,6 +71,7 @@ actual fun PlatformPlayerSurface(
|
|||
sourceAudioUrl: String?,
|
||||
sourceHeaders: Map<String, String>,
|
||||
sourceResponseHeaders: Map<String, String>,
|
||||
externalSubtitles: List<com.nuvio.app.features.streams.StreamSubtitle>,
|
||||
useYoutubeChunkedPlayback: Boolean,
|
||||
modifier: Modifier,
|
||||
playWhenReady: Boolean,
|
||||
|
|
@ -146,12 +150,26 @@ actual fun PlatformPlayerSurface(
|
|||
.setTsExtractorFlags(DefaultTsPayloadReaderFactory.FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS)
|
||||
.setTsExtractorTimestampSearchBytes(1500 * TsExtractor.TS_PACKET_SIZE)
|
||||
|
||||
val dataSourceFactory = PlatformPlaybackDataSourceFactory.create(
|
||||
context = context,
|
||||
defaultRequestHeaders = sanitizedSourceHeaders,
|
||||
val baseNetworkFactory = if (useYoutubeChunkedPlayback) {
|
||||
YoutubeChunkedDataSourceFactory(defaultRequestHeaders = sanitizedSourceHeaders)
|
||||
} else {
|
||||
PlayerPlaybackNetworking.createHttpDataSourceFactory(sanitizedSourceHeaders)
|
||||
}
|
||||
|
||||
val subtitleHeaderFactory = SubtitleRequestHeaderDataSourceFactory(
|
||||
upstreamFactory = baseNetworkFactory,
|
||||
externalSubtitles = externalSubtitles
|
||||
)
|
||||
|
||||
val baseFactory: DataSource.Factory = DefaultDataSource.Factory(context, subtitleHeaderFactory)
|
||||
val dataSourceFactory = if (sanitizedSourceResponseHeaders.isEmpty()) {
|
||||
baseFactory
|
||||
} else {
|
||||
ResponseHeaderOverridingDataSourceFactory(
|
||||
upstreamFactory = baseFactory,
|
||||
defaultResponseHeaders = sanitizedSourceResponseHeaders,
|
||||
useYoutubeChunkedPlayback = useYoutubeChunkedPlayback,
|
||||
)
|
||||
}
|
||||
|
||||
val player = if (useLibass) {
|
||||
ExoPlayer.Builder(context)
|
||||
|
|
@ -179,13 +197,33 @@ actual fun PlatformPlayerSurface(
|
|||
}
|
||||
|
||||
player.apply {
|
||||
val mediaItemBuilder = MediaItem.Builder()
|
||||
.setUri(Uri.parse(sourceUrl))
|
||||
.setMediaId(sourceUrl)
|
||||
|
||||
val subtitleConfigs = externalSubtitles.mapNotNull { subtitle ->
|
||||
val mimeType = resolveSubtitleMimeType(subtitle.url, subtitle.headers)
|
||||
MediaItem.SubtitleConfiguration.Builder(Uri.parse(subtitle.url))
|
||||
.setMimeType(mimeType)
|
||||
.setLanguage(subtitle.language)
|
||||
.setLabel(subtitle.name ?: subtitle.language)
|
||||
.setRoleFlags(C.ROLE_FLAG_SUBTITLE)
|
||||
.build()
|
||||
}
|
||||
|
||||
if (subtitleConfigs.isNotEmpty()) {
|
||||
mediaItemBuilder.setSubtitleConfigurations(subtitleConfigs)
|
||||
}
|
||||
|
||||
val mediaItem = mediaItemBuilder.build()
|
||||
|
||||
if (!sourceAudioUrl.isNullOrBlank()) {
|
||||
val msf = DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory)
|
||||
val videoSource = msf.createMediaSource(MediaItem.fromUri(sourceUrl))
|
||||
val videoSource = msf.createMediaSource(mediaItem)
|
||||
val audioSource = msf.createMediaSource(MediaItem.fromUri(sourceAudioUrl))
|
||||
setMediaSource(MergingMediaSource(videoSource, audioSource))
|
||||
} else {
|
||||
setMediaItem(MediaItem.fromUri(sourceUrl))
|
||||
setMediaItem(mediaItem)
|
||||
}
|
||||
fallbackStartPositionMs?.let { seekTo(it.coerceAtLeast(0L)) }
|
||||
prepare()
|
||||
|
|
@ -709,15 +747,15 @@ private fun ExoPlayer.logCurrentTracks(context: String) {
|
|||
Log.d(TAG, "--- end logCurrentTracks ---")
|
||||
}
|
||||
|
||||
private fun resolveSubtitleMimeType(url: String): String {
|
||||
probeSubtitleHeaders(url)?.let { (contentType, contentDisposition) ->
|
||||
private fun resolveSubtitleMimeType(url: String, headers: Map<String, String>? = null): String {
|
||||
probeSubtitleHeaders(url, headers)?.let { (contentType, contentDisposition) ->
|
||||
mapSubtitleMime(contentType)?.let { return it }
|
||||
filenameFromContentDisposition(contentDisposition)?.let(::guessSubtitleMime)?.let { return it }
|
||||
}
|
||||
return guessSubtitleMime(url)
|
||||
}
|
||||
|
||||
private fun probeSubtitleHeaders(url: String): Pair<String?, String?>? {
|
||||
private fun probeSubtitleHeaders(url: String, headers: Map<String, String>? = null): Pair<String?, String?>? {
|
||||
val methods = listOf("HEAD", "GET")
|
||||
methods.forEach { method ->
|
||||
runCatching {
|
||||
|
|
@ -727,6 +765,9 @@ private fun probeSubtitleHeaders(url: String): Pair<String?, String?>? {
|
|||
readTimeout = 5_000
|
||||
instanceFollowRedirects = true
|
||||
setRequestProperty("Accept", "*/*")
|
||||
headers?.forEach { (key, value) ->
|
||||
setRequestProperty(key, value)
|
||||
}
|
||||
}
|
||||
try {
|
||||
connection.responseCode
|
||||
|
|
@ -781,3 +822,50 @@ private fun guessSubtitleMime(url: String): String {
|
|||
else -> MimeTypes.TEXT_VTT
|
||||
}
|
||||
}
|
||||
|
||||
private class SubtitleRequestHeaderDataSourceFactory(
|
||||
private val upstreamFactory: DataSource.Factory,
|
||||
private val externalSubtitles: List<com.nuvio.app.features.streams.StreamSubtitle>,
|
||||
) : DataSource.Factory {
|
||||
override fun createDataSource(): DataSource =
|
||||
SubtitleRequestHeaderDataSource(
|
||||
upstream = upstreamFactory.createDataSource(),
|
||||
externalSubtitles = externalSubtitles,
|
||||
)
|
||||
}
|
||||
|
||||
private class SubtitleRequestHeaderDataSource(
|
||||
private val upstream: DataSource,
|
||||
private val externalSubtitles: List<com.nuvio.app.features.streams.StreamSubtitle>,
|
||||
) : DataSource {
|
||||
override fun addTransferListener(transferListener: TransferListener) {
|
||||
upstream.addTransferListener(transferListener)
|
||||
}
|
||||
|
||||
override fun open(dataSpec: DataSpec): Long {
|
||||
val url = dataSpec.uri.toString()
|
||||
val subtitle = externalSubtitles.find { it.url == url }
|
||||
val headers = subtitle?.headers
|
||||
|
||||
return if (headers.isNullOrEmpty()) {
|
||||
upstream.open(dataSpec)
|
||||
} else {
|
||||
val mergedHeaders = dataSpec.httpRequestHeaders.toMutableMap()
|
||||
headers.forEach { (key, value) ->
|
||||
mergedHeaders[key] = value
|
||||
}
|
||||
upstream.open(dataSpec.buildUpon().setHttpRequestHeaders(mergedHeaders).build())
|
||||
}
|
||||
}
|
||||
|
||||
override fun read(buffer: ByteArray, offset: Int, length: Int): Int =
|
||||
upstream.read(buffer, offset, length)
|
||||
|
||||
override fun getUri(): Uri? = upstream.uri
|
||||
|
||||
override fun getResponseHeaders(): Map<String, List<String>> = upstream.responseHeaders
|
||||
|
||||
override fun close() {
|
||||
upstream.close()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -872,6 +872,7 @@ private fun MainAppContent(
|
|||
sourceUrl = localSourceUrl,
|
||||
sourceHeaders = emptyMap(),
|
||||
sourceResponseHeaders = emptyMap(),
|
||||
externalSubtitles = emptyList(),
|
||||
logo = logo,
|
||||
poster = poster,
|
||||
background = background,
|
||||
|
|
@ -1457,6 +1458,7 @@ private fun MainAppContent(
|
|||
sourceUrl = cached.url,
|
||||
sourceHeaders = sanitizePlaybackHeaders(cached.requestHeaders),
|
||||
sourceResponseHeaders = sanitizePlaybackResponseHeaders(cached.responseHeaders),
|
||||
externalSubtitles = emptyList(),
|
||||
logo = launch.logo,
|
||||
poster = launch.poster,
|
||||
background = launch.background,
|
||||
|
|
@ -1563,6 +1565,7 @@ private fun MainAppContent(
|
|||
sourceUrl = sourceUrl,
|
||||
sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||
sourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
||||
externalSubtitles = stream.externalSubtitles,
|
||||
logo = launch.logo,
|
||||
poster = launch.poster,
|
||||
background = launch.background,
|
||||
|
|
@ -1582,8 +1585,7 @@ private fun MainAppContent(
|
|||
parentMetaType = launch.parentMetaType ?: launch.type,
|
||||
initialPositionMs = launch.resumePositionMs ?: 0L,
|
||||
initialProgressFraction = launch.resumeProgressFraction,
|
||||
)
|
||||
StreamsRepository.consumeAutoPlay()
|
||||
) StreamsRepository.consumeAutoPlay()
|
||||
StreamsRepository.cancelLoading()
|
||||
if (playerSettings.externalPlayerEnabled) {
|
||||
openExternalPlayback(playerLaunch)
|
||||
|
|
@ -1673,6 +1675,7 @@ private fun MainAppContent(
|
|||
sourceUrl = sourceUrl,
|
||||
sourceHeaders = sanitizePlaybackHeaders(stream.behaviorHints.proxyHeaders?.request),
|
||||
sourceResponseHeaders = sanitizePlaybackResponseHeaders(stream.behaviorHints.proxyHeaders?.response),
|
||||
externalSubtitles = stream.externalSubtitles,
|
||||
logo = launch.logo,
|
||||
poster = launch.poster,
|
||||
background = launch.background,
|
||||
|
|
@ -1803,6 +1806,7 @@ private fun MainAppContent(
|
|||
sourceAudioUrl = launch.sourceAudioUrl,
|
||||
sourceHeaders = launch.sourceHeaders,
|
||||
sourceResponseHeaders = launch.sourceResponseHeaders,
|
||||
externalSubtitles = launch.externalSubtitles,
|
||||
logo = launch.logo,
|
||||
poster = launch.poster,
|
||||
background = launch.background,
|
||||
|
|
@ -1908,6 +1912,7 @@ private fun MainAppContent(
|
|||
sourceUrl = sourceUrl,
|
||||
sourceHeaders = emptyMap(),
|
||||
sourceResponseHeaders = emptyMap(),
|
||||
externalSubtitles = emptyList(),
|
||||
logo = item.logo,
|
||||
poster = item.poster,
|
||||
background = item.background,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ expect fun PlatformPlayerSurface(
|
|||
sourceAudioUrl: String? = null,
|
||||
sourceHeaders: Map<String, String> = emptyMap(),
|
||||
sourceResponseHeaders: Map<String, String> = emptyMap(),
|
||||
externalSubtitles: List<com.nuvio.app.features.streams.StreamSubtitle> = emptyList(),
|
||||
useYoutubeChunkedPlayback: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
playWhenReady: Boolean = true,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ data class PlayerLaunch(
|
|||
val sourceAudioUrl: String? = null,
|
||||
val sourceHeaders: Map<String, String> = emptyMap(),
|
||||
val sourceResponseHeaders: Map<String, String> = emptyMap(),
|
||||
val externalSubtitles: List<com.nuvio.app.features.streams.StreamSubtitle> = emptyList(),
|
||||
val logo: String? = null,
|
||||
val poster: String? = null,
|
||||
val background: String? = null,
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ fun PlayerScreen(
|
|||
sourceAudioUrl: String? = null,
|
||||
sourceHeaders: Map<String, String> = emptyMap(),
|
||||
sourceResponseHeaders: Map<String, String> = emptyMap(),
|
||||
externalSubtitles: List<com.nuvio.app.features.streams.StreamSubtitle> = emptyList(),
|
||||
providerName: String,
|
||||
streamTitle: String,
|
||||
streamSubtitle: String?,
|
||||
|
|
@ -144,6 +145,8 @@ fun PlayerScreen(
|
|||
videoId: String? = null,
|
||||
parentMetaId: String,
|
||||
parentMetaType: String,
|
||||
parentMetaLogo: String? = null,
|
||||
parentMetaPoster: String? = null,
|
||||
providerAddonId: String? = null,
|
||||
initialPositionMs: Long = 0L,
|
||||
initialProgressFraction: Float? = null,
|
||||
|
|
@ -1713,6 +1716,7 @@ fun PlayerScreen(
|
|||
sourceAudioUrl = activeSourceAudioUrl,
|
||||
sourceHeaders = activeSourceHeaders,
|
||||
sourceResponseHeaders = activeSourceResponseHeaders,
|
||||
externalSubtitles = externalSubtitles,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
playWhenReady = shouldPlay,
|
||||
resizeMode = resizeMode,
|
||||
|
|
|
|||
|
|
@ -401,5 +401,13 @@ private fun PluginRuntimeResult.toStreamItem(scraper: PluginScraper): StreamItem
|
|||
proxyHeaders = com.nuvio.app.features.streams.StreamProxyHeaders(request = requestHeaders),
|
||||
)
|
||||
},
|
||||
externalSubtitles = subtitles?.map {
|
||||
com.nuvio.app.features.streams.StreamSubtitle(
|
||||
url = it.url,
|
||||
language = it.language,
|
||||
name = it.name,
|
||||
headers = it.headers
|
||||
)
|
||||
} ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,15 @@ data class PluginRuntimeResult(
|
|||
val peers: Int? = null,
|
||||
val infoHash: String? = null,
|
||||
val headers: Map<String, String>? = null,
|
||||
val subtitles: List<PluginSubtitleResult>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PluginSubtitleResult(
|
||||
val url: String,
|
||||
val language: String,
|
||||
val name: String? = null,
|
||||
val headers: Map<String, String>? = null
|
||||
)
|
||||
|
||||
data class PluginsUiState(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@ import kotlinx.coroutines.runBlocking
|
|||
import nuvio.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
@Serializable
|
||||
data class StreamSubtitle(
|
||||
val url: String,
|
||||
val language: String,
|
||||
val name: String? = null,
|
||||
val headers: Map<String, String>? = null
|
||||
)
|
||||
|
||||
data class StreamItem(
|
||||
val name: String? = null,
|
||||
val title: String? = null,
|
||||
|
|
@ -18,6 +26,7 @@ data class StreamItem(
|
|||
val addonId: String,
|
||||
val behaviorHints: StreamBehaviorHints = StreamBehaviorHints(),
|
||||
val clientResolve: StreamClientResolve? = null,
|
||||
val externalSubtitles: List<StreamSubtitle> = emptyList(),
|
||||
) {
|
||||
val streamLabel: String
|
||||
get() = name ?: runBlocking { getString(Res.string.stream_default_name) }
|
||||
|
|
|
|||
|
|
@ -665,6 +665,14 @@ private fun PluginRuntimeResult.toStreamItem(
|
|||
proxyHeaders = StreamProxyHeaders(request = requestHeaders),
|
||||
)
|
||||
},
|
||||
externalSubtitles = subtitles?.map {
|
||||
StreamSubtitle(
|
||||
url = it.url,
|
||||
language = it.language,
|
||||
name = it.name,
|
||||
headers = it.headers
|
||||
)
|
||||
} ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class PlayerLaunchStoreTest {
|
|||
val launch = PlayerLaunch(
|
||||
title = "Title",
|
||||
sourceUrl = "https://example.com/video.m3u8?token=a/b:c",
|
||||
externalSubtitles = emptyList(),
|
||||
streamTitle = "Source",
|
||||
providerName = "Provider",
|
||||
parentMetaId = "tt1234567",
|
||||
|
|
|
|||
|
|
@ -206,6 +206,25 @@ internal object PluginRuntime {
|
|||
?.toMap()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
val subtitles = (item["subtitles"] as? JsonArray)?.mapNotNull { subElement ->
|
||||
val subObj = subElement as? JsonObject ?: return@mapNotNull null
|
||||
val subUrl = subObj["url"]?.jsonPrimitive?.contentOrNull ?: return@mapNotNull null
|
||||
val subLang = subObj["language"]?.jsonPrimitive?.contentOrNull ?: "Unknown"
|
||||
val subName = subObj["name"]?.jsonPrimitive?.contentOrNull
|
||||
val subHeaders = (subObj["headers"] as? JsonObject)
|
||||
?.mapNotNull { (key, value) ->
|
||||
value.jsonPrimitive.contentOrNull?.let { key to it }
|
||||
}
|
||||
?.toMap()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
com.nuvio.app.features.plugins.PluginSubtitleResult(
|
||||
url = subUrl,
|
||||
language = subLang,
|
||||
name = subName,
|
||||
headers = subHeaders
|
||||
)
|
||||
}?.takeIf { it.isNotEmpty() }
|
||||
|
||||
PluginRuntimeResult(
|
||||
title = item.stringOrNull("title") ?: item.stringOrNull("name") ?: "Unknown",
|
||||
name = item.stringOrNull("name"),
|
||||
|
|
@ -219,6 +238,7 @@ internal object PluginRuntime {
|
|||
peers = item["peers"]?.jsonPrimitive?.intOrNull,
|
||||
infoHash = item.stringOrNull("infoHash"),
|
||||
headers = headers,
|
||||
subtitles = subtitles,
|
||||
)
|
||||
}.filter { it.url.isNotBlank() }
|
||||
}.getOrElse { emptyList() }
|
||||
|
|
|
|||
|
|
@ -9,7 +9,12 @@ import platform.UIKit.UIViewController
|
|||
interface NuvioPlayerBridge {
|
||||
fun createPlayerViewController(): UIViewController
|
||||
fun loadFile(url: String)
|
||||
fun loadFileWithAudio(videoUrl: String, audioUrl: String?, headersJson: String?)
|
||||
fun loadFileWithAudio(
|
||||
videoUrl: String,
|
||||
audioUrl: String?,
|
||||
headersJson: String?,
|
||||
subtitlesJson: String? = null
|
||||
)
|
||||
fun play()
|
||||
fun pause()
|
||||
fun seekTo(positionMs: Long)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ actual fun PlatformPlayerSurface(
|
|||
sourceAudioUrl: String?,
|
||||
sourceHeaders: Map<String, String>,
|
||||
sourceResponseHeaders: Map<String, String>,
|
||||
externalSubtitles: List<com.nuvio.app.features.streams.StreamSubtitle>,
|
||||
useYoutubeChunkedPlayback: Boolean,
|
||||
modifier: Modifier,
|
||||
playWhenReady: Boolean,
|
||||
|
|
@ -222,12 +223,13 @@ actual fun PlatformPlayerSurface(
|
|||
}
|
||||
|
||||
// Load file and set initial state
|
||||
LaunchedEffect(bridge, sourceUrl, sourceAudioUrl, sourceHeaders) {
|
||||
LaunchedEffect(bridge, sourceUrl, sourceAudioUrl, sourceHeaders, externalSubtitles) {
|
||||
bridge.applyIosVideoOutputSettings(latestPlayerSettings.value)
|
||||
bridge.loadFileWithAudio(
|
||||
sourceUrl,
|
||||
sourceAudioUrl,
|
||||
encodePlaybackHeadersForBridge(sourceHeaders),
|
||||
videoUrl = sourceUrl,
|
||||
audioUrl = sourceAudioUrl,
|
||||
headersJson = encodePlaybackHeadersForBridge(sourceHeaders),
|
||||
subtitlesJson = encodeExternalSubtitlesForBridge(externalSubtitles),
|
||||
)
|
||||
if (playWhenReady) {
|
||||
bridge.play()
|
||||
|
|
@ -339,6 +341,13 @@ private fun Int.toHexByte(): String {
|
|||
}
|
||||
}
|
||||
|
||||
private fun encodeExternalSubtitlesForBridge(subtitles: List<com.nuvio.app.features.streams.StreamSubtitle>): String? {
|
||||
if (subtitles.isEmpty()) return null
|
||||
return runCatching {
|
||||
Json.encodeToString(subtitles)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun encodePlaybackHeadersForBridge(headers: Map<String, String>): String? {
|
||||
val sanitized = sanitizePlaybackHeaders(headers)
|
||||
if (sanitized.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -16,13 +16,41 @@ final class MPVPlayerBridgeImpl: NSObject, NuvioPlayerBridge {
|
|||
}
|
||||
|
||||
func loadFile(url: String) { playerVC?.loadFile(url) }
|
||||
func loadFileWithAudio(videoUrl: String, audioUrl: String?, headersJson: String?) {
|
||||
func loadFileWithAudio(videoUrl: String, audioUrl: String?, headersJson: String?, subtitlesJson: String?) {
|
||||
playerVC?.loadFile(
|
||||
videoUrl,
|
||||
audioUrl: audioUrl,
|
||||
requestHeaders: parseRequestHeaders(headersJson)
|
||||
requestHeaders: parseRequestHeaders(headersJson),
|
||||
subtitles: parseSubtitles(subtitlesJson)
|
||||
)
|
||||
}
|
||||
|
||||
private func parseSubtitles(_ json: String?) -> [PluginSubtitle] {
|
||||
guard
|
||||
let json,
|
||||
let data = json.data(using: .utf8),
|
||||
let raw = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]]
|
||||
else {
|
||||
return []
|
||||
}
|
||||
return raw.compactMap { dict in
|
||||
guard let url = dict["url"] as? String else { return nil }
|
||||
return PluginSubtitle(
|
||||
url: url,
|
||||
language: dict["language"] as? String ?? "Unknown",
|
||||
name: dict["name"] as? String,
|
||||
headers: dict["headers"] as? [String: String]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PluginSubtitle {
|
||||
val url: String
|
||||
val language: String
|
||||
val name: String?
|
||||
val headers: [String: String]?
|
||||
}
|
||||
func play() { playerVC?.playPlayback() }
|
||||
func pause() { playerVC?.pausePlayback() }
|
||||
func seekTo(positionMs: Int64) { playerVC?.seekToMs(positionMs) }
|
||||
|
|
@ -172,6 +200,7 @@ private struct PendingLoadRequest {
|
|||
let urlString: String
|
||||
let audioUrl: String?
|
||||
let requestHeaders: [String: String]
|
||||
let subtitles: [PluginSubtitle]
|
||||
let queuedAtUptime: TimeInterval
|
||||
}
|
||||
|
||||
|
|
@ -357,11 +386,12 @@ final class MPVPlayerViewController: UIViewController {
|
|||
|
||||
// MARK: - Playback API
|
||||
|
||||
func loadFile(_ urlString: String, audioUrl: String? = nil, requestHeaders: [String: String] = [:]) {
|
||||
func loadFile(_ urlString: String, audioUrl: String? = nil, requestHeaders: [String: String] = [:], subtitles: [PluginSubtitle] = []) {
|
||||
let request = PendingLoadRequest(
|
||||
urlString: urlString,
|
||||
audioUrl: audioUrl,
|
||||
requestHeaders: requestHeaders,
|
||||
subtitles: subtitles,
|
||||
queuedAtUptime: ProcessInfo.processInfo.systemUptime
|
||||
)
|
||||
|
||||
|
|
@ -409,6 +439,13 @@ final class MPVPlayerViewController: UIViewController {
|
|||
self?.command("audio-add", args: [audioUrl, "select"], checkForErrors: false)
|
||||
}
|
||||
}
|
||||
|
||||
// Add external subtitles
|
||||
for subtitle in request.subtitles {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||
self?.command("sub-add", args: [subtitle.url, "auto", subtitle.name ?? subtitle.language, subtitle.language], checkForErrors: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func isViewportReadyForPlayback(queuedAtUptime: TimeInterval) -> Bool {
|
||||
|
|
|
|||
Loading…
Reference in a new issue