Shared KMP core and UI, switch to Koin

This commit is contained in:
2026-10-25 20:28:07 +03:00
parent f63b09a9d3
commit 69048452b8
161 changed files with 6626 additions and 4485 deletions

View File

@@ -0,0 +1,21 @@
package ru.stenfredd.sleepyowl.playback.api
import android.widget.FrameLayout
/** Engines that show video as Android views (surface, subtitle overlay) inside the player's container. */
interface AndroidVideoOutput {
/** Adds the engine's own video views into [container]. */
fun attachVideo(container: FrameLayout)
fun detachVideo()
}
/** [VideoHost] of the TV player: each engine puts its views into [container]. */
class FrameLayoutVideoHost(private val container: FrameLayout) : VideoHost {
override fun show(engine: PlaybackEngine) {
(engine as? AndroidVideoOutput)?.attachVideo(container)
}
override fun hide(engine: PlaybackEngine) {
(engine as? AndroidVideoOutput)?.detachVideo()
}
}

View File

@@ -0,0 +1,30 @@
package ru.stenfredd.sleepyowl.playback.api
import android.media.AudioFormat
/** Encoding that can be passed through to the audio output, for compressed surround formats. */
fun Codecs.passthroughEncoding(codec: String?, profile: String?): Int? = when (codec?.lowercase()) {
"ac3" -> AudioFormat.ENCODING_AC3
"eac3" -> if (profile?.contains("Atmos", true) == true) AudioFormat.ENCODING_E_AC3_JOC else AudioFormat.ENCODING_E_AC3
"truehd" -> AudioFormat.ENCODING_DOLBY_TRUEHD
"dts" -> if (profile?.contains("HD", true) == true || profile?.contains("X", false) == true) {
AudioFormat.ENCODING_DTS_HD
} else {
AudioFormat.ENCODING_DTS
}
else -> null
}
fun Codecs.encodingName(encoding: Int): String = when (encoding) {
AudioFormat.ENCODING_AC3 -> "AC3"
AudioFormat.ENCODING_E_AC3 -> "E-AC3"
AudioFormat.ENCODING_E_AC3_JOC -> "E-AC3 JOC"
AudioFormat.ENCODING_DOLBY_TRUEHD -> "TrueHD"
AudioFormat.ENCODING_DTS -> "DTS"
AudioFormat.ENCODING_DTS_HD -> "DTS-HD"
AudioFormat.ENCODING_AC4 -> "AC4"
AudioFormat.ENCODING_DOLBY_MAT -> "Dolby MAT"
AudioFormat.ENCODING_PCM_16BIT -> "PCM 16"
AudioFormat.ENCODING_PCM_FLOAT -> "PCM float"
else -> "enc#$encoding"
}

View File

@@ -0,0 +1,33 @@
package ru.stenfredd.sleepyowl.playback.api
/** Mapping between Jellyfin (ffprobe) codec names and MIME types; Android audio encodings are in `Codecs.android.kt`. */
object Codecs {
fun videoMime(codec: String?): String? = when (codec?.lowercase()) {
"h264", "avc" -> "video/avc"
"hevc", "h265" -> "video/hevc"
"vp8" -> "video/x-vnd.on2.vp8"
"vp9" -> "video/x-vnd.on2.vp9"
"av1" -> "video/av01"
"mpeg2video", "mpeg1video" -> "video/mpeg2"
"mpeg4" -> "video/mp4v-es"
"h263" -> "video/3gpp"
"vc1" -> "video/wvc1"
else -> null
}
fun audioMime(codec: String?): String? = when (codec?.lowercase()) {
"aac" -> "audio/mp4a-latm"
"mp3" -> "audio/mpeg"
"mp2" -> "audio/mpeg-L2"
"ac3" -> "audio/ac3"
"eac3" -> "audio/eac3"
"truehd" -> "audio/true-hd"
"dts" -> "audio/vnd.dts"
"flac" -> "audio/flac"
"opus" -> "audio/opus"
"vorbis" -> "audio/vorbis"
"alac" -> "audio/alac"
"pcm_s16le", "pcm_s24le", "pcm_s16be", "pcm_s24be" -> "audio/raw"
else -> null
}
}

View File

