mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
Simplify Jellyfin playback url retrieval
This commit is contained in:
@@ -17,8 +17,6 @@ import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.player.preference.TrackPreferencesRepository
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.data.jellyfin.playback.JellyfinPlaybackResolver
|
||||
import hu.bbara.purefin.data.jellyfin.playback.PlaybackDecision
|
||||
import hu.bbara.purefin.data.jellyfin.playback.playbackCustomCacheKey
|
||||
import hu.bbara.purefin.model.Episode
|
||||
import hu.bbara.purefin.model.MediaSegment
|
||||
import hu.bbara.purefin.model.PlayableMedia
|
||||
@@ -86,14 +84,14 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
downloadedMediaItem: MediaItem?,
|
||||
): PlayableMedia? {
|
||||
val baseItem = jellyfinApiClient.getItemInfo(mediaId) ?: return null
|
||||
val playbackDecision = jellyfinPlaybackResolver.getPlaybackDecision(mediaId) ?: return null
|
||||
val playbackSource = jellyfinPlaybackResolver.getPlaybackSource(mediaId) ?: return null
|
||||
|
||||
val mediaItem = if (downloadedMediaItem == null) {
|
||||
getMediaItem(baseItem, playbackDecision)
|
||||
getMediaItem(baseItem, playbackSource.directPlayUrl)
|
||||
} else {
|
||||
getDownloadedMediaItem(baseItem, playbackDecision, downloadedMediaItem)
|
||||
getDownloadedMediaItem(baseItem, downloadedMediaItem)
|
||||
}
|
||||
val resumePositionMs = calculateResumePosition(baseItem, playbackDecision.mediaSource)
|
||||
val resumePositionMs = calculateResumePosition(baseItem, playbackSource.mediaSource)
|
||||
val preferenceMediaId = when (baseItem.type) {
|
||||
BaseItemKind.EPISODE -> baseItem.seriesId ?: mediaId
|
||||
else -> mediaId
|
||||
@@ -172,7 +170,7 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun getMediaItem(baseItem: BaseItemDto, playbackDecision: PlaybackDecision): MediaItem = withContext(Dispatchers.IO) {
|
||||
private suspend fun getMediaItem(baseItem: BaseItemDto, url: String): MediaItem = withContext(Dispatchers.IO) {
|
||||
val mediaId = baseItem.id
|
||||
val baseItem = jellyfinApiClient.getItemInfo(mediaId)
|
||||
|
||||
@@ -181,11 +179,12 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
|
||||
val mediaItem = createMediaItem(
|
||||
mediaId = mediaId.toString(),
|
||||
playbackDecision = playbackDecision,
|
||||
title = baseItem?.name ?: playbackDecision.mediaSource.name ?: "Unknown",
|
||||
url = url,
|
||||
title = baseItem?.name ?: "Unknown",
|
||||
subtitle = seasonEpisodeLabel(baseItem),
|
||||
artworkUrl = artworkUrl,
|
||||
playbackReportContext = playbackDecision.reportContext,
|
||||
// TODO
|
||||
playbackReportContext = null,
|
||||
)
|
||||
|
||||
return@withContext mediaItem
|
||||
@@ -193,7 +192,6 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
|
||||
private suspend fun getDownloadedMediaItem(
|
||||
baseItem: BaseItemDto,
|
||||
playbackDecision: PlaybackDecision,
|
||||
downloadedMediaItem: MediaItem,
|
||||
): MediaItem = withContext(Dispatchers.IO) {
|
||||
val mediaId = baseItem.id
|
||||
@@ -203,10 +201,11 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
|
||||
return@withContext downloadedMediaItem.withMetadata(
|
||||
mediaId = mediaId.toString(),
|
||||
title = baseItem?.name ?: playbackDecision.mediaSource.name ?: "Unknown",
|
||||
title = baseItem?.name ?: "Unknown",
|
||||
subtitle = seasonEpisodeLabel(baseItem),
|
||||
artworkUrl = artworkUrl,
|
||||
playbackReportContext = playbackDecision.reportContext,
|
||||
// TODO
|
||||
playbackReportContext = null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -240,7 +239,7 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun createMediaItem(
|
||||
mediaId: String,
|
||||
playbackDecision: PlaybackDecision,
|
||||
url: String,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
artworkUrl: String,
|
||||
@@ -252,17 +251,10 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
.setArtworkUri(artworkUrl.toUri())
|
||||
.build()
|
||||
val builder = MediaItem.Builder()
|
||||
.setUri(playbackDecision.url.toUri())
|
||||
.setUri(url.toUri())
|
||||
.setMediaId(mediaId)
|
||||
.setMediaMetadata(metadata)
|
||||
.setTag(playbackReportContext)
|
||||
|
||||
playbackCustomCacheKey(
|
||||
mediaId = mediaId,
|
||||
playbackUrl = playbackDecision.url,
|
||||
playMethod = playbackDecision.reportContext.playMethod,
|
||||
)?.let(builder::setCustomCacheKey)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -511,7 +511,6 @@ class JellyfinApiClient @Inject constructor(
|
||||
mediaSourceId = reportContext.mediaSourceId,
|
||||
audioStreamIndex = reportContext.audioStreamIndex,
|
||||
subtitleStreamIndex = reportContext.subtitleStreamIndex,
|
||||
liveStreamId = reportContext.liveStreamId,
|
||||
playSessionId = reportContext.playSessionId,
|
||||
playMethod = reportContext.playMethod.toJellyfinPlayMethod(),
|
||||
repeatMode = RepeatMode.REPEAT_NONE,
|
||||
@@ -539,7 +538,6 @@ class JellyfinApiClient @Inject constructor(
|
||||
mediaSourceId = reportContext.mediaSourceId,
|
||||
audioStreamIndex = reportContext.audioStreamIndex,
|
||||
subtitleStreamIndex = reportContext.subtitleStreamIndex,
|
||||
liveStreamId = reportContext.liveStreamId,
|
||||
playSessionId = reportContext.playSessionId,
|
||||
playMethod = reportContext.playMethod.toJellyfinPlayMethod(),
|
||||
repeatMode = RepeatMode.REPEAT_NONE,
|
||||
@@ -561,7 +559,6 @@ class JellyfinApiClient @Inject constructor(
|
||||
itemId = itemId,
|
||||
positionTicks = positionTicks,
|
||||
mediaSourceId = reportContext.mediaSourceId,
|
||||
liveStreamId = reportContext.liveStreamId,
|
||||
playSessionId = reportContext.playSessionId,
|
||||
failed = false,
|
||||
),
|
||||
|
||||
@@ -3,7 +3,6 @@ package hu.bbara.purefin.data.jellyfin.download
|
||||
import hu.bbara.purefin.core.data.DownloadMediaSourceResolver
|
||||
import hu.bbara.purefin.core.data.EpisodeDownloadSource
|
||||
import hu.bbara.purefin.core.data.MovieDownloadSource
|
||||
import hu.bbara.purefin.core.data.PlaybackMethod
|
||||
import hu.bbara.purefin.core.data.UserSessionRepository
|
||||
import hu.bbara.purefin.data.converter.toEpisode
|
||||
import hu.bbara.purefin.data.converter.toMovie
|
||||
@@ -11,7 +10,7 @@ import hu.bbara.purefin.data.converter.toSeason
|
||||
import hu.bbara.purefin.data.converter.toSeries
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.data.jellyfin.playback.JellyfinPlaybackResolver
|
||||
import hu.bbara.purefin.data.jellyfin.playback.PlaybackDecision
|
||||
import hu.bbara.purefin.data.jellyfin.playback.PlaybackSource
|
||||
import hu.bbara.purefin.data.jellyfin.playback.playbackCustomCacheKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -32,13 +31,13 @@ class JellyfinDownloadMediaSourceResolver @Inject constructor(
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val playbackDecision = jellyfinPlaybackResolver.getPlaybackDecision(movieId) ?: return@withContext null
|
||||
val playbackSource = jellyfinPlaybackResolver.getPlaybackSource(movieId) ?: return@withContext null
|
||||
val itemInfo = jellyfinApiClient.getItemInfo(movieId) ?: return@withContext null
|
||||
|
||||
MovieDownloadSource(
|
||||
movie = itemInfo.toMovie(serverUrl),
|
||||
playbackUrl = playbackDecision.url,
|
||||
customCacheKey = playbackDecision.downloadCustomCacheKey(movieId),
|
||||
playbackUrl = playbackSource.directPlayUrl,
|
||||
customCacheKey = playbackSource.downloadCustomCacheKey(movieId),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,7 +47,7 @@ class JellyfinDownloadMediaSourceResolver @Inject constructor(
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val playbackDecision = jellyfinPlaybackResolver.getPlaybackDecision(episodeId) ?: return@withContext null
|
||||
val playbackSource = jellyfinPlaybackResolver.getPlaybackSource(episodeId) ?: return@withContext null
|
||||
val episodeDto = jellyfinApiClient.getItemInfo(episodeId) ?: return@withContext null
|
||||
val episode = episodeDto.toEpisode(serverUrl)
|
||||
val series = jellyfinApiClient.getItemInfo(episode.seriesId)?.toSeries(serverUrl) ?: return@withContext null
|
||||
@@ -58,8 +57,8 @@ class JellyfinDownloadMediaSourceResolver @Inject constructor(
|
||||
episode = episode,
|
||||
series = series,
|
||||
season = season,
|
||||
playbackUrl = playbackDecision.url,
|
||||
customCacheKey = playbackDecision.downloadCustomCacheKey(episodeId),
|
||||
playbackUrl = playbackSource.directPlayUrl,
|
||||
customCacheKey = playbackSource.downloadCustomCacheKey(episodeId),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,14 +90,11 @@ class JellyfinDownloadMediaSourceResolver @Inject constructor(
|
||||
.map { it.id }
|
||||
}
|
||||
|
||||
private fun PlaybackDecision.downloadCustomCacheKey(mediaId: UUID): String? {
|
||||
if (reportContext.playMethod != PlaybackMethod.DIRECT_PLAY) {
|
||||
return null
|
||||
}
|
||||
private fun PlaybackSource.downloadCustomCacheKey(mediaId: UUID): String? {
|
||||
return playbackCustomCacheKey(
|
||||
mediaId = mediaId.toString(),
|
||||
playbackUrl = url,
|
||||
playMethod = reportContext.playMethod,
|
||||
playbackUrl = directPlayUrl,
|
||||
playMethod = playbackReportContext.playMethod,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import hu.bbara.purefin.core.data.PlaybackMethod
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
import hu.bbara.purefin.core.data.UserSessionRepository
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -17,7 +19,7 @@ class JellyfinPlaybackResolver @Inject constructor(
|
||||
private val playbackProfilePolicy: PlaybackProfilePolicy,
|
||||
) {
|
||||
|
||||
suspend fun getPlaybackDecision(mediaId: UUID): PlaybackDecision? = withContext(Dispatchers.IO) {
|
||||
suspend fun getPlaybackSource(mediaId: UUID): PlaybackSource? = withContext(Dispatchers.IO) {
|
||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||
if (serverUrl.isBlank()) {
|
||||
return@withContext null
|
||||
@@ -33,6 +35,11 @@ class JellyfinPlaybackResolver @Inject constructor(
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
if (playbackInfo.mediaSources.isEmpty()) {
|
||||
Timber.tag(TAG).w("No media sources available for $mediaId")
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val directPlayUrl = jellyfinApiClient.getVideoStreamUrl(
|
||||
itemId = mediaId,
|
||||
)
|
||||
@@ -41,19 +48,18 @@ class JellyfinPlaybackResolver @Inject constructor(
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val decision = PlaybackDecisionResolver.resolve(
|
||||
mediaSources = playbackInfo.mediaSources,
|
||||
playSessionId = playbackInfo.playSessionId,
|
||||
serverUrl = serverUrl,
|
||||
directPlayUrl = directPlayUrl
|
||||
PlaybackSource(
|
||||
mediaSource = playbackInfo.mediaSources.first(),
|
||||
directPlayUrl = directPlayUrl,
|
||||
transcodingUrl = playbackInfo.mediaSources.firstOrNull()?.transcodingUrl,
|
||||
playbackReportContext = PlaybackReportContext(
|
||||
playMethod = PlaybackMethod.DIRECT_PLAY,
|
||||
mediaSourceId = playbackInfo.mediaSources.firstOrNull()?.id,
|
||||
audioStreamIndex = playbackInfo.mediaSources.firstOrNull()?.defaultAudioStreamIndex,
|
||||
subtitleStreamIndex = playbackInfo.mediaSources.firstOrNull()?.defaultSubtitleStreamIndex,
|
||||
playSessionId = playbackInfo.playSessionId,
|
||||
),
|
||||
)
|
||||
|
||||
if (decision == null) {
|
||||
Timber.tag(TAG).w("No compatible playback path for $mediaId")
|
||||
} else {
|
||||
Timber.tag(TAG).d("Playback decision for $mediaId resolved as ${decision.reportContext.playMethod}")
|
||||
}
|
||||
decision
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import hu.bbara.purefin.core.data.PlaybackMethod
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
import org.jellyfin.sdk.model.api.MediaProtocol
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
|
||||
internal object PlaybackDecisionResolver {
|
||||
fun resolve(
|
||||
mediaSources: List<MediaSourceInfo>,
|
||||
playSessionId: String?,
|
||||
serverUrl: String,
|
||||
directPlayUrl: String,
|
||||
): PlaybackDecision? {
|
||||
val mediaSource = mediaSources.firstOrNull { it.protocol == MediaProtocol.FILE && !it.isRemote }
|
||||
?: return null
|
||||
|
||||
val playMethod = when {
|
||||
mediaSource.supportsDirectPlay -> PlaybackMethod.DIRECT_PLAY
|
||||
mediaSource.supportsDirectStream && !mediaSource.transcodingUrl.isNullOrBlank() -> PlaybackMethod.DIRECT_STREAM
|
||||
mediaSource.supportsTranscoding && !mediaSource.transcodingUrl.isNullOrBlank() -> PlaybackMethod.TRANSCODE
|
||||
else -> return null
|
||||
}
|
||||
|
||||
val url = when (playMethod) {
|
||||
PlaybackMethod.DIRECT_PLAY -> directPlayUrl
|
||||
PlaybackMethod.DIRECT_STREAM,
|
||||
PlaybackMethod.TRANSCODE,
|
||||
-> absolutePlaybackUrl(serverUrl, requireNotNull(mediaSource.transcodingUrl))
|
||||
}
|
||||
|
||||
return PlaybackDecision(
|
||||
url = url,
|
||||
mediaSource = mediaSource,
|
||||
reportContext = PlaybackReportContext(
|
||||
playMethod = playMethod,
|
||||
mediaSourceId = mediaSource.id,
|
||||
audioStreamIndex = mediaSource.defaultAudioStreamIndex,
|
||||
subtitleStreamIndex = mediaSource.defaultSubtitleStreamIndex,
|
||||
liveStreamId = mediaSource.liveStreamId,
|
||||
playSessionId = playSessionId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun absolutePlaybackUrl(serverUrl: String, playbackUrl: String): String = when {
|
||||
playbackUrl.startsWith("http://", ignoreCase = true) -> playbackUrl
|
||||
playbackUrl.startsWith("https://", ignoreCase = true) -> playbackUrl
|
||||
playbackUrl.startsWith("/") -> "${serverUrl.trimEnd('/')}$playbackUrl"
|
||||
else -> "${serverUrl.trimEnd('/')}/${playbackUrl.trimStart('/')}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
|
||||
data class PlaybackSource(
|
||||
val mediaSource: MediaSourceInfo,
|
||||
val directPlayUrl: String,
|
||||
val transcodingUrl: String?,
|
||||
val playbackReportContext: PlaybackReportContext,
|
||||
)
|
||||
@@ -1,151 +0,0 @@
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import hu.bbara.purefin.core.data.PlaybackMethod
|
||||
import org.jellyfin.sdk.model.api.MediaProtocol
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.MediaSourceType
|
||||
import org.jellyfin.sdk.model.api.MediaStreamProtocol
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
|
||||
class PlaybackDecisionResolverTest {
|
||||
@Test
|
||||
fun `resolve prefers local file-backed source for direct play`() {
|
||||
val remoteSource = mediaSource(
|
||||
id = "remote",
|
||||
protocol = MediaProtocol.HTTP,
|
||||
isRemote = true,
|
||||
supportsDirectPlay = true,
|
||||
)
|
||||
val localSource = mediaSource(
|
||||
id = "local",
|
||||
supportsDirectPlay = true,
|
||||
defaultAudioStreamIndex = 2,
|
||||
defaultSubtitleStreamIndex = 4,
|
||||
liveStreamId = "live-1",
|
||||
)
|
||||
|
||||
val decision = requireNotNull(
|
||||
PlaybackDecisionResolver.resolve(
|
||||
mediaSources = listOf(remoteSource, localSource),
|
||||
playSessionId = "session-1",
|
||||
serverUrl = "https://example.com/jellyfin",
|
||||
) { mediaSource ->
|
||||
"direct://${mediaSource.id}"
|
||||
}
|
||||
)
|
||||
|
||||
assertSame(localSource, decision.mediaSource)
|
||||
assertEquals("direct://local", decision.url)
|
||||
assertEquals(PlaybackMethod.DIRECT_PLAY, decision.reportContext.playMethod)
|
||||
assertEquals("local", decision.reportContext.mediaSourceId)
|
||||
assertEquals(2, decision.reportContext.audioStreamIndex)
|
||||
assertEquals(4, decision.reportContext.subtitleStreamIndex)
|
||||
assertEquals("live-1", decision.reportContext.liveStreamId)
|
||||
assertEquals("session-1", decision.reportContext.playSessionId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve uses transcoding url for direct stream`() {
|
||||
val source = mediaSource(
|
||||
id = "direct-stream",
|
||||
supportsDirectStream = true,
|
||||
transcodingUrl = "/Videos/1/master.m3u8",
|
||||
)
|
||||
|
||||
val decision = requireNotNull(
|
||||
PlaybackDecisionResolver.resolve(
|
||||
mediaSources = listOf(source),
|
||||
playSessionId = "session-2",
|
||||
serverUrl = "https://example.com/jellyfin",
|
||||
) { error("direct play should not be used") }
|
||||
)
|
||||
|
||||
assertEquals(PlaybackMethod.DIRECT_STREAM, decision.reportContext.playMethod)
|
||||
assertEquals("https://example.com/jellyfin/Videos/1/master.m3u8", decision.url)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve uses transcoding when direct stream is unavailable`() {
|
||||
val source = mediaSource(
|
||||
id = "transcode",
|
||||
supportsTranscoding = true,
|
||||
transcodingUrl = "Videos/2/master.m3u8",
|
||||
)
|
||||
|
||||
val decision = requireNotNull(
|
||||
PlaybackDecisionResolver.resolve(
|
||||
mediaSources = listOf(source),
|
||||
playSessionId = "session-3",
|
||||
serverUrl = "https://example.com/jellyfin",
|
||||
) { error("direct play should not be used") }
|
||||
)
|
||||
|
||||
assertEquals(PlaybackMethod.TRANSCODE, decision.reportContext.playMethod)
|
||||
assertEquals("https://example.com/jellyfin/Videos/2/master.m3u8", decision.url)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve returns null when no usable source exists`() {
|
||||
val source = mediaSource(id = "unusable")
|
||||
|
||||
val decision = PlaybackDecisionResolver.resolve(
|
||||
mediaSources = listOf(source),
|
||||
playSessionId = "session-4",
|
||||
serverUrl = "https://example.com/jellyfin",
|
||||
) { error("direct play should not be used") }
|
||||
|
||||
assertNull(decision)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `absolute playback url preserves absolute urls`() {
|
||||
val url = PlaybackDecisionResolver.absolutePlaybackUrl(
|
||||
serverUrl = "https://example.com/jellyfin",
|
||||
playbackUrl = "https://cdn.example.com/master.m3u8",
|
||||
)
|
||||
|
||||
assertEquals("https://cdn.example.com/master.m3u8", url)
|
||||
}
|
||||
|
||||
private fun mediaSource(
|
||||
id: String,
|
||||
protocol: MediaProtocol = MediaProtocol.FILE,
|
||||
isRemote: Boolean = false,
|
||||
supportsDirectPlay: Boolean = false,
|
||||
supportsDirectStream: Boolean = false,
|
||||
supportsTranscoding: Boolean = false,
|
||||
transcodingUrl: String? = null,
|
||||
defaultAudioStreamIndex: Int? = null,
|
||||
defaultSubtitleStreamIndex: Int? = null,
|
||||
liveStreamId: String? = null,
|
||||
): MediaSourceInfo {
|
||||
return MediaSourceInfo(
|
||||
protocol = protocol,
|
||||
id = id,
|
||||
type = MediaSourceType.DEFAULT,
|
||||
container = "mkv",
|
||||
isRemote = isRemote,
|
||||
readAtNativeFramerate = true,
|
||||
ignoreDts = false,
|
||||
ignoreIndex = false,
|
||||
genPtsInput = false,
|
||||
supportsTranscoding = supportsTranscoding,
|
||||
supportsDirectStream = supportsDirectStream,
|
||||
supportsDirectPlay = supportsDirectPlay,
|
||||
isInfiniteStream = false,
|
||||
requiresOpening = false,
|
||||
requiresClosing = false,
|
||||
liveStreamId = liveStreamId,
|
||||
requiresLooping = false,
|
||||
supportsProbing = true,
|
||||
transcodingUrl = transcodingUrl,
|
||||
defaultAudioStreamIndex = defaultAudioStreamIndex,
|
||||
defaultSubtitleStreamIndex = defaultSubtitleStreamIndex,
|
||||
transcodingSubProtocol = MediaStreamProtocol.HLS,
|
||||
hasSegments = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user