mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
Stabilize HLS playback timestamps
This commit is contained in:
@@ -68,6 +68,8 @@ object AndroidDeviceProfile {
|
||||
videoCodecs: List<String>,
|
||||
maxAudioChannels: Int
|
||||
): DeviceProfile {
|
||||
val hlsOptions = HlsPlaybackStability.conservativeHlsOptions
|
||||
|
||||
return DeviceProfile(
|
||||
name = "Android Media3",
|
||||
maxStaticBitrate = 100_000_000,
|
||||
@@ -94,8 +96,10 @@ object AndroidDeviceProfile {
|
||||
protocol = MediaStreamProtocol.HLS,
|
||||
context = EncodingContext.STREAMING,
|
||||
maxAudioChannels = maxAudioChannels.toString(),
|
||||
minSegments = 2,
|
||||
breakOnNonKeyFrames = true,
|
||||
minSegments = hlsOptions.minSegments,
|
||||
segmentLength = hlsOptions.segmentLengthSeconds,
|
||||
breakOnNonKeyFrames = hlsOptions.breakOnNonKeyFrames,
|
||||
copyTimestamps = hlsOptions.copyTimestamps,
|
||||
conditions = emptyList()
|
||||
)
|
||||
),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package hu.bbara.purefin.core.data.client
|
||||
|
||||
internal data class HlsStabilityOptions(
|
||||
val segmentLengthSeconds: Int,
|
||||
val minSegments: Int,
|
||||
val breakOnNonKeyFrames: Boolean,
|
||||
val copyTimestamps: Boolean
|
||||
)
|
||||
|
||||
internal object HlsPlaybackStability {
|
||||
val conservativeHlsOptions = HlsStabilityOptions(
|
||||
segmentLengthSeconds = 6,
|
||||
minSegments = 3,
|
||||
breakOnNonKeyFrames = false,
|
||||
copyTimestamps = true
|
||||
)
|
||||
}
|
||||
@@ -50,6 +50,14 @@ class JellyfinApiClient @Inject constructor(
|
||||
companion object {
|
||||
private const val TAG = "JellyfinApiClient"
|
||||
private const val MAX_STREAMING_BITRATE = 100_000_000
|
||||
|
||||
internal fun streamingHlsOptionsFor(
|
||||
urlStrategy: PlaybackSourcePlanner.UrlStrategy
|
||||
): HlsStabilityOptions? = when (urlStrategy) {
|
||||
PlaybackSourcePlanner.UrlStrategy.DIRECT_PLAY -> null
|
||||
PlaybackSourcePlanner.UrlStrategy.DIRECT_STREAM,
|
||||
PlaybackSourcePlanner.UrlStrategy.TRANSCODE -> HlsPlaybackStability.conservativeHlsOptions
|
||||
}
|
||||
}
|
||||
|
||||
private val jellyfin = createJellyfin {
|
||||
@@ -405,6 +413,11 @@ class JellyfinApiClient @Inject constructor(
|
||||
}
|
||||
|
||||
val capabilitySnapshot = AndroidDeviceProfile.getSnapshot(applicationContext)
|
||||
Log.d(
|
||||
TAG,
|
||||
"Playback request for $mediaId (forceTranscode=$forceTranscode) maxAudioChannels=${capabilitySnapshot.maxAudioChannels}, " +
|
||||
"hlsOptions=${HlsPlaybackStability.conservativeHlsOptions}"
|
||||
)
|
||||
val response = api.mediaInfoApi.getPostedPlaybackInfo(
|
||||
mediaId,
|
||||
PlaybackInfoDto(
|
||||
@@ -467,6 +480,14 @@ class JellyfinApiClient @Inject constructor(
|
||||
playSessionId: String?,
|
||||
maxAudioChannels: Int
|
||||
): String {
|
||||
val hlsOptions = streamingHlsOptionsFor(PlaybackSourcePlanner.UrlStrategy.DIRECT_STREAM)
|
||||
?: error("Direct stream requests must use HLS stability options.")
|
||||
|
||||
Log.d(
|
||||
TAG,
|
||||
"Stream URL request for $mediaId strategy=DIRECT_STREAM container=${mediaSource.transcodingContainer ?: mediaSource.container}, " +
|
||||
"hlsOptions=$hlsOptions"
|
||||
)
|
||||
return api.videosApi.getVideoStreamUrl(
|
||||
itemId = mediaId,
|
||||
static = false,
|
||||
@@ -474,9 +495,13 @@ class JellyfinApiClient @Inject constructor(
|
||||
playSessionId = playSessionId,
|
||||
liveStreamId = mediaSource.liveStreamId,
|
||||
container = mediaSource.transcodingContainer ?: mediaSource.container,
|
||||
segmentLength = hlsOptions.segmentLengthSeconds,
|
||||
minSegments = hlsOptions.minSegments,
|
||||
enableAutoStreamCopy = true,
|
||||
allowVideoStreamCopy = true,
|
||||
allowAudioStreamCopy = true,
|
||||
breakOnNonKeyFrames = hlsOptions.breakOnNonKeyFrames,
|
||||
copyTimestamps = hlsOptions.copyTimestamps,
|
||||
maxAudioChannels = maxAudioChannels
|
||||
)
|
||||
}
|
||||
@@ -487,7 +512,15 @@ class JellyfinApiClient @Inject constructor(
|
||||
playSessionId: String?,
|
||||
maxAudioChannels: Int
|
||||
): String? {
|
||||
val hlsOptions = streamingHlsOptionsFor(PlaybackSourcePlanner.UrlStrategy.TRANSCODE)
|
||||
?: error("Transcode requests must use HLS stability options.")
|
||||
|
||||
mediaSource.transcodingUrl?.takeIf { it.isNotBlank() }?.let { transcodingUrl ->
|
||||
Log.d(
|
||||
TAG,
|
||||
"Using server transcodingUrl for $mediaId strategy=TRANSCODE container=${mediaSource.transcodingContainer ?: mediaSource.container}, " +
|
||||
"hlsOptions=$hlsOptions"
|
||||
)
|
||||
if (transcodingUrl.startsWith("http", ignoreCase = true)) {
|
||||
return transcodingUrl
|
||||
}
|
||||
@@ -495,6 +528,11 @@ class JellyfinApiClient @Inject constructor(
|
||||
return "$baseUrl$transcodingUrl"
|
||||
}
|
||||
|
||||
Log.d(
|
||||
TAG,
|
||||
"Stream URL request for $mediaId strategy=TRANSCODE container=${mediaSource.transcodingContainer ?: mediaSource.container ?: "mp4"}, " +
|
||||
"hlsOptions=$hlsOptions"
|
||||
)
|
||||
return api.videosApi.getVideoStreamUrl(
|
||||
itemId = mediaId,
|
||||
static = false,
|
||||
@@ -502,9 +540,13 @@ class JellyfinApiClient @Inject constructor(
|
||||
playSessionId = playSessionId,
|
||||
liveStreamId = mediaSource.liveStreamId,
|
||||
container = mediaSource.transcodingContainer ?: mediaSource.container ?: "mp4",
|
||||
segmentLength = hlsOptions.segmentLengthSeconds,
|
||||
minSegments = hlsOptions.minSegments,
|
||||
enableAutoStreamCopy = false,
|
||||
allowVideoStreamCopy = false,
|
||||
allowAudioStreamCopy = false,
|
||||
breakOnNonKeyFrames = hlsOptions.breakOnNonKeyFrames,
|
||||
copyTimestamps = hlsOptions.copyTimestamps,
|
||||
maxAudioChannels = maxAudioChannels
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.jellyfin.sdk.model.api.DlnaProfileType
|
||||
import org.jellyfin.sdk.model.api.EncodingContext
|
||||
import org.jellyfin.sdk.model.api.MediaStreamProtocol
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
@@ -25,7 +26,9 @@ class AndroidDeviceProfileTest {
|
||||
assertEquals(MediaStreamProtocol.HLS, transcodingProfile.protocol)
|
||||
assertEquals(EncodingContext.STREAMING, transcodingProfile.context)
|
||||
assertEquals("6", transcodingProfile.maxAudioChannels)
|
||||
assertEquals(2, transcodingProfile.minSegments)
|
||||
assertTrue(transcodingProfile.breakOnNonKeyFrames)
|
||||
assertEquals(3, transcodingProfile.minSegments)
|
||||
assertEquals(6, transcodingProfile.segmentLength)
|
||||
assertFalse(transcodingProfile.breakOnNonKeyFrames)
|
||||
assertTrue(transcodingProfile.copyTimestamps)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package hu.bbara.purefin.core.data.client
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class JellyfinApiClientTest {
|
||||
@Test
|
||||
fun `direct play requests do not apply HLS stability options`() {
|
||||
val options = JellyfinApiClient.streamingHlsOptionsFor(PlaybackSourcePlanner.UrlStrategy.DIRECT_PLAY)
|
||||
|
||||
assertNull(options)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `direct stream requests use conservative HLS stability options`() {
|
||||
val options = JellyfinApiClient.streamingHlsOptionsFor(PlaybackSourcePlanner.UrlStrategy.DIRECT_STREAM)
|
||||
|
||||
assertEquals(HlsPlaybackStability.conservativeHlsOptions, options)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transcode requests use the same HLS stability options as direct stream`() {
|
||||
val directStreamOptions = JellyfinApiClient.streamingHlsOptionsFor(PlaybackSourcePlanner.UrlStrategy.DIRECT_STREAM)
|
||||
val transcodeOptions = JellyfinApiClient.streamingHlsOptionsFor(PlaybackSourcePlanner.UrlStrategy.TRANSCODE)
|
||||
|
||||
assertEquals(directStreamOptions, transcodeOptions)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user