Shared KMP core and UI, switch to Koin
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.hilt)
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -16,12 +14,11 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":playback:api"))
|
||||
implementation(project(":core:common"))
|
||||
implementation(libs.media3.exoplayer)
|
||||
implementation(libs.media3.exoplayer.hls)
|
||||
implementation(libs.media3.ui)
|
||||
implementation(libs.ass.media)
|
||||
implementation(libs.jellyfin.ffmpeg)
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.compiler)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.koin.android)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.media3.extractor.ExtractorOutput
|
||||
import androidx.media3.extractor.PositionHolder
|
||||
import androidx.media3.extractor.SeekMap
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
import timber.log.Timber
|
||||
import ru.stenfredd.sleepyowl.core.common.Log
|
||||
import java.io.EOFException
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
@@ -113,7 +113,7 @@ internal class AviAudioFramingExtractor(private val delegate: Extractor) : Extra
|
||||
while (pendingLength - i >= HEADER_BYTES) {
|
||||
val frameSize = frameSize(codec, i)
|
||||
if (frameSize <= 0) {
|
||||
if (resyncs++ % 1000 == 0L) Timber.w("AVI %s: no sync word, skipping a byte (%d)", codec, resyncs)
|
||||
if (resyncs++ % 1000 == 0L) Log.w("AVI %s: no sync word, skipping a byte (%d)", codec, resyncs)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.api.PlayMethod
|
||||
import org.jellyfin.sdk.model.api.VideoRange
|
||||
import org.jellyfin.sdk.model.api.VideoRangeType
|
||||
import ru.stenfredd.sleepyowl.core.common.Log
|
||||
import ru.stenfredd.sleepyowl.playback.api.AndroidVideoOutput
|
||||
import ru.stenfredd.sleepyowl.playback.api.Codecs
|
||||
import ru.stenfredd.sleepyowl.playback.api.DolbyVisionSwitchable
|
||||
import ru.stenfredd.sleepyowl.playback.api.EngineError
|
||||
@@ -71,10 +73,10 @@ import ru.stenfredd.sleepyowl.playback.api.SubtitleStyle
|
||||
import ru.stenfredd.sleepyowl.playback.api.SubtitleStyleable
|
||||
import ru.stenfredd.sleepyowl.playback.api.VideoPathSwitchable
|
||||
import ru.stenfredd.sleepyowl.playback.api.embeddedOrdinal
|
||||
import ru.stenfredd.sleepyowl.playback.api.encodingName
|
||||
import ru.stenfredd.sleepyowl.playback.api.videoStream
|
||||
import timber.log.Timber
|
||||
|
||||
class ExoEngine(context: Context) : PlaybackEngine, VideoPathSwitchable, DolbyVisionSwitchable, SubtitleStyleable {
|
||||
class ExoEngine(context: Context) : PlaybackEngine, AndroidVideoOutput, VideoPathSwitchable, DolbyVisionSwitchable, SubtitleStyleable {
|
||||
override val id = ExoEngineProvider.ID
|
||||
|
||||
/** See [gpuPathFor]: only TVs known to over-process SDR on the video plane. */
|
||||
@@ -202,19 +204,19 @@ class ExoEngine(context: Context) : PlaybackEngine, VideoPathSwitchable, DolbyVi
|
||||
// Not visible as dropped frames: AC3-in-AVI / MPEG-PS style timing problems show up as these.
|
||||
override fun onAudioUnderrun(eventTime: AnalyticsListener.EventTime, bufferSize: Int, bufferSizeMs: Long, elapsedSinceLastFeedMs: Long) {
|
||||
audioUnderruns++
|
||||
Timber.w("Exo audio underrun #%d (%d ms since last feed)", audioUnderruns, elapsedSinceLastFeedMs)
|
||||
Log.w("Exo audio underrun #%d (%d ms since last feed)", audioUnderruns, elapsedSinceLastFeedMs)
|
||||
_stats.update { it.copy(extra = it.extra + ("срывы звука" to audioUnderruns.toString())) }
|
||||
}
|
||||
override fun onBandwidthEstimate(eventTime: AnalyticsListener.EventTime, totalLoadTimeMs: Int, totalBytesLoaded: Long, bitrateEstimate: Long) {
|
||||
Timber.d("Exo bandwidth: %.1f Mbit/s (%d KB in %d ms)", bitrateEstimate / 1e6, totalBytesLoaded / 1024, totalLoadTimeMs)
|
||||
Log.d("Exo bandwidth: %.1f Mbit/s (%d KB in %d ms)", bitrateEstimate / 1e6, totalBytesLoaded / 1024, totalLoadTimeMs)
|
||||
}
|
||||
override fun onAudioSinkError(eventTime: AnalyticsListener.EventTime, audioSinkError: Exception) {
|
||||
Timber.w(audioSinkError, "Exo audio sink error")
|
||||
Log.w(audioSinkError, "Exo audio sink error")
|
||||
}
|
||||
override fun onPlaybackStateChanged(eventTime: AnalyticsListener.EventTime, state: Int) {
|
||||
if (state != Player.STATE_BUFFERING || eventTime.realtimeMs - startedAtMs < 3000) return
|
||||
rebuffers++
|
||||
Timber.w("Exo rebuffering #%d at %d ms", rebuffers, eventTime.currentPlaybackPositionMs)
|
||||
Log.w("Exo rebuffering #%d at %d ms", rebuffers, eventTime.currentPlaybackPositionMs)
|
||||
_stats.update { it.copy(extra = it.extra + ("буферизаций" to rebuffers.toString())) }
|
||||
}
|
||||
override fun onAudioTrackInitialized(eventTime: AnalyticsListener.EventTime, audioTrackConfig: AudioSink.AudioTrackConfig) {
|
||||
@@ -270,7 +272,7 @@ class ExoEngine(context: Context) : PlaybackEngine, VideoPathSwitchable, DolbyVi
|
||||
if (lastRendered >= 0) {
|
||||
val fps = (rendered - lastRendered) * 1000f / FRAME_RATE_SAMPLE_MS
|
||||
val skipped = counters.skippedOutputBufferCount + counters.skippedInputBufferCount
|
||||
Timber.d("Exo video: %.1f fps shown, skipped %d, dropped %d", fps, skipped - lastSkipped, counters.droppedBufferCount)
|
||||
Log.d("Exo video: %.1f fps shown, skipped %d, dropped %d", fps, skipped - lastSkipped, counters.droppedBufferCount)
|
||||
_stats.update { it.copy(extra = it.extra + ("кадров/с на экране" to "%.1f".format(fps)) + ("пропущено декодером" to skipped.toString())) }
|
||||
lastSkipped = skipped
|
||||
}
|
||||
@@ -325,7 +327,7 @@ class ExoEngine(context: Context) : PlaybackEngine, VideoPathSwitchable, DolbyVi
|
||||
if (gpu) player.setVideoTextureView(textureView) else player.setVideoSurfaceView(surfaceView)
|
||||
val label = (if (gpu) "через GPU (TextureView)" else "видеослой ТВ (SurfaceView)") +
|
||||
if (pathMode == VideoPathSwitchable.Mode.AUTO) ", авто" else ", вручную"
|
||||
Timber.i("Exo video output: %s", label)
|
||||
Log.i("Exo video output: %s", label)
|
||||
_stats.update { it.copy(extra = it.extra + ("вывод видео" to label)) }
|
||||
}
|
||||
|
||||
@@ -425,7 +427,7 @@ class ExoEngine(context: Context) : PlaybackEngine, VideoPathSwitchable, DolbyVi
|
||||
textGroups.filterNot { it.getTrackFormat(0).id?.contains(EXTERNAL_PREFIX) == true }.getOrNull(ordinal)
|
||||
}
|
||||
if (group != null) builder.setOverrideForType(TrackSelectionOverride(group.mediaTrackGroup, 0))
|
||||
Timber.i(
|
||||
Log.i(
|
||||
"Exo subtitle %d -> %s (text tracks: %s)",
|
||||
streamIndex,
|
||||
group?.getTrackFormat(0)?.let { "${it.id} ${it.sampleMimeType}" } ?: "не найдена",
|
||||
@@ -486,7 +488,7 @@ class ExoEngine(context: Context) : PlaybackEngine, VideoPathSwitchable, DolbyVi
|
||||
private fun reportError(error: EngineError) {
|
||||
if (errorReported) return
|
||||
errorReported = true
|
||||
Timber.w(error.cause, "Exo error: %s", error.message)
|
||||
Log.w(error.cause, "Exo error: %s", error.message)
|
||||
_events.tryEmit(EngineEvent.Error(error))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
package ru.stenfredd.sleepyowl.playback.exo
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import ru.stenfredd.sleepyowl.playback.api.EngineProvider
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class ExoEngineModule {
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindProvider(provider: ExoEngineProvider): EngineProvider
|
||||
val exoEngineModule = module {
|
||||
single { ExoEngineProvider(androidContext()) } bind EngineProvider::class
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.media.MediaCodecInfo.CodecProfileLevel
|
||||
import android.view.Display.HdrCapabilities
|
||||
import androidx.media3.decoder.ffmpeg.FfmpegLibrary
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.jellyfin.sdk.model.api.CodecType
|
||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||
import org.jellyfin.sdk.model.api.DlnaProfileType
|
||||
@@ -23,12 +22,12 @@ import ru.stenfredd.sleepyowl.playback.api.Loss
|
||||
import ru.stenfredd.sleepyowl.playback.api.PlaybackEngine
|
||||
import ru.stenfredd.sleepyowl.playback.api.StreamSelection
|
||||
import ru.stenfredd.sleepyowl.playback.api.Verdict
|
||||
import ru.stenfredd.sleepyowl.playback.api.passthroughEncoding
|
||||
import ru.stenfredd.sleepyowl.playback.api.stream
|
||||
import ru.stenfredd.sleepyowl.playback.api.videoStream
|
||||
import javax.inject.Inject
|
||||
|
||||
class ExoEngineProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
class ExoEngineProvider(
|
||||
private val context: Context,
|
||||
) : EngineProvider {
|
||||
override val id = ID
|
||||
override val displayName = "ExoPlayer"
|
||||
|
||||
@@ -12,7 +12,7 @@ import androidx.media3.extractor.ExtractorOutput
|
||||
import androidx.media3.extractor.PositionHolder
|
||||
import androidx.media3.extractor.SeekMap
|
||||
import androidx.media3.extractor.TrackOutput
|
||||
import timber.log.Timber
|
||||
import ru.stenfredd.sleepyowl.core.common.Log
|
||||
import java.io.EOFException
|
||||
import kotlin.math.abs
|
||||
|
||||
@@ -126,7 +126,7 @@ internal class Mpeg2TimestampFixExtractor(private val delegate: Extractor) : Ext
|
||||
val expectedUs = anchorTimeUs + (reference - anchorReference) * frameDurationUs
|
||||
if (extrapolated || abs(timeUs - expectedUs) > MAX_PTS_DEVIATION_US) fixedTimeUs = expectedUs
|
||||
if (fixedTimeUs != timeUs && fixedCount++ == 0L) {
|
||||
Timber.i("MPEG-2: rebuilding broken timestamps from temporal_reference")
|
||||
Log.i("MPEG-2: rebuilding broken timestamps from temporal_reference")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import androidx.media3.extractor.avi.AviExtractor
|
||||
import androidx.media3.extractor.mkv.MatroskaExtractor
|
||||
import androidx.media3.extractor.text.SubtitleParser
|
||||
import androidx.media3.extractor.ts.PsExtractor
|
||||
import ru.stenfredd.sleepyowl.core.common.Log
|
||||
import ru.stenfredd.sleepyowl.playback.exo.ps.DvdPsExtractor
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* [DefaultExtractorsFactory] with fixes: [MatroskaExtractor] wrapped in [SeekHeadChainExtractor], [PsExtractor]
|
||||
@@ -93,7 +93,7 @@ internal class SeekHeadChainExtractor(private val delegate: Extractor) : Extract
|
||||
Phase.PROBE -> {
|
||||
phase = Phase.PASS
|
||||
if (input.position == 0L) {
|
||||
val target = runCatching { probe(input) }.onFailure { Timber.w(it, "MKV SeekHead probe failed") }.getOrNull()
|
||||
val target = runCatching { probe(input) }.onFailure { Log.w(it, "MKV SeekHead probe failed") }.getOrNull()
|
||||
input.resetPeekPosition()
|
||||
if (target != null) {
|
||||
phase = Phase.READ_TAIL
|
||||
@@ -104,7 +104,7 @@ internal class SeekHeadChainExtractor(private val delegate: Extractor) : Extract
|
||||
}
|
||||
Phase.READ_TAIL -> {
|
||||
phase = Phase.PASS
|
||||
runCatching { readTail(input) }.onFailure { Timber.w(it, "MKV tail SeekHead read failed") }
|
||||
runCatching { readTail(input) }.onFailure { Log.w(it, "MKV tail SeekHead read failed") }
|
||||
seekPosition.position = 0
|
||||
return Extractor.RESULT_SEEK
|
||||
}
|
||||
@@ -151,7 +151,7 @@ internal class SeekHeadChainExtractor(private val delegate: Extractor) : Extract
|
||||
Patch(link.idOffset, byteArrayOf(0x1C, 0x53, 0xBB.toByte(), 0x6B)),
|
||||
Patch(link.positionOffset, ByteArray(size) { i -> (cues.position ushr (8 * (size - 1 - i))).toByte() }),
|
||||
)
|
||||
Timber.i("MKV SeekHead chain: Cues at %d via second SeekHead, first SeekHead patched", segmentStart + cues.position)
|
||||
Log.i("MKV SeekHead chain: Cues at %d via second SeekHead, first SeekHead patched", segmentStart + cues.position)
|
||||
}
|
||||
|
||||
private fun parseSeekHead(r: EbmlReader, size: Long): List<SeekEntry> {
|
||||
|
||||
Reference in New Issue
Block a user