@@ -4,8 +4,8 @@ import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.api.MediaSourceInfo
/**
* Plugin entry point of an engine module. Bound into a Hilt `Set<EngineProvider>` with `@IntoSet`,
* so adding an engine means adding a module, nothing else changes.
* Plugin entry point of an engine module. Each engine module binds its provider in its Koin module
* (`bind EngineProvider::class`), so adding an engine means adding a module, nothing else changes.
*/
interface EngineProvider {
val id: EngineId

View File

@@ -1,6 +1,5 @@
package ru.stenfredd.sleepyowl.playback.api
import android.widget.FrameLayout
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -25,10 +24,6 @@ interface PlaybackEngine {
/** How far the media is loaded ahead (the timeline's buffer bar), or -1 if the engine doesn't tell. */
val bufferedPositionMs: Long get() = -1
/** Adds the engine's own video views (surface, subtitle overlay) into [container]. */
fun attachVideo(container: FrameLayout)
fun detachVideo()
fun load(request: LoadRequest)
fun play()
fun pause()
@@ -41,6 +36,15 @@ interface PlaybackEngine {
fun release()
}
/**
* The player screen's place for video. A session hands it every engine it starts, before the engine loads; how the
* picture gets there depends on the platform (on Android the engines put their views into a FrameLayout).
*/
interface VideoHost {
fun show(engine: PlaybackEngine)
fun hide(engine: PlaybackEngine)
}
/** Optional capabilities: checked with `engine as? X`, the UI hides what an engine can't do. */
interface AudioDelayCapable {
fun setAudioDelay(delayMs: Long)

View File

@@ -1,62 +0,0 @@
package ru.stenfredd.sleepyowl.playback.api
import android.media.AudioFormat
/** Mapping between Jellyfin (ffprobe) codec names and Android MIME types / encodings. */
object Codecs {
fun videoMime(codec: String?): String? = when (codec?.lowercase()) {
"h264", "avc" -> "video/avc"
"hevc", "h265" -> "video/hevc"
"vp8" -> "video/x-vnd.on2.vp8"
"vp9" -> "video/x-vnd.on2.vp9"
"av1" -> "video/av01"
"mpeg2video", "mpeg1video" -> "video/mpeg2"
"mpeg4" -> "video/mp4v-es"
"h263" -> "video/3gpp"
"vc1" -> "video/wvc1"
else -> null
}
fun audioMime(codec: String?): String? = when (codec?.lowercase()) {
"aac" -> "audio/mp4a-latm"
"mp3" -> "audio/mpeg"
"mp2" -> "audio/mpeg-L2"
"ac3" -> "audio/ac3"
"eac3" -> "audio/eac3"
"truehd" -> "audio/true-hd"
"dts" -> "audio/vnd.dts"
"flac" -> "audio/flac"
"opus" -> "audio/opus"
"vorbis" -> "audio/vorbis"
"alac" -> "audio/alac"
"pcm_s16le", "pcm_s24le", "pcm_s16be", "pcm_s24be" -> "audio/raw"
else -> null
}
/** Encoding that can be passed through to the audio output, for compressed surround formats. */
fun passthroughEncoding(codec: String?, profile: String?): Int? = when (codec?.lowercase()) {
"ac3" -> AudioFormat.ENCODING_AC3
"eac3" -> if (profile?.contains("Atmos", true) == true) AudioFormat.ENCODING_E_AC3_JOC else AudioFormat.ENCODING_E_AC3
"truehd" -> AudioFormat.ENCODING_DOLBY_TRUEHD
"dts" -> if (profile?.contains("HD", true) == true || profile?.contains("X", false) == true) {
AudioFormat.ENCODING_DTS_HD
} else {
AudioFormat.ENCODING_DTS
}
else -> null
}
fun encodingName(encoding: Int): String = when (encoding) {
AudioFormat.ENCODING_AC3 -> "AC3"
AudioFormat.ENCODING_E_AC3 -> "E-AC3"
AudioFormat.ENCODING_E_AC3_JOC -> "E-AC3 JOC"
AudioFormat.ENCODING_DOLBY_TRUEHD -> "TrueHD"
AudioFormat.ENCODING_DTS -> "DTS"
AudioFormat.ENCODING_DTS_HD -> "DTS-HD"
AudioFormat.ENCODING_AC4 -> "AC4"
AudioFormat.ENCODING_DOLBY_MAT -> "Dolby MAT"
AudioFormat.ENCODING_PCM_16BIT -> "PCM 16"
AudioFormat.ENCODING_PCM_FLOAT -> "PCM float"
else -> "enc#$encoding"
}
}