From 9eb81c4e17b643172bb74e27f126c57b9474cbda Mon Sep 17 00:00:00 2001 From: ModerRAS Date: Tue, 11 Aug 2026 08:15:12 +0800 Subject: [PATCH] fix(player): restore embedded mpv playback --- .../tv/LaunchTestSourceContentModeTest.kt | 6 +- .../tv/model/PlaybackUiConventions.kt | 2 +- .../miruplay/tv/model/ToneMappingModels.kt | 4 +- .../PlaybackPreferencesManagerTest.kt | 4 +- .../miruplay/tv/player/AudioDspMpvOptions.kt | 2 +- .../tv/player/DeviceGlCapabilities.kt | 19 + .../kotlin/com/miruplay/tv/player/DiModule.kt | 37 -- .../tv/player/EmbeddedMpvSessionOptions.kt | 9 +- .../tv/player/ExoPlaybackController.kt | 168 +----- .../ExperimentalHdrSdrVideoGraphFactory.kt | 12 - ...mentalHdrSurfaceMediaCodecVideoRenderer.kt | 151 ----- .../tv/player/ExperimentalRenderersFactory.kt | 100 --- .../player/ExperimentalVideoPipelineMode.kt | 50 -- .../tv/player/PlaybackPlayerQualifiers.kt | 4 - .../miruplay/tv/player/PlayerFactoryImpl.kt | 3 - .../tv/player/AudioDspMpvOptionsTest.kt | 2 + .../player/EmbeddedMpvSessionOptionsTest.kt | 25 + .../ExoPlaybackControllerLazyInitTest.kt | 77 +-- ...ExperimentalHdrSdrVideoGraphFactoryTest.kt | 40 -- ...alHdrSurfaceMediaCodecVideoRendererTest.kt | 110 ---- .../ExperimentalVideoPipelineModeTest.kt | 76 --- .../src/main/kotlin/is/xyz/mpv/BaseMPVView.kt | 6 +- .../kotlin/is/xyz/mpv/MiruMpvSurfaceView.kt | 3 +- .../tv/ui/player/GLVideoSurfaceView.kt | 570 ------------------ .../com/miruplay/tv/ui/player/PlayerScreen.kt | 151 +---- .../tv/ui/settings/AddSourceScreen.kt | 2 +- .../GLVideoSurfaceViewAttachmentPolicyTest.kt | 31 - .../webcontrol/PlaybackDebugConfigMapping.kt | 1 - .../PlaybackDebugConfigMappingTest.kt | 2 +- 29 files changed, 96 insertions(+), 1571 deletions(-) create mode 100644 player-core/src/main/kotlin/com/miruplay/tv/player/DeviceGlCapabilities.kt delete mode 100644 player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalHdrSdrVideoGraphFactory.kt delete mode 100644 player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalHdrSurfaceMediaCodecVideoRenderer.kt delete mode 100644 player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalRenderersFactory.kt delete mode 100644 player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalVideoPipelineMode.kt delete mode 100644 player-core/src/test/kotlin/com/miruplay/tv/player/ExperimentalHdrSdrVideoGraphFactoryTest.kt delete mode 100644 player-core/src/test/kotlin/com/miruplay/tv/player/ExperimentalHdrSurfaceMediaCodecVideoRendererTest.kt delete mode 100644 player-core/src/test/kotlin/com/miruplay/tv/player/ExperimentalVideoPipelineModeTest.kt delete mode 100644 ui-tv/src/main/kotlin/com/miruplay/tv/ui/player/GLVideoSurfaceView.kt delete mode 100644 ui-tv/src/test/kotlin/com/miruplay/tv/ui/player/GLVideoSurfaceViewAttachmentPolicyTest.kt diff --git a/app/src/test/kotlin/com/miruplay/tv/LaunchTestSourceContentModeTest.kt b/app/src/test/kotlin/com/miruplay/tv/LaunchTestSourceContentModeTest.kt index e90333f7..0b40eb50 100644 --- a/app/src/test/kotlin/com/miruplay/tv/LaunchTestSourceContentModeTest.kt +++ b/app/src/test/kotlin/com/miruplay/tv/LaunchTestSourceContentModeTest.kt @@ -367,7 +367,7 @@ class LaunchTestSourceContentModeTest { rawPreset = "punchy", ) - assertEquals(PlaybackRenderBackend.EXPERIMENTAL_GL, overrides.backend) + assertEquals(PlaybackRenderBackend.STANDARD_EXO, overrides.backend) assertEquals(VideoRenderRuleKey.HDR10_PLUS, overrides.ruleKey) assertEquals(ToneMappingProfilePreset.PUNCHY, overrides.preset) } @@ -461,7 +461,7 @@ class LaunchTestSourceContentModeTest { hasBaseUrlExtra = false, ), playbackOverrides = LaunchPlaybackOverrides( - backend = PlaybackRenderBackend.EXPERIMENTAL_GL, + backend = PlaybackRenderBackend.STANDARD_EXO, ruleKey = null, preset = null, ), @@ -513,7 +513,7 @@ class LaunchTestSourceContentModeTest { hasBaseUrlExtra = false, ), playbackOverrides = LaunchPlaybackOverrides( - backend = PlaybackRenderBackend.EXPERIMENTAL_GL, + backend = PlaybackRenderBackend.STANDARD_EXO, ruleKey = null, preset = null, ), diff --git a/core/model/src/main/kotlin/com/miruplay/tv/model/PlaybackUiConventions.kt b/core/model/src/main/kotlin/com/miruplay/tv/model/PlaybackUiConventions.kt index 6e5eccde..c498101a 100644 --- a/core/model/src/main/kotlin/com/miruplay/tv/model/PlaybackUiConventions.kt +++ b/core/model/src/main/kotlin/com/miruplay/tv/model/PlaybackUiConventions.kt @@ -137,7 +137,7 @@ fun pictureSessionOverrideLabel(): String = fun playbackBackendLabel(backend: PlaybackRenderBackend): String = when (backend.normalizeSupportedBackend()) { PlaybackRenderBackend.STANDARD_EXO -> "标准 Exo" - PlaybackRenderBackend.EXPERIMENTAL_GL -> "旧实验 GL" + PlaybackRenderBackend.EXPERIMENTAL_GL -> "标准 Exo" PlaybackRenderBackend.EXPERIMENTAL_MPV_ANDROID -> "实验 mpv 内嵌" PlaybackRenderBackend.EXPERIMENTAL_MPV_EMBEDDED -> "实验 mpv 内嵌" PlaybackRenderBackend.EXPERIMENTAL_IJKPLAYER -> "实验 ijkplayer" diff --git a/core/model/src/main/kotlin/com/miruplay/tv/model/ToneMappingModels.kt b/core/model/src/main/kotlin/com/miruplay/tv/model/ToneMappingModels.kt index 3f38c025..717940ae 100644 --- a/core/model/src/main/kotlin/com/miruplay/tv/model/ToneMappingModels.kt +++ b/core/model/src/main/kotlin/com/miruplay/tv/model/ToneMappingModels.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable @Serializable enum class PlaybackRenderBackend { STANDARD_EXO, + // Serialized migration token only; normalizeSupportedBackend maps it to STANDARD_EXO. EXPERIMENTAL_GL, EXPERIMENTAL_MPV_ANDROID, EXPERIMENTAL_MPV_EMBEDDED, @@ -15,7 +16,7 @@ enum class PlaybackRenderBackend { fun PlaybackRenderBackend.normalizeSupportedBackend(): PlaybackRenderBackend = when (this) { PlaybackRenderBackend.STANDARD_EXO -> PlaybackRenderBackend.STANDARD_EXO - PlaybackRenderBackend.EXPERIMENTAL_GL -> PlaybackRenderBackend.EXPERIMENTAL_GL + PlaybackRenderBackend.EXPERIMENTAL_GL -> PlaybackRenderBackend.STANDARD_EXO PlaybackRenderBackend.EXPERIMENTAL_MPV_ANDROID -> PlaybackRenderBackend.EXPERIMENTAL_MPV_EMBEDDED PlaybackRenderBackend.EXPERIMENTAL_MPV_EMBEDDED -> PlaybackRenderBackend.EXPERIMENTAL_MPV_EMBEDDED PlaybackRenderBackend.EXPERIMENTAL_IJKPLAYER -> PlaybackRenderBackend.EXPERIMENTAL_IJKPLAYER @@ -25,7 +26,6 @@ fun PlaybackRenderBackend.normalizeSupportedBackend(): PlaybackRenderBackend = fun supportedPlaybackRenderBackends(): List = listOf( PlaybackRenderBackend.STANDARD_EXO, - PlaybackRenderBackend.EXPERIMENTAL_GL, PlaybackRenderBackend.EXPERIMENTAL_MPV_EMBEDDED, PlaybackRenderBackend.EXPERIMENTAL_IJKPLAYER, ) diff --git a/data/src/test/kotlin/com/miruplay/tv/data/preferences/PlaybackPreferencesManagerTest.kt b/data/src/test/kotlin/com/miruplay/tv/data/preferences/PlaybackPreferencesManagerTest.kt index 84f57510..77dd24e5 100644 --- a/data/src/test/kotlin/com/miruplay/tv/data/preferences/PlaybackPreferencesManagerTest.kt +++ b/data/src/test/kotlin/com/miruplay/tv/data/preferences/PlaybackPreferencesManagerTest.kt @@ -97,7 +97,7 @@ class PlaybackPreferencesManagerTest { } @Test - fun `manager persists and reloads customized format-aware tone mapping preferences`() = runBlocking { + fun `manager migrates legacy gl backend while preserving tone mapping preferences`() = runBlocking { val updated = FormatAwareToneMappingPreferences( defaultBackend = PlaybackRenderBackend.EXPERIMENTAL_GL, rules = FormatAwareToneMappingPreferences().rules + ( @@ -119,7 +119,7 @@ class PlaybackPreferencesManagerTest { val restored = manager.getFormatAwareToneMappingPreferences() - assertEquals(PlaybackRenderBackend.EXPERIMENTAL_GL, restored.defaultBackend) + assertEquals(PlaybackRenderBackend.STANDARD_EXO, restored.defaultBackend) assertEquals( ToneMappingCurvePreset.REINHARD, restored.rules.getValue(VideoRenderRuleKey.HDR10).curvePreset, diff --git a/player-core/src/main/kotlin/com/miruplay/tv/player/AudioDspMpvOptions.kt b/player-core/src/main/kotlin/com/miruplay/tv/player/AudioDspMpvOptions.kt index a531ea1a..c1196625 100644 --- a/player-core/src/main/kotlin/com/miruplay/tv/player/AudioDspMpvOptions.kt +++ b/player-core/src/main/kotlin/com/miruplay/tv/player/AudioDspMpvOptions.kt @@ -110,7 +110,7 @@ private fun buildLinearPhaseFilter(preset: com.miruplay.tv.model.AudioDspPreset) } val response = FrequencyResponse.sample(plan, frequencies) val entries = response.frequenciesHz.indices.joinToString(";") { index -> - "${number(response.frequenciesHz[index])}:${number(response.magnitudeDb[index])}" + "entry(${number(response.frequenciesHz[index])},${number(response.magnitudeDb[index])})" } val delaySeconds = plan.groupDelayFrames.toDouble() / DSP_SAMPLE_RATE_HZ return "firequalizer=gain_entry='$entries':delay=${number(delaySeconds)}:multi=true:zero_phase=false" diff --git a/player-core/src/main/kotlin/com/miruplay/tv/player/DeviceGlCapabilities.kt b/player-core/src/main/kotlin/com/miruplay/tv/player/DeviceGlCapabilities.kt new file mode 100644 index 00000000..4e666e20 --- /dev/null +++ b/player-core/src/main/kotlin/com/miruplay/tv/player/DeviceGlCapabilities.kt @@ -0,0 +1,19 @@ +package com.miruplay.tv.player + +import android.app.ActivityManager +import android.content.Context +import android.content.pm.ConfigurationInfo + +fun resolveGlEsMajorVersion(reqGlEsVersion: Int): Int = + when { + reqGlEsVersion == ConfigurationInfo.GL_ES_VERSION_UNDEFINED -> 2 + reqGlEsVersion <= 0 -> 2 + else -> reqGlEsVersion shr 16 + } + +fun resolveDeviceGlEsMajorVersion(context: Context): Int { + val activityManager = context.getSystemService(ActivityManager::class.java) + val reqGlEsVersion = activityManager?.deviceConfigurationInfo?.reqGlEsVersion + ?: ConfigurationInfo.GL_ES_VERSION_UNDEFINED + return resolveGlEsMajorVersion(reqGlEsVersion) +} diff --git a/player-core/src/main/kotlin/com/miruplay/tv/player/DiModule.kt b/player-core/src/main/kotlin/com/miruplay/tv/player/DiModule.kt index 33046fda..c8c95c43 100644 --- a/player-core/src/main/kotlin/com/miruplay/tv/player/DiModule.kt +++ b/player-core/src/main/kotlin/com/miruplay/tv/player/DiModule.kt @@ -54,40 +54,6 @@ object PlayerModule { } } - @Provides - @Singleton - @ExperimentalPlaybackPlayer - fun provideExperimentalExoPlayer( - @ApplicationContext context: Context, - dataSourceFactory: PlaybackDataSourceFactory, - audioDspRuntimeConfig: AudioDspRuntimeConfig, - ): ExoPlayer { - val libassSession = LibassSubtitleSession() - val nativeAssAvailable = NativeAssRenderer.isAvailable() - val renderersFactory = ExperimentalRenderersFactory(context, audioDspRuntimeConfig, libassSession) - // The experimental HDR backend relies on stable HEVC surface attachment across - // vendor codecs, so we bias toward compatibility over async throughput here. - .forceDisableMediaCodecAsynchronousQueueing() - .setEnableDecoderFallback(true) - .setMediaCodecSelector(PlaybackMediaCodecSelector) - - return ExoPlayer.Builder(context, renderersFactory) - .setMediaSourceFactory( - DefaultMediaSourceFactory( - ZlibSubtitleProtectingDataSourceFactory(dataSourceFactory), - ZlibSubtitleExtractorsFactory( - session = libassSession, - nativeAvailable = { nativeAssAvailable }, - ), - ), - ) - .build() - .also { player -> - LibassSubtitleRegistry.register(player, libassSession) - if (nativeAssAvailable) player.setVideoFrameMetadataListener(libassSession) - } - } - @Provides @Singleton fun providePlaybackConfig(): PlaybackConfig = PlaybackConfig() @@ -98,8 +64,6 @@ object PlayerModule { @ApplicationContext context: Context, @StandardPlaybackPlayer standardPlayerProvider: javax.inject.Provider, - @ExperimentalPlaybackPlayer - experimentalPlayerProvider: javax.inject.Provider, dataSourceFactory: PlaybackDataSourceFactory, httpRequestResolver: PlaybackHttpRequestResolver, playbackPreferencesRepository: PlaybackPreferencesRepository, @@ -111,7 +75,6 @@ object PlayerModule { return ExoPlaybackController( context = context, standardExoPlayerProvider = standardPlayerProvider, - experimentalExoPlayerProvider = experimentalPlayerProvider, dataSourceFactory = dataSourceFactory, httpRequestResolver = httpRequestResolver, playbackPreferencesRepository = playbackPreferencesRepository, diff --git a/player-core/src/main/kotlin/com/miruplay/tv/player/EmbeddedMpvSessionOptions.kt b/player-core/src/main/kotlin/com/miruplay/tv/player/EmbeddedMpvSessionOptions.kt index 0823d212..a07ac9f9 100644 --- a/player-core/src/main/kotlin/com/miruplay/tv/player/EmbeddedMpvSessionOptions.kt +++ b/player-core/src/main/kotlin/com/miruplay/tv/player/EmbeddedMpvSessionOptions.kt @@ -13,6 +13,7 @@ fun buildEmbeddedMpvSessionOptions( shaderPaths: List, speed: Float = 1.0f, runtimeAbiIs32Bit: Boolean = isEmbeddedMpvRuntime32Bit(), + glEsMajorVersion: Int = 3, debugConfig: EmbeddedMpvDebugConfig = EmbeddedMpvDebugConfig(), audioDspConfig: AudioDspConfig = AudioDspConfig.neutral(), ): MiruMpvSurfaceView.SessionOptions { @@ -26,6 +27,7 @@ fun buildEmbeddedMpvSessionOptions( return MiruMpvSurfaceView.SessionOptions( vo = effectiveEmbeddedMpvVideoOutput( runtimeAbiIs32Bit = runtimeAbiIs32Bit, + glEsMajorVersion = glEsMajorVersion, debugConfig = debugConfig, ), hwdec = effectiveEmbeddedMpvHwdec(debugConfig), @@ -130,8 +132,13 @@ private fun resolveEmbeddedMpvPeakDetection(strategy: PeakDetectionStrategy): Em fun effectiveEmbeddedMpvVideoOutput( runtimeAbiIs32Bit: Boolean = isEmbeddedMpvRuntime32Bit(), + glEsMajorVersion: Int = 3, debugConfig: EmbeddedMpvDebugConfig = EmbeddedMpvDebugConfig(), -): String = debugConfig.vo ?: if (runtimeAbiIs32Bit) "gpu-hq" else "gpu-next" +): String = debugConfig.vo ?: when { + glEsMajorVersion < 3 -> "mediacodec_embed" + runtimeAbiIs32Bit -> "gpu-hq" + else -> "gpu-next" +} fun effectiveEmbeddedMpvHwdec( debugConfig: EmbeddedMpvDebugConfig = EmbeddedMpvDebugConfig(), diff --git a/player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt b/player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt index d9c89353..17ee9c3b 100644 --- a/player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt +++ b/player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt @@ -11,7 +11,6 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.media3.common.C -import androidx.media3.common.Effect import androidx.media3.common.Format import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata @@ -38,7 +37,6 @@ import com.miruplay.tv.model.SubtitleTrack import com.miruplay.tv.model.ToneMappingRuleSet import com.miruplay.tv.model.VideoRenderRuleKey import com.miruplay.tv.model.VideoSignalDescriptor -import com.miruplay.tv.model.VideoSignalKind import com.miruplay.tv.model.defaultToneMappingRuleSet import com.miruplay.tv.model.normalizeSupportedBackend import com.miruplay.tv.model.preferredSubtitleTrackIndex @@ -77,8 +75,6 @@ class ExoPlaybackController @Inject constructor( @ApplicationContext private val context: Context, @StandardPlaybackPlayer private val standardExoPlayerProvider: Provider, - @ExperimentalPlaybackPlayer - private val experimentalExoPlayerProvider: Provider, private val dataSourceFactory: PlaybackDataSourceFactory, private val httpRequestResolver: PlaybackHttpRequestResolver, private val playbackPreferencesRepository: PlaybackPreferencesRepository, @@ -150,11 +146,8 @@ class ExoPlaybackController @Inject constructor( private val playbackClockSamples = AtomicReference>(emptyList()) private var standardExoPlayer: ExoPlayer? = null - private var experimentalExoPlayer: ExoPlayer? = null private var standardListener: Player.Listener? = null - private var experimentalListener: Player.Listener? = null private var standardAnalyticsListener: AnalyticsListener? = null - private var experimentalAnalyticsListener: AnalyticsListener? = null override suspend fun play(source: PlaybackSource) { MiruLog.i( @@ -239,7 +232,6 @@ class ExoPlaybackController @Inject constructor( } else { activateRemoteControlSession() } - applyVideoEffectsForCurrentConfig() if ( _activeRenderBackend.value == PlaybackRenderBackend.EXPERIMENTAL_MPV_EMBEDDED && !httpConfig.isWebDav(source.uri) @@ -248,7 +240,6 @@ class ExoPlaybackController @Inject constructor( return@withContext } val player = if (httpConfig.isWebDav(source.uri)) standardExoPlayer() else activeExoPlayer() - stopInactivePlayers(player) preparePlayerForPlayback(player) val subtitleConfigs = source.subtitleTracks.mapIndexed { index, track -> @@ -475,7 +466,6 @@ class ExoPlaybackController @Inject constructor( subtitleSelectionWasManual = false containerSignalDescriptor = null _currentVideoSignalDescriptor.value = null - PlaybackCodecSelectionState.decoderPreference = PlaybackDecoderPreference.DEFAULT sessionState = sessionState.afterPlaybackReset(clearSessionState) _requestedRenderBackend.value = playbackPreferences.defaultBackend.normalizeSupportedBackend() _sessionRuleOverrides.value = sessionState.ruleOverrides @@ -742,21 +732,12 @@ class ExoPlaybackController @Inject constructor( releaseLibassSession(player) player.release() } - experimentalExoPlayer?.let { player -> - experimentalListener?.let(player::removeListener) - experimentalAnalyticsListener?.let(player::removeAnalyticsListener) - releaseLibassSession(player) - player.release() - } remoteControlSession?.release() remoteControlSession = null dataSourceFactory.clearHttpConfig() standardExoPlayer = null - experimentalExoPlayer = null standardListener = null - experimentalListener = null standardAnalyticsListener = null - experimentalAnalyticsListener = null } private fun createPlayerListener(player: ExoPlayer) = object : Player.Listener { @@ -1273,26 +1254,6 @@ class ExoPlaybackController @Inject constructor( } else { baseConfig } - PlaybackCodecSelectionState.decoderPreference = when { - config.activeBackend == PlaybackRenderBackend.EXPERIMENTAL_GL && - shouldUseDedicatedExperimentalGlSurface(deviceGlEsMajorVersion) && - descriptor.isHdr -> { - PlaybackDecoderPreference.PREFER_SOFTWARE_VIDEO_FOR_HDR - } - - config.activeBackend == PlaybackRenderBackend.EXPERIMENTAL_GL && - shouldUseExperimentalVideoEffectsPipeline( - activeBackend = config.activeBackend, - glEsMajorVersion = deviceGlEsMajorVersion, - ) && - (descriptor.signalKind == VideoSignalKind.HDR10 || - descriptor.signalKind == VideoSignalKind.HDR10_PLUS || - descriptor.signalKind == VideoSignalKind.UNKNOWN_HDR) -> { - PlaybackDecoderPreference.PREFER_SOFTWARE_HEVC_FOR_HDR - } - - else -> PlaybackDecoderPreference.DEFAULT - } _currentRenderRuleKey.value = config.ruleKey _currentToneMappingRuleSet.value = config.appliedRuleSet _activeRenderBackend.value = config.activeBackend @@ -1319,87 +1280,8 @@ class ExoPlaybackController @Inject constructor( "fallback_reason" to config.fallbackReason.orEmpty(), ), ) - runCatching { - applyVideoEffectsForCurrentConfig() - }.onFailure { error -> - MiruLog.w( - "ExoPlaybackController", - "Failed to apply Exo video effects", - error, - mapOf( - "active_backend" to config.activeBackend.name, - "signal_kind" to descriptor.signalKind.name, - ), - ) - } } - private fun applyVideoEffectsForCurrentConfig() { - val usesExperimentalVideoEffectsPipeline = shouldUseExperimentalVideoEffectsPipeline( - activeBackend = _activeRenderBackend.value, - glEsMajorVersion = deviceGlEsMajorVersion, - ) - if ( - shouldBypassExoVideoEffectsDispatch( - activeBackend = _activeRenderBackend.value, - glEsMajorVersion = deviceGlEsMajorVersion, - ) - ) { - MiruLog.i( - "ExoPlaybackController", - "Bypassed Exo video effects dispatch for dedicated GL surface pipeline", - mapOf( - "active_backend" to _activeRenderBackend.value.name, - "gl_es_major_version" to deviceGlEsMajorVersion.toString(), - ), - ) - return - } - if (!shouldUseExoVideoEffectsPipeline(true, _activeRenderBackend.value, usesExperimentalVideoEffectsPipeline)) { - experimentalPlayerOrNull()?.setVideoEffects(emptyList()) - MiruLog.i( - "ExoPlaybackController", - "Skipped Exo video effects for current backend", - mapOf( - "active_backend" to _activeRenderBackend.value.name, - "gl_es_major_version" to deviceGlEsMajorVersion.toString(), - "experimental_effects_pipeline" to usesExperimentalVideoEffectsPipeline.toString(), - ), - ) - return - } - if (_activeRenderBackend.value != PlaybackRenderBackend.EXPERIMENTAL_GL) { - experimentalPlayerOrNull()?.setVideoEffects(emptyList()) - return - } - val effects = currentVideoEffects() - experimentalExoPlayer().setVideoEffects(effects) - MiruLog.i( - "ExoPlaybackController", - "Applied Exo video effects", - mapOf( - "active_backend" to _activeRenderBackend.value.name, - "effect_count" to effects.size.toString(), - "effects" to effects.joinToString(",") { it.javaClass.simpleName }, - ), - ) - } - - private fun currentVideoEffects(): List = - if ( - shouldUseExperimentalVideoEffectsPipeline( - activeBackend = _activeRenderBackend.value, - glEsMajorVersion = deviceGlEsMajorVersion, - ) - ) { - buildExoVideoEffects( - ruleSet = _currentToneMappingRuleSet.value, - signalDescriptor = _currentVideoSignalDescriptor.value, - ) - } else { - emptyList() - } - private fun scheduleContainerSignalProbeCompletionIfNeeded( sourceUri: String, httpConfig: PlaybackHttpRequestConfig, @@ -1819,48 +1701,21 @@ class ExoPlaybackController @Inject constructor( ruleSet = ruleSet, shaderPaths = shaderPaths, speed = speed, + glEsMajorVersion = deviceGlEsMajorVersion, debugConfig = playbackDebugOverrides.embeddedMpvDebugConfig, audioDspConfig = audioDspRuntimeConfig.config, ) } - private fun activeExoPlayer(): ExoPlayer = - if (_activeRenderBackend.value == PlaybackRenderBackend.EXPERIMENTAL_GL) { - experimentalExoPlayer() - } else { - standardExoPlayer() - } + private fun activeExoPlayer(): ExoPlayer = standardExoPlayer() - private fun activeExoPlayerOrNull(): ExoPlayer? = - if (_activeRenderBackend.value == PlaybackRenderBackend.EXPERIMENTAL_GL) { - experimentalPlayerOrNull() - } else { - standardPlayerOrNull() - } + private fun activeExoPlayerOrNull(): ExoPlayer? = standardPlayerOrNull() private fun isCurrentPlayer(player: ExoPlayer): Boolean = - when (_activeRenderBackend.value) { - PlaybackRenderBackend.EXPERIMENTAL_GL -> player === experimentalPlayerOrNull() - PlaybackRenderBackend.STANDARD_EXO -> player === standardPlayerOrNull() - else -> false - } - - private fun stopInactivePlayers(activePlayer: ExoPlayer) { - standardPlayerOrNull()?.let { player -> - if (activePlayer !== player) { - preparePlayerForPlayback(player) - } - } - experimentalPlayerOrNull()?.let { player -> - if (activePlayer !== player) { - preparePlayerForPlayback(player) - } - } - } + player === standardPlayerOrNull() private fun stopAllPlayers() { standardPlayerOrNull()?.let(::preparePlayerForPlayback) - experimentalPlayerOrNull()?.let(::preparePlayerForPlayback) } private fun preparePlayerForPlayback(player: ExoPlayer) { @@ -1890,22 +1745,7 @@ class ExoPlaybackController @Inject constructor( return player } - private fun experimentalExoPlayer(): ExoPlayer { - experimentalExoPlayer?.let { return it } - val player = experimentalExoPlayerProvider.get() - val listener = createPlayerListener(player) - val analyticsListener = createAnalyticsListener(player) - player.addListener(listener) - player.addAnalyticsListener(analyticsListener) - experimentalExoPlayer = player - experimentalListener = listener - experimentalAnalyticsListener = analyticsListener - return player - } - private fun standardPlayerOrNull(): ExoPlayer? = standardExoPlayer - - private fun experimentalPlayerOrNull(): ExoPlayer? = experimentalExoPlayer } internal data class EmbeddedMpvStartupState( diff --git a/player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalHdrSdrVideoGraphFactory.kt b/player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalHdrSdrVideoGraphFactory.kt deleted file mode 100644 index 588eea15..00000000 --- a/player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalHdrSdrVideoGraphFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -@file:Suppress("UnsafeOptInUsageError") - -package com.miruplay.tv.player - -internal fun shouldUseExoVideoEffectsPipeline( - effectPipelineEnabled: Boolean, - activeBackend: com.miruplay.tv.model.PlaybackRenderBackend, - usesExperimentalEffectsPlayer: Boolean, -): Boolean = - effectPipelineEnabled && - activeBackend == com.miruplay.tv.model.PlaybackRenderBackend.EXPERIMENTAL_GL && - usesExperimentalEffectsPlayer diff --git a/player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalHdrSurfaceMediaCodecVideoRenderer.kt b/player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalHdrSurfaceMediaCodecVideoRenderer.kt deleted file mode 100644 index cfca8ac2..00000000 --- a/player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalHdrSurfaceMediaCodecVideoRenderer.kt +++ /dev/null @@ -1,151 +0,0 @@ -@file:Suppress("UnsafeOptInUsageError") - -package com.miruplay.tv.player - -import android.content.Context -import android.media.MediaFormat -import android.os.Handler -import android.util.Log -import androidx.media3.common.ColorInfo -import androidx.media3.common.Effect -import androidx.media3.common.Format -import androidx.media3.common.MimeTypes -import androidx.media3.common.util.UnstableApi -import androidx.media3.common.util.Util -import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter -import androidx.media3.exoplayer.mediacodec.MediaCodecSelector -import androidx.media3.exoplayer.video.MediaCodecVideoRenderer -import androidx.media3.exoplayer.video.VideoRendererEventListener -import com.miruplay.tv.core.common.logging.MiruLog - -internal fun shouldRequestDecoderToneMapToSdr( - format: Format, - experimentalVideoPipelineMode: ExperimentalVideoPipelineMode, - sdkInt: Int = Util.SDK_INT, -): Boolean { - if (sdkInt < 31 || experimentalVideoPipelineMode != ExperimentalVideoPipelineMode.DEDICATED_GL_SURFACE) { - return false - } - // The dedicated GL backend is responsible for consuming HDR output and applying tone mapping. - // Requesting decoder-side HDR->SDR conversion on the tested Rockchip box produces black frames - // while also bypassing the format-aware mapping pipeline we actually want to validate. - return false -} - -@UnstableApi -internal class ExperimentalHdrSurfaceMediaCodecVideoRenderer( - context: Context, - mediaCodecAdapterFactory: MediaCodecAdapter.Factory, - mediaCodecSelector: MediaCodecSelector, - allowedVideoJoiningTimeMs: Long, - enableDecoderFallback: Boolean, - eventHandler: Handler, - eventListener: VideoRendererEventListener, - maxDroppedFramesToNotify: Int, - private val experimentalVideoPipelineMode: ExperimentalVideoPipelineMode, -) : MediaCodecVideoRenderer( - context, - mediaCodecAdapterFactory, - mediaCodecSelector, - allowedVideoJoiningTimeMs, - enableDecoderFallback, - eventHandler, - eventListener, - maxDroppedFramesToNotify, -) { - - override fun getMediaFormat( - format: Format, - codecMimeType: String, - codecMaxValues: CodecMaxValues, - codecOperatingRate: Float, - deviceNeedsNoPostProcessWorkaround: Boolean, - tunnelingAudioSessionId: Int, - ): MediaFormat { - val mediaFormat = super.getMediaFormat( - format, - codecMimeType, - codecMaxValues, - codecOperatingRate, - deviceNeedsNoPostProcessWorkaround, - tunnelingAudioSessionId, - ) - val isHdrInput = format.colorInfo?.let(ColorInfo::isTransferHdr) == true || - format.sampleMimeType == MimeTypes.VIDEO_DOLBY_VISION - if (shouldRequestDecoderToneMapToSdr(format, experimentalVideoPipelineMode)) { - mediaFormat.setInteger( - MediaFormat.KEY_COLOR_TRANSFER_REQUEST, - MediaFormat.COLOR_TRANSFER_SDR_VIDEO, - ) - Log.i( - "ExperimentalHdrSurfaceRenderer", - "Requesting decoder HDR->SDR output sampleMimeType=${format.sampleMimeType} codecs=${format.codecs.orEmpty()}", - ) - MiruLog.i( - "ExperimentalHdrSurfaceRenderer", - "Requesting decoder HDR->SDR output", - mapOf( - "sample_mime_type" to format.sampleMimeType.orEmpty(), - "codecs" to format.codecs.orEmpty(), - "width" to format.width.toString(), - "height" to format.height.toString(), - ), - ) - } else if (isHdrInput && experimentalVideoPipelineMode == ExperimentalVideoPipelineMode.DEDICATED_GL_SURFACE) { - Log.i( - "ExperimentalHdrSurfaceRenderer", - "Keeping decoder HDR output for dedicated GL tone mapping sampleMimeType=${format.sampleMimeType} codecs=${format.codecs.orEmpty()}", - ) - MiruLog.i( - "ExperimentalHdrSurfaceRenderer", - "Keeping decoder HDR output for dedicated GL tone mapping", - mapOf( - "sample_mime_type" to format.sampleMimeType.orEmpty(), - "codecs" to format.codecs.orEmpty(), - "width" to format.width.toString(), - "height" to format.height.toString(), - ), - ) - } - return mediaFormat - } - - override fun onCodecInitialized( - name: String, - configuration: MediaCodecAdapter.Configuration, - initializedTimestampMs: Long, - initializationDurationMs: Long, - ) { - super.onCodecInitialized(name, configuration, initializedTimestampMs, initializationDurationMs) - Log.i( - "ExperimentalHdrSurfaceRenderer", - "Initialized codec name=$name durationMs=$initializationDurationMs pipeline=$experimentalVideoPipelineMode", - ) - MiruLog.i( - "ExperimentalHdrSurfaceRenderer", - "Initialized codec", - mapOf( - "codec_name" to name, - "initialization_duration_ms" to initializationDurationMs.toString(), - "pipeline" to experimentalVideoPipelineMode.name, - ), - ) - } - - override fun setVideoEffects(videoEffects: List) { - if (experimentalVideoPipelineMode == ExperimentalVideoPipelineMode.DEDICATED_GL_SURFACE) { - Log.i( - "ExperimentalHdrSurfaceRenderer", - "Ignoring Media3 video effects update for dedicated GL surface pipeline size=${videoEffects.size}", - ) - MiruLog.i( - "ExperimentalHdrSurfaceRenderer", - "Ignoring Media3 video effects update for dedicated GL surface pipeline", - mapOf("effect_count" to videoEffects.size.toString()), - ) - return - } - super.setVideoEffects(videoEffects) - } - -} diff --git a/player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalRenderersFactory.kt b/player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalRenderersFactory.kt deleted file mode 100644 index 39d2f38e..00000000 --- a/player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalRenderersFactory.kt +++ /dev/null @@ -1,100 +0,0 @@ -@file:android.annotation.SuppressLint("RestrictedApi") - -package com.miruplay.tv.player - -import android.content.Context -import android.os.Handler -import android.os.Looper -import android.util.Log -import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.DefaultRenderersFactory -import androidx.media3.exoplayer.Renderer -import androidx.media3.exoplayer.mediacodec.MediaCodecSelector -import androidx.media3.exoplayer.video.MediaCodecVideoRenderer -import androidx.media3.exoplayer.video.VideoRendererEventListener -import androidx.media3.exoplayer.audio.AudioSink -import androidx.media3.exoplayer.audio.DefaultAudioSink -import androidx.media3.exoplayer.text.TextOutput - -@UnstableApi -class ExperimentalRenderersFactory( - context: Context, - private val audioDspRuntimeConfig: AudioDspRuntimeConfig, - private val libassSession: LibassSubtitleSession, -) : DefaultRenderersFactory(context) { - private val experimentalVideoPipelineMode = - resolveExperimentalVideoPipelineMode(resolveDeviceGlEsMajorVersion(context)) - - override fun buildAudioSink( - context: Context, - enableFloatOutput: Boolean, - enableAudioTrackPlaybackParams: Boolean, - ): AudioSink { - val policy = AudioDspOutputPolicy.forConfig(audioDspRuntimeConfig.config) - return DefaultAudioSink.Builder(context) - .setEnableFloatOutput(enableFloatOutput || policy.forcePcm) - .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) - .setAudioProcessors(arrayOf(DspAudioProcessor(audioDspRuntimeConfig))) - .build() - } - - override fun buildTextRenderers( - context: Context, - output: TextOutput, - outputLooper: Looper, - extensionRendererMode: Int, - out: ArrayList, - ) = addLibassTextRenderers(libassSession, output, outputLooper, out) - - override fun buildVideoRenderers( - context: Context, - extensionRendererMode: Int, - mediaCodecSelector: MediaCodecSelector, - enableDecoderFallback: Boolean, - eventHandler: Handler, - eventListener: VideoRendererEventListener, - allowedVideoJoiningTimeMs: Long, - out: ArrayList, - ) { - super.buildVideoRenderers( - context, - extensionRendererMode, - mediaCodecSelector, - enableDecoderFallback, - eventHandler, - eventListener, - allowedVideoJoiningTimeMs, - out, - ) - - val rendererIndex = out.indexOfFirst { it is MediaCodecVideoRenderer } - if (rendererIndex < 0) { - return - } - - if (experimentalVideoPipelineMode == ExperimentalVideoPipelineMode.MEDIA3_EFFECTS) { - Log.i( - "ExperimentalRenderersFactory", - "Using Media3 native effects renderer", - ) - return - } - - val replacementRenderer = ExperimentalHdrSurfaceMediaCodecVideoRenderer( - context = context, - mediaCodecAdapterFactory = getCodecAdapterFactory(), - mediaCodecSelector = mediaCodecSelector, - allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs, - enableDecoderFallback = enableDecoderFallback, - eventHandler = eventHandler, - eventListener = eventListener, - maxDroppedFramesToNotify = MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY, - experimentalVideoPipelineMode = experimentalVideoPipelineMode, - ) - out[rendererIndex] = replacementRenderer - Log.i( - "ExperimentalRenderersFactory", - "Configured experimental renderer mode=$experimentalVideoPipelineMode renderer=${replacementRenderer.javaClass.simpleName}", - ) - } -} diff --git a/player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalVideoPipelineMode.kt b/player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalVideoPipelineMode.kt deleted file mode 100644 index d363284d..00000000 --- a/player-core/src/main/kotlin/com/miruplay/tv/player/ExperimentalVideoPipelineMode.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.miruplay.tv.player - -import android.app.ActivityManager -import android.content.Context -import android.content.pm.ConfigurationInfo -import com.miruplay.tv.model.PlaybackRenderBackend - -enum class ExperimentalVideoPipelineMode { - MEDIA3_EFFECTS, - DEDICATED_GL_SURFACE, -} - -fun resolveGlEsMajorVersion(reqGlEsVersion: Int): Int = - when { - reqGlEsVersion == ConfigurationInfo.GL_ES_VERSION_UNDEFINED -> 2 - reqGlEsVersion <= 0 -> 2 - else -> reqGlEsVersion shr 16 - } - -fun resolveDeviceGlEsMajorVersion(context: Context): Int { - val activityManager = context.getSystemService(ActivityManager::class.java) - val reqGlEsVersion = activityManager?.deviceConfigurationInfo?.reqGlEsVersion - ?: ConfigurationInfo.GL_ES_VERSION_UNDEFINED - return resolveGlEsMajorVersion(reqGlEsVersion) -} - -fun resolveExperimentalVideoPipelineMode(glEsMajorVersion: Int): ExperimentalVideoPipelineMode = - if (glEsMajorVersion >= 3) { - ExperimentalVideoPipelineMode.MEDIA3_EFFECTS - } else { - ExperimentalVideoPipelineMode.DEDICATED_GL_SURFACE - } - -fun shouldUseDedicatedExperimentalGlSurface(glEsMajorVersion: Int): Boolean = - resolveExperimentalVideoPipelineMode(glEsMajorVersion) == - ExperimentalVideoPipelineMode.DEDICATED_GL_SURFACE - -fun shouldUseExperimentalVideoEffectsPipeline( - activeBackend: PlaybackRenderBackend, - glEsMajorVersion: Int, -): Boolean = - activeBackend == PlaybackRenderBackend.EXPERIMENTAL_GL && - !shouldUseDedicatedExperimentalGlSurface(glEsMajorVersion) - -fun shouldBypassExoVideoEffectsDispatch( - activeBackend: PlaybackRenderBackend, - glEsMajorVersion: Int, -): Boolean = - activeBackend == PlaybackRenderBackend.EXPERIMENTAL_GL && - shouldUseDedicatedExperimentalGlSurface(glEsMajorVersion) diff --git a/player-core/src/main/kotlin/com/miruplay/tv/player/PlaybackPlayerQualifiers.kt b/player-core/src/main/kotlin/com/miruplay/tv/player/PlaybackPlayerQualifiers.kt index 25442c86..736ea351 100644 --- a/player-core/src/main/kotlin/com/miruplay/tv/player/PlaybackPlayerQualifiers.kt +++ b/player-core/src/main/kotlin/com/miruplay/tv/player/PlaybackPlayerQualifiers.kt @@ -5,7 +5,3 @@ import javax.inject.Qualifier @Qualifier @Retention(AnnotationRetention.BINARY) annotation class StandardPlaybackPlayer - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class ExperimentalPlaybackPlayer diff --git a/player-core/src/main/kotlin/com/miruplay/tv/player/PlayerFactoryImpl.kt b/player-core/src/main/kotlin/com/miruplay/tv/player/PlayerFactoryImpl.kt index 014654a1..8122c175 100644 --- a/player-core/src/main/kotlin/com/miruplay/tv/player/PlayerFactoryImpl.kt +++ b/player-core/src/main/kotlin/com/miruplay/tv/player/PlayerFactoryImpl.kt @@ -15,8 +15,6 @@ class PlayerFactoryImpl @Inject constructor( @ApplicationContext private val context: Context, @StandardPlaybackPlayer private val standardExoPlayerProvider: Provider, - @ExperimentalPlaybackPlayer - private val experimentalExoPlayerProvider: Provider, private val dataSourceFactory: PlaybackDataSourceFactory, private val httpRequestResolver: PlaybackHttpRequestResolver, private val playbackPreferencesRepository: PlaybackPreferencesRepository, @@ -27,7 +25,6 @@ class PlayerFactoryImpl @Inject constructor( return ExoPlaybackController( context = context, standardExoPlayerProvider = standardExoPlayerProvider, - experimentalExoPlayerProvider = experimentalExoPlayerProvider, dataSourceFactory = dataSourceFactory, httpRequestResolver = httpRequestResolver, playbackPreferencesRepository = playbackPreferencesRepository, diff --git a/player-core/src/test/kotlin/com/miruplay/tv/player/AudioDspMpvOptionsTest.kt b/player-core/src/test/kotlin/com/miruplay/tv/player/AudioDspMpvOptionsTest.kt index 3cc7660e..8565b236 100644 --- a/player-core/src/test/kotlin/com/miruplay/tv/player/AudioDspMpvOptionsTest.kt +++ b/player-core/src/test/kotlin/com/miruplay/tv/player/AudioDspMpvOptionsTest.kt @@ -158,6 +158,8 @@ class AudioDspMpvOptionsTest { assertTrue(filters.contains("firequalizer")) assertTrue(filters.contains("delay=")) assertTrue(filters.contains("gain_entry='")) + assertTrue(filters.contains("entry(10.0,")) + assertTrue(!filters.contains("10.0:")) assertTrue(filters.length > 500) } diff --git a/player-core/src/test/kotlin/com/miruplay/tv/player/EmbeddedMpvSessionOptionsTest.kt b/player-core/src/test/kotlin/com/miruplay/tv/player/EmbeddedMpvSessionOptionsTest.kt index 6583038c..28c5674c 100644 --- a/player-core/src/test/kotlin/com/miruplay/tv/player/EmbeddedMpvSessionOptionsTest.kt +++ b/player-core/src/test/kotlin/com/miruplay/tv/player/EmbeddedMpvSessionOptionsTest.kt @@ -154,4 +154,29 @@ class EmbeddedMpvSessionOptionsTest { assertEquals("gpu-hq", options.vo) assertEquals("no", options.hwdec) } + + @Test + fun `gles version parser falls back to es2 for missing capability`() { + assertEquals(2, resolveGlEsMajorVersion(0)) + assertEquals(2, resolveGlEsMajorVersion(-1)) + assertEquals(3, resolveGlEsMajorVersion(0x00030000)) + } + + @Test + fun `gles2 devices use direct mediacodec output regardless of runtime abi`() { + assertEquals( + "mediacodec_embed", + effectiveEmbeddedMpvVideoOutput( + runtimeAbiIs32Bit = false, + glEsMajorVersion = 2, + ), + ) + assertEquals( + "mediacodec_embed", + effectiveEmbeddedMpvVideoOutput( + runtimeAbiIs32Bit = true, + glEsMajorVersion = 2, + ), + ) + } } diff --git a/player-core/src/test/kotlin/com/miruplay/tv/player/ExoPlaybackControllerLazyInitTest.kt b/player-core/src/test/kotlin/com/miruplay/tv/player/ExoPlaybackControllerLazyInitTest.kt index 34d8be0e..09009d9c 100644 --- a/player-core/src/test/kotlin/com/miruplay/tv/player/ExoPlaybackControllerLazyInitTest.kt +++ b/player-core/src/test/kotlin/com/miruplay/tv/player/ExoPlaybackControllerLazyInitTest.kt @@ -4,15 +4,13 @@ import android.app.ActivityManager import android.content.Context import androidx.media3.exoplayer.ExoPlayer import com.miruplay.tv.model.PlaybackRenderBackend -import com.miruplay.tv.model.VideoSignalDescriptor -import com.miruplay.tv.model.VideoSignalKind import com.miruplay.tv.repository.PlaybackPreferencesRepository -import javax.inject.Provider import io.mockk.every import io.mockk.mockk +import javax.inject.Provider import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain @@ -36,84 +34,41 @@ class ExoPlaybackControllerLazyInitTest { } @Test - fun `constructing controller does not initialize exo player providers`() { - val standardPlayer = mockk(relaxed = true) - val experimentalPlayer = mockk(relaxed = true) - val standardProvider = CountingProvider { standardPlayer } - val experimentalProvider = CountingProvider { experimentalPlayer } + fun `constructing controller does not initialize exo player provider`() { + val standardProvider = CountingProvider { mockk(relaxed = true) } - createController( - standardProvider = standardProvider, - experimentalProvider = experimentalProvider, - ) + createController(standardProvider) assertEquals(0, standardProvider.getCallCount) - assertEquals(0, experimentalProvider.getCallCount) } @Test - fun `getPlayer initializes only the active standard exo player`() { + fun `getPlayer initializes the standard exo player`() { val standardPlayer = mockk(relaxed = true) - val experimentalPlayer = mockk(relaxed = true) val standardProvider = CountingProvider { standardPlayer } - val experimentalProvider = CountingProvider { experimentalPlayer } - val controller = createController( - standardProvider = standardProvider, - experimentalProvider = experimentalProvider, - ) + val controller = createController(standardProvider) val player = controller.getPlayer() assertSame(standardPlayer, player) assertEquals(1, standardProvider.getCallCount) - assertEquals(0, experimentalProvider.getCallCount) } @Test - fun `experimental backend initializes only the experimental exo player`() = runBlocking { - val standardPlayer = mockk(relaxed = true) - val experimentalPlayer = mockk(relaxed = true) - val standardProvider = CountingProvider { standardPlayer } - val experimentalProvider = CountingProvider { experimentalPlayer } - val controller = createController( - standardProvider = standardProvider, - experimentalProvider = experimentalProvider, - ) - - controller.setRequestedRenderBackend(PlaybackRenderBackend.EXPERIMENTAL_GL) - val player = controller.getPlayer() - - assertSame(experimentalPlayer, player) - assertEquals(0, standardProvider.getCallCount) - assertEquals(1, experimentalProvider.getCallCount) - } - - @Test - fun `ijk backend does not initialize either exo player`() = runBlocking { + fun `ijk backend does not initialize exo player`() = runBlocking { val standardProvider = CountingProvider { mockk(relaxed = true) } - val experimentalProvider = CountingProvider { mockk(relaxed = true) } - val controller = createController( - standardProvider = standardProvider, - experimentalProvider = experimentalProvider, - ) + val controller = createController(standardProvider) controller.setRequestedRenderBackend(PlaybackRenderBackend.EXPERIMENTAL_IJKPLAYER) assertNull(controller.getPlayer()) assertEquals(0, standardProvider.getCallCount) - assertEquals(0, experimentalProvider.getCallCount) } @Test - fun `refreshing runtime config for standard backend does not initialize experimental exo player`() = runBlocking { - val standardPlayer = mockk(relaxed = true) - val experimentalPlayer = mockk(relaxed = true) - val standardProvider = CountingProvider { standardPlayer } - val experimentalProvider = CountingProvider { experimentalPlayer } - val controller = createController( - standardProvider = standardProvider, - experimentalProvider = experimentalProvider, - ) + fun `refreshing runtime config for standard backend does not initialize exo player`() = runBlocking { + val standardProvider = CountingProvider { mockk(relaxed = true) } + val controller = createController(standardProvider) controller.setRequestedRenderBackend(PlaybackRenderBackend.STANDARD_EXO) controller.setSessionRuleOverride( @@ -121,20 +76,16 @@ class ExoPlaybackControllerLazyInitTest { ruleSet = com.miruplay.tv.model.defaultToneMappingRuleSet(com.miruplay.tv.model.VideoRenderRuleKey.HDR10), ) - assertEquals(0, experimentalProvider.getCallCount) + assertEquals(0, standardProvider.getCallCount) } - private fun createController( - standardProvider: Provider, - experimentalProvider: Provider, - ): ExoPlaybackController { + private fun createController(standardProvider: Provider): ExoPlaybackController { val context = mockk(relaxed = true).apply { every { getSystemService(ActivityManager::class.java) } returns null } return ExoPlaybackController( context = context, standardExoPlayerProvider = standardProvider, - experimentalExoPlayerProvider = experimentalProvider, dataSourceFactory = mockk(relaxed = true), httpRequestResolver = mockk(relaxed = true), playbackPreferencesRepository = mockk(relaxed = true), diff --git a/player-core/src/test/kotlin/com/miruplay/tv/player/ExperimentalHdrSdrVideoGraphFactoryTest.kt b/player-core/src/test/kotlin/com/miruplay/tv/player/ExperimentalHdrSdrVideoGraphFactoryTest.kt deleted file mode 100644 index 5f291608..00000000 --- a/player-core/src/test/kotlin/com/miruplay/tv/player/ExperimentalHdrSdrVideoGraphFactoryTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.miruplay.tv.player - -import com.miruplay.tv.model.PlaybackRenderBackend -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test - -class ExperimentalHdrSdrVideoGraphFactoryTest { - @Test - fun `only experimental backend enables exo effect pipeline`() { - assertTrue( - shouldUseExoVideoEffectsPipeline( - effectPipelineEnabled = true, - activeBackend = PlaybackRenderBackend.EXPERIMENTAL_GL, - usesExperimentalEffectsPlayer = true, - ) - ) - assertFalse( - shouldUseExoVideoEffectsPipeline( - effectPipelineEnabled = true, - activeBackend = PlaybackRenderBackend.STANDARD_EXO, - usesExperimentalEffectsPlayer = true, - ) - ) - assertFalse( - shouldUseExoVideoEffectsPipeline( - effectPipelineEnabled = false, - activeBackend = PlaybackRenderBackend.EXPERIMENTAL_GL, - usesExperimentalEffectsPlayer = true, - ) - ) - assertFalse( - shouldUseExoVideoEffectsPipeline( - effectPipelineEnabled = true, - activeBackend = PlaybackRenderBackend.EXPERIMENTAL_GL, - usesExperimentalEffectsPlayer = false, - ) - ) - } -} diff --git a/player-core/src/test/kotlin/com/miruplay/tv/player/ExperimentalHdrSurfaceMediaCodecVideoRendererTest.kt b/player-core/src/test/kotlin/com/miruplay/tv/player/ExperimentalHdrSurfaceMediaCodecVideoRendererTest.kt deleted file mode 100644 index 45ca31e6..00000000 --- a/player-core/src/test/kotlin/com/miruplay/tv/player/ExperimentalHdrSurfaceMediaCodecVideoRendererTest.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.miruplay.tv.player - -import androidx.media3.common.C -import androidx.media3.common.ColorInfo -import androidx.media3.common.Format -import androidx.media3.common.MimeTypes -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test - -class ExperimentalHdrSurfaceMediaCodecVideoRendererTest { - @Test - fun `dedicated gl pipeline keeps hdr decoder output for hdr color info on api 31 plus`() { - val hdrFormat = Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setColorInfo( - ColorInfo.Builder() - .setColorSpace(C.COLOR_SPACE_BT2020) - .setColorRange(C.COLOR_RANGE_LIMITED) - .setColorTransfer(C.COLOR_TRANSFER_ST2084) - .setLumaBitdepth(10) - .setChromaBitdepth(10) - .build(), - ) - .build() - - assertFalse( - shouldRequestDecoderToneMapToSdr( - format = hdrFormat, - experimentalVideoPipelineMode = ExperimentalVideoPipelineMode.DEDICATED_GL_SURFACE, - sdkInt = 35, - ) - ) - } - - @Test - fun `media3 effects pipeline does not request decoder hdr to sdr`() { - val hdrFormat = Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H265) - .setColorInfo( - ColorInfo.Builder() - .setColorSpace(C.COLOR_SPACE_BT2020) - .setColorRange(C.COLOR_RANGE_LIMITED) - .setColorTransfer(C.COLOR_TRANSFER_HLG) - .build(), - ) - .build() - - assertFalse( - shouldRequestDecoderToneMapToSdr( - format = hdrFormat, - experimentalVideoPipelineMode = ExperimentalVideoPipelineMode.MEDIA3_EFFECTS, - sdkInt = 35, - ) - ) - } - - @Test - fun `sdr format does not request decoder hdr to sdr`() { - val sdrFormat = Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setColorInfo(ColorInfo.SDR_BT709_LIMITED) - .build() - - assertFalse( - shouldRequestDecoderToneMapToSdr( - format = sdrFormat, - experimentalVideoPipelineMode = ExperimentalVideoPipelineMode.DEDICATED_GL_SURFACE, - sdkInt = 35, - ) - ) - } - - @Test - fun `dolby vision samples keep hdr decoder output even when color info is absent`() { - val dolbyVisionFormat = Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) - .build() - - assertFalse( - shouldRequestDecoderToneMapToSdr( - format = dolbyVisionFormat, - experimentalVideoPipelineMode = ExperimentalVideoPipelineMode.DEDICATED_GL_SURFACE, - sdkInt = 35, - ) - ) - } - - @Test - fun `api 30 and below cannot request decoder hdr to sdr`() { - val hdrFormat = Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) - .setColorInfo( - ColorInfo.Builder() - .setColorSpace(C.COLOR_SPACE_BT2020) - .setColorRange(C.COLOR_RANGE_LIMITED) - .setColorTransfer(C.COLOR_TRANSFER_ST2084) - .build(), - ) - .build() - - assertFalse( - shouldRequestDecoderToneMapToSdr( - format = hdrFormat, - experimentalVideoPipelineMode = ExperimentalVideoPipelineMode.DEDICATED_GL_SURFACE, - sdkInt = 30, - ) - ) - } -} diff --git a/player-core/src/test/kotlin/com/miruplay/tv/player/ExperimentalVideoPipelineModeTest.kt b/player-core/src/test/kotlin/com/miruplay/tv/player/ExperimentalVideoPipelineModeTest.kt deleted file mode 100644 index 51cc08c6..00000000 --- a/player-core/src/test/kotlin/com/miruplay/tv/player/ExperimentalVideoPipelineModeTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.miruplay.tv.player - -import com.miruplay.tv.model.PlaybackRenderBackend -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test - -class ExperimentalVideoPipelineModeTest { - @Test - fun `es2 devices use the dedicated gl surface experimental pipeline`() { - assertEquals( - ExperimentalVideoPipelineMode.DEDICATED_GL_SURFACE, - resolveExperimentalVideoPipelineMode(glEsMajorVersion = 2), - ) - assertTrue(shouldUseDedicatedExperimentalGlSurface(glEsMajorVersion = 2)) - } - - @Test - fun `es3 devices keep the media3 video effects pipeline`() { - assertEquals( - ExperimentalVideoPipelineMode.MEDIA3_EFFECTS, - resolveExperimentalVideoPipelineMode(glEsMajorVersion = 3), - ) - assertFalse(shouldUseDedicatedExperimentalGlSurface(glEsMajorVersion = 3)) - } - - @Test - fun `experimental video effects pipeline is only used for experimental backend on es3 devices`() { - assertTrue( - shouldUseExperimentalVideoEffectsPipeline( - activeBackend = PlaybackRenderBackend.EXPERIMENTAL_GL, - glEsMajorVersion = 3, - ) - ) - assertFalse( - shouldUseExperimentalVideoEffectsPipeline( - activeBackend = PlaybackRenderBackend.EXPERIMENTAL_GL, - glEsMajorVersion = 2, - ) - ) - assertFalse( - shouldUseExperimentalVideoEffectsPipeline( - activeBackend = PlaybackRenderBackend.STANDARD_EXO, - glEsMajorVersion = 3, - ) - ) - } - - @Test - fun `dedicated gl pipeline bypasses exo video effects dispatch entirely`() { - assertTrue( - shouldBypassExoVideoEffectsDispatch( - activeBackend = PlaybackRenderBackend.EXPERIMENTAL_GL, - glEsMajorVersion = 2, - ) - ) - assertFalse( - shouldBypassExoVideoEffectsDispatch( - activeBackend = PlaybackRenderBackend.EXPERIMENTAL_GL, - glEsMajorVersion = 3, - ) - ) - assertFalse( - shouldBypassExoVideoEffectsDispatch( - activeBackend = PlaybackRenderBackend.STANDARD_EXO, - glEsMajorVersion = 2, - ) - ) - } - - @Test - fun `undefined gl es version falls back to the safer dedicated surface pipeline`() { - assertEquals(2, resolveGlEsMajorVersion(android.content.pm.ConfigurationInfo.GL_ES_VERSION_UNDEFINED)) - } -} diff --git a/player-mpv-android/src/main/kotlin/is/xyz/mpv/BaseMPVView.kt b/player-mpv-android/src/main/kotlin/is/xyz/mpv/BaseMPVView.kt index 266d6ee1..33ad3876 100644 --- a/player-mpv-android/src/main/kotlin/is/xyz/mpv/BaseMPVView.kt +++ b/player-mpv-android/src/main/kotlin/is/xyz/mpv/BaseMPVView.kt @@ -44,10 +44,14 @@ abstract class BaseMPVView @JvmOverloads constructor( } fun setVo(vo: String) { - voInUse = vo + updateVoInUse(vo) MPVLib.setOptionString("vo", vo) } + protected fun updateVoInUse(vo: String) { + voInUse = vo + } + protected fun isPlaybackSurfaceAttached(): Boolean = surfaceAttached override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { diff --git a/player-mpv-android/src/main/kotlin/is/xyz/mpv/MiruMpvSurfaceView.kt b/player-mpv-android/src/main/kotlin/is/xyz/mpv/MiruMpvSurfaceView.kt index d4774f07..9e3fdcbd 100644 --- a/player-mpv-android/src/main/kotlin/is/xyz/mpv/MiruMpvSurfaceView.kt +++ b/player-mpv-android/src/main/kotlin/is/xyz/mpv/MiruMpvSurfaceView.kt @@ -194,7 +194,7 @@ class MiruMpvSurfaceView @JvmOverloads constructor( MPVLib.setOptionString("audio-set-media-role", "yes") MPVLib.setOptionString("hwdec", sessionOptions.hwdec) MPVLib.setOptionString("hwdec-codecs", EMBEDDED_MPV_HWDEC_CODECS) - MPVLib.setOptionString("vo", sessionOptions.vo) + setVo(sessionOptions.vo) MPVLib.setOptionString("save-position-on-quit", "no") // Keep authored ASS styles and positions intact; libass owns subtitle layout. mpvSubtitleLayoutNormalisationOptions.forEach { (name, value) -> @@ -289,6 +289,7 @@ class MiruMpvSurfaceView @JvmOverloads constructor( } private fun applyRuntimeOptions(options: SessionOptions) { + updateVoInUse(options.vo) MPVLib.setPropertyString("vo", options.vo) MPVLib.setPropertyString("hwdec", options.hwdec) applyColorPipelineProperties(options) diff --git a/ui-tv/src/main/kotlin/com/miruplay/tv/ui/player/GLVideoSurfaceView.kt b/ui-tv/src/main/kotlin/com/miruplay/tv/ui/player/GLVideoSurfaceView.kt deleted file mode 100644 index 4884534a..00000000 --- a/ui-tv/src/main/kotlin/com/miruplay/tv/ui/player/GLVideoSurfaceView.kt +++ /dev/null @@ -1,570 +0,0 @@ -package com.miruplay.tv.ui.player - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.SurfaceTexture -import android.opengl.GLES11Ext -import android.opengl.GLES20 -import android.opengl.GLSurfaceView -import android.opengl.GLES30 -import android.util.Log -import android.view.Surface -import com.miruplay.tv.core.common.logging.MiruLog -import androidx.media3.common.Player -import com.miruplay.tv.model.ToneMappingCurvePreset -import com.miruplay.tv.model.ToneMappingRuleSet -import com.miruplay.tv.model.VideoSignalDescriptor -import com.miruplay.tv.model.VideoSignalKind -import java.io.File -import java.io.FileOutputStream -import java.nio.ByteBuffer -import java.nio.ByteOrder -import java.nio.FloatBuffer -import java.util.concurrent.atomic.AtomicBoolean -import javax.microedition.khronos.egl.EGLConfig -import javax.microedition.khronos.opengles.GL10 - -internal const val MIN_DECODER_SURFACE_SIZE = 4 - -internal fun isDecoderVideoSurfaceReady( - surface: Surface?, - width: Int, - height: Int, -): Boolean = surface != null && width >= MIN_DECODER_SURFACE_SIZE && height >= MIN_DECODER_SURFACE_SIZE - -class GLVideoSurfaceView( - context: Context, -) : GLSurfaceView(context) { - private val renderer = ToneMappingRenderer() - private var currentPlayer: Player? = null - private var attachedPlayer: Player? = null - private var attachedSurface: Surface? = null - private var onVideoSurfaceReadyChanged: ((Boolean) -> Unit)? = null - private var onFrameCaptured: ((String) -> Unit)? = null - private var outputSurfaceWidth: Int = 0 - private var outputSurfaceHeight: Int = 0 - private var requestedCaptureLabelForTest: String? = null - - init { - setEGLContextClientVersion(2) - preserveEGLContextOnPause = true - setRenderer(renderer) - renderMode = RENDERMODE_CONTINUOUSLY - } - - fun bind( - player: Player?, - ruleSet: ToneMappingRuleSet, - signalDescriptor: VideoSignalDescriptor?, - ) { - queueEvent { - renderer.updateConfig(ruleSet, signalDescriptor) - } - if (currentPlayer !== player) { - detachPlayerSurface() - currentPlayer = player - attachPlayerSurfaceIfReady() - } - requestRender() - } - - fun setOnVideoSurfaceReadyChanged(listener: ((Boolean) -> Unit)?) { - onVideoSurfaceReadyChanged = listener - listener?.invoke( - isDecoderVideoSurfaceReady(renderer.outputSurface, outputSurfaceWidth, outputSurfaceHeight), - ) - } - - fun decoderOutputSurface(): Surface? = renderer.outputSurface - - fun decoderOutputSurfaceTexture(): SurfaceTexture? = renderer.outputSurfaceTexture() - - fun decoderOutputSurfaceWidth(): Int = outputSurfaceWidth - - fun decoderOutputSurfaceHeight(): Int = outputSurfaceHeight - - fun captureNextRenderedFrame(label: String) { - requestedCaptureLabelForTest = label - Log.i("GLVideoSurfaceView", "Queued GL frame capture for label=$label") - queueEvent { - renderer.requestFrameCapture(label) - } - requestRender() - } - - fun setOnFrameCaptured(listener: ((String) -> Unit)?) { - onFrameCaptured = listener - } - - internal fun pendingCaptureLabelForTest(): String? = - renderer.pendingCaptureLabelForTest() ?: requestedCaptureLabelForTest - - override fun onDetachedFromWindow() { - detachPlayerSurface() - queueEvent { - renderer.releaseResources() - } - super.onDetachedFromWindow() - } - - private fun attachPlayerSurfaceIfReady() { - val player = currentPlayer ?: return - val surface = renderer.outputSurface ?: return - if (!isDecoderVideoSurfaceReady(surface, outputSurfaceWidth, outputSurfaceHeight)) { - Log.i( - "GLVideoSurfaceView", - "Deferring player surface attach until decoder surface has a valid size " + - "surface=${surface.hashCode()} size=${outputSurfaceWidth}x${outputSurfaceHeight}", - ) - return - } - if (attachedPlayer === player && attachedSurface === surface) { - return - } - detachPlayerSurface() - attachedPlayer = player - attachedSurface = surface - Log.i( - "GLVideoSurfaceView", - "Attaching player surface surface=${surface.hashCode()} size=${outputSurfaceWidth}x${outputSurfaceHeight}", - ) - player.setVideoSurface(surface) - } - - private fun detachPlayerSurface() { - val player = attachedPlayer - val surface = attachedSurface - if (player != null && surface != null) { - player.clearVideoSurface(surface) - } - attachedPlayer = null - attachedSurface = null - } - - private inner class ToneMappingRenderer : - Renderer, - SurfaceTexture.OnFrameAvailableListener { - private val frameAvailable = AtomicBoolean(false) - private val textureTransform = FloatArray(16) - private val vertexBuffer: FloatBuffer = ByteBuffer - .allocateDirect(VERTEX_DATA.size * Float.SIZE_BYTES) - .order(ByteOrder.nativeOrder()) - .asFloatBuffer() - .apply { - put(VERTEX_DATA) - position(0) - } - - var outputSurface: Surface? = null - private set - - private var surfaceTexture: SurfaceTexture? = null - private var programId: Int = 0 - private var textureId: Int = 0 - private var positionHandle: Int = 0 - private var texCoordHandle: Int = 0 - private var textureMatrixHandle: Int = 0 - private var samplerHandle: Int = 0 - private var enabledHandle: Int = 0 - private var exposureHandle: Int = 0 - private var contrastHandle: Int = 0 - private var saturationHandle: Int = 0 - private var highlightCompressionHandle: Int = 0 - private var shaderConfig: ShaderConfig = ShaderConfig() - private var pendingCaptureLabel: String? = null - private var hasRenderedVideoFrame: Boolean = false - private var waitingForFirstFrameCaptureLabel: String? = null - - override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) { - val glVersion = GLES20.glGetString(GLES20.GL_VERSION).orEmpty() - val glVendor = GLES20.glGetString(GLES20.GL_VENDOR).orEmpty() - val glRenderer = GLES20.glGetString(GLES20.GL_RENDERER).orEmpty() - val glExtensions = GLES20.glGetString(GLES20.GL_EXTENSIONS).orEmpty() - Log.i( - "GLVideoSurfaceView", - "GL context created version=$glVersion vendor=$glVendor renderer=$glRenderer " + - "extYuvTarget=${glExtensions.contains("GL_EXT_YUV_target")} " + - "ext10Bit=${glExtensions.contains("GL_EXT_texture_norm16")} " + - "extBT2020=${glExtensions.contains("GL_EXT_gl_colorspace_bt2020_pq")}", - ) - programId = createProgram(VERTEX_SHADER, FRAGMENT_SHADER) - textureId = createExternalTexture() - positionHandle = GLES20.glGetAttribLocation(programId, "aPosition") - texCoordHandle = GLES20.glGetAttribLocation(programId, "aTexCoord") - textureMatrixHandle = GLES20.glGetUniformLocation(programId, "uTexMatrix") - samplerHandle = GLES20.glGetUniformLocation(programId, "uTexture") - enabledHandle = GLES20.glGetUniformLocation(programId, "uEnabled") - exposureHandle = GLES20.glGetUniformLocation(programId, "uExposure") - contrastHandle = GLES20.glGetUniformLocation(programId, "uContrast") - saturationHandle = GLES20.glGetUniformLocation(programId, "uSaturation") - highlightCompressionHandle = GLES20.glGetUniformLocation(programId, "uHighlightCompression") - surfaceTexture = SurfaceTexture(textureId).also { - it.setOnFrameAvailableListener(this) - } - outputSurface = Surface(surfaceTexture) - outputSurfaceWidth = 0 - outputSurfaceHeight = 0 - Log.i("GLVideoSurfaceView", "Surface created for experimental GL output") - post { - onVideoSurfaceReadyChanged?.invoke( - isDecoderVideoSurfaceReady(outputSurface, outputSurfaceWidth, outputSurfaceHeight), - ) - } - } - - override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) { - outputSurfaceWidth = width - outputSurfaceHeight = height - surfaceTexture?.setDefaultBufferSize(width, height) - Log.i( - "GLVideoSurfaceView", - "Surface changed for experimental GL output size=${width}x${height}", - ) - GLES20.glViewport(0, 0, width, height) - post { - attachPlayerSurfaceIfReady() - onVideoSurfaceReadyChanged?.invoke( - isDecoderVideoSurfaceReady(outputSurface, outputSurfaceWidth, outputSurfaceHeight), - ) - } - } - - override fun onDrawFrame(gl: GL10?) { - GLES20.glClearColor(0f, 0f, 0f, 1f) - GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) - - val localSurfaceTexture = surfaceTexture ?: return - if (frameAvailable.compareAndSet(true, false)) { - localSurfaceTexture.updateTexImage() - localSurfaceTexture.getTransformMatrix(textureTransform) - hasRenderedVideoFrame = true - } - - GLES20.glUseProgram(programId) - vertexBuffer.position(0) - GLES20.glVertexAttribPointer( - positionHandle, - 2, - GLES20.GL_FLOAT, - false, - STRIDE_BYTES, - vertexBuffer, - ) - GLES20.glEnableVertexAttribArray(positionHandle) - - vertexBuffer.position(2) - GLES20.glVertexAttribPointer( - texCoordHandle, - 2, - GLES20.GL_FLOAT, - false, - STRIDE_BYTES, - vertexBuffer, - ) - GLES20.glEnableVertexAttribArray(texCoordHandle) - - GLES20.glUniformMatrix4fv(textureMatrixHandle, 1, false, textureTransform, 0) - GLES20.glUniform1i(samplerHandle, 0) - GLES20.glUniform1f(enabledHandle, if (shaderConfig.enabled) 1f else 0f) - GLES20.glUniform1f(exposureHandle, shaderConfig.exposure) - GLES20.glUniform1f(contrastHandle, shaderConfig.contrast) - GLES20.glUniform1f(saturationHandle, shaderConfig.saturation) - GLES20.glUniform1f(highlightCompressionHandle, shaderConfig.highlightCompression) - - GLES20.glActiveTexture(GLES20.GL_TEXTURE0) - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId) - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) - captureFrameIfRequested() - } - - override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) { - frameAvailable.set(true) - if (!hasRenderedVideoFrame) { - Log.i("GLVideoSurfaceView", "Received first video frame from SurfaceTexture") - } - requestRender() - } - - fun updateConfig( - ruleSet: ToneMappingRuleSet, - signalDescriptor: VideoSignalDescriptor?, - ) { - shaderConfig = ShaderConfig.from(ruleSet, signalDescriptor) - } - - fun requestFrameCapture(label: String) { - pendingCaptureLabel = label - } - - fun pendingCaptureLabelForTest(): String? = pendingCaptureLabel - - fun outputSurfaceTexture(): SurfaceTexture? = surfaceTexture - - fun releaseResources() { - outputSurface?.let { surface -> - post { - if (attachedSurface === surface) { - detachPlayerSurface() - } - onVideoSurfaceReadyChanged?.invoke(false) - } - surface.release() - } - outputSurface = null - outputSurfaceWidth = 0 - outputSurfaceHeight = 0 - surfaceTexture?.release() - surfaceTexture = null - if (textureId != 0) { - GLES20.glDeleteTextures(1, intArrayOf(textureId), 0) - textureId = 0 - } - if (programId != 0) { - GLES20.glDeleteProgram(programId) - programId = 0 - } - } - - private fun captureFrameIfRequested() { - val label = pendingCaptureLabel ?: return - if (!hasRenderedVideoFrame) { - if (waitingForFirstFrameCaptureLabel != label) { - waitingForFirstFrameCaptureLabel = label - Log.i("GLVideoSurfaceView", "Waiting for first video frame before capturing label=$label") - } - return - } - waitingForFirstFrameCaptureLabel = null - pendingCaptureLabel = null - try { - Log.i("GLVideoSurfaceView", "Starting GL frame capture for label=$label") - val width = width - val height = height - if (width <= 0 || height <= 0) { - Log.w("GLVideoSurfaceView", "Skipping GL frame capture for $label because size is ${width}x${height}") - MiruLog.w( - "GLVideoSurfaceView", - "Skipping GL frame capture because surface size is invalid", - attributes = mapOf( - "label" to label, - "width" to width.toString(), - "height" to height.toString(), - ), - ) - return - } - val pixelBuffer = ByteBuffer.allocateDirect(width * height * 4).order(ByteOrder.nativeOrder()) - GLES20.glReadPixels( - 0, - 0, - width, - height, - GLES20.GL_RGBA, - GLES20.GL_UNSIGNED_BYTE, - pixelBuffer, - ) - pixelBuffer.rewind() - val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - bitmap.copyPixelsFromBuffer(pixelBuffer) - val flipped = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - for (y in 0 until height) { - for (x in 0 until width) { - flipped.setPixel(x, height - y - 1, bitmap.getPixel(x, y)) - } - } - bitmap.recycle() - val outputDir = File( - this@GLVideoSurfaceView.context.filesDir, - "MiruPlayGlCaptures", - ) - if (!outputDir.exists()) { - outputDir.mkdirs() - } - val outputFile = File(outputDir, "${sanitizeFileName(label)}.png") - FileOutputStream(outputFile).use { stream -> - flipped.compress(Bitmap.CompressFormat.PNG, 100, stream) - } - flipped.recycle() - Log.i( - "GLVideoSurfaceView", - "Captured GL frame label=$label path=${outputFile.absolutePath} size=${width}x${height}", - ) - post { - onFrameCaptured?.invoke(label) - } - MiruLog.i( - "GLVideoSurfaceView", - "Captured GL frame", - mapOf( - "label" to label, - "path" to outputFile.absolutePath, - "width" to width.toString(), - "height" to height.toString(), - ), - ) - } catch (error: Throwable) { - Log.e("GLVideoSurfaceView", "Failed to capture GL frame for $label", error) - MiruLog.e( - "GLVideoSurfaceView", - "Failed to capture GL frame", - error, - mapOf("label" to label), - ) - } - } - } - - private data class ShaderConfig( - val enabled: Boolean = false, - val exposure: Float = 1f, - val contrast: Float = 1f, - val saturation: Float = 1f, - val highlightCompression: Float = 0f, - ) { - companion object { - fun from( - ruleSet: ToneMappingRuleSet, - signalDescriptor: VideoSignalDescriptor?, - ): ShaderConfig { - val hdrFactor = when (signalDescriptor?.signalKind) { - VideoSignalKind.HDR10_PLUS -> 1.08f - VideoSignalKind.HDR10 -> 1.03f - VideoSignalKind.DOLBY_VISION -> 1.02f - VideoSignalKind.UNKNOWN_HDR -> 1.01f - else -> 1f - } - val curveBias = when (ruleSet.curvePreset) { - ToneMappingCurvePreset.PASSTHROUGH -> 1f - ToneMappingCurvePreset.MOBIUS -> 1.02f - ToneMappingCurvePreset.REINHARD -> 0.98f - } - return ShaderConfig( - enabled = ruleSet.enabled, - exposure = (ruleSet.targetSdrNits / 120f).coerceIn(0.75f, 1.35f) * hdrFactor * curveBias, - contrast = (1f + ruleSet.contrastRecovery / 40f).coerceIn(0.8f, 1.8f), - saturation = (1f + ruleSet.saturationRecovery / 45f).coerceIn(0.75f, 1.8f), - highlightCompression = (ruleSet.highlightCompression / 100f).coerceIn(0f, 0.6f), - ) - } - } - } - - companion object { - private const val STRIDE_BYTES = 4 * Float.SIZE_BYTES - private val VERTEX_DATA = floatArrayOf( - -1f, -1f, 0f, 0f, - 1f, -1f, 1f, 0f, - -1f, 1f, 0f, 1f, - 1f, 1f, 1f, 1f, - ) - - private const val VERTEX_SHADER = """ - attribute vec4 aPosition; - attribute vec2 aTexCoord; - uniform mat4 uTexMatrix; - varying vec2 vTexCoord; - void main() { - gl_Position = aPosition; - vec4 transformed = uTexMatrix * vec4(aTexCoord, 0.0, 1.0); - vTexCoord = transformed.xy; - } - """ - - private const val FRAGMENT_SHADER = """ - #extension GL_OES_EGL_image_external : require - precision mediump float; - uniform samplerExternalOES uTexture; - uniform float uEnabled; - uniform float uExposure; - uniform float uContrast; - uniform float uSaturation; - uniform float uHighlightCompression; - varying vec2 vTexCoord; - - vec3 applyToneMapping(vec3 color) { - color = max(color * uExposure, vec3(0.0)); - float luma = dot(color, vec3(0.2627, 0.6780, 0.0593)); - float shoulderStart = 0.60; - float shoulder = max(luma - shoulderStart, 0.0); - float compressedLuma = luma / (1.0 + shoulder * uHighlightCompression * 2.0); - color *= (compressedLuma + 0.0001) / max(luma, 0.0001); - color = (color - 0.5) * uContrast + 0.5; - float gray = dot(color, vec3(0.299, 0.587, 0.114)); - color = mix(vec3(gray), color, uSaturation); - return clamp(color, 0.0, 1.0); - } - - void main() { - vec4 sampled = texture2D(uTexture, vTexCoord); - vec3 color = sampled.rgb; - if (uEnabled > 0.5) { - color = applyToneMapping(color); - } - gl_FragColor = vec4(color, sampled.a); - } - """ - - private fun createExternalTexture(): Int { - val textures = IntArray(1) - GLES20.glGenTextures(1, textures, 0) - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0]) - GLES20.glTexParameterf( - GLES11Ext.GL_TEXTURE_EXTERNAL_OES, - GLES20.GL_TEXTURE_MIN_FILTER, - GLES20.GL_LINEAR.toFloat(), - ) - GLES20.glTexParameterf( - GLES11Ext.GL_TEXTURE_EXTERNAL_OES, - GLES20.GL_TEXTURE_MAG_FILTER, - GLES20.GL_LINEAR.toFloat(), - ) - GLES20.glTexParameteri( - GLES11Ext.GL_TEXTURE_EXTERNAL_OES, - GLES20.GL_TEXTURE_WRAP_S, - GLES20.GL_CLAMP_TO_EDGE, - ) - GLES20.glTexParameteri( - GLES11Ext.GL_TEXTURE_EXTERNAL_OES, - GLES20.GL_TEXTURE_WRAP_T, - GLES20.GL_CLAMP_TO_EDGE, - ) - return textures[0] - } - - private fun createProgram(vertexShaderSource: String, fragmentShaderSource: String): Int { - val vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, vertexShaderSource) - val fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderSource) - return GLES20.glCreateProgram().also { program -> - GLES20.glAttachShader(program, vertexShader) - GLES20.glAttachShader(program, fragmentShader) - GLES20.glLinkProgram(program) - val linkStatus = IntArray(1) - GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0) - if (linkStatus[0] == 0) { - val error = GLES20.glGetProgramInfoLog(program) - GLES20.glDeleteProgram(program) - error("Failed to link GL program: $error") - } - GLES20.glDeleteShader(vertexShader) - GLES20.glDeleteShader(fragmentShader) - } - } - - private fun compileShader(type: Int, source: String): Int { - return GLES20.glCreateShader(type).also { shader -> - GLES20.glShaderSource(shader, source) - GLES20.glCompileShader(shader) - val compileStatus = IntArray(1) - GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0) - if (compileStatus[0] == 0) { - val error = GLES20.glGetShaderInfoLog(shader) - GLES20.glDeleteShader(shader) - error("Failed to compile shader: $error") - } - } - } - - private fun sanitizeFileName(value: String): String = - value.replace(Regex("""[^A-Za-z0-9._-]"""), "_") - } -} diff --git a/ui-tv/src/main/kotlin/com/miruplay/tv/ui/player/PlayerScreen.kt b/ui-tv/src/main/kotlin/com/miruplay/tv/ui/player/PlayerScreen.kt index de81f4a4..208e3659 100644 --- a/ui-tv/src/main/kotlin/com/miruplay/tv/ui/player/PlayerScreen.kt +++ b/ui-tv/src/main/kotlin/com/miruplay/tv/ui/player/PlayerScreen.kt @@ -160,8 +160,6 @@ import com.miruplay.tv.ui.components.toMiruPlayInputIntent import com.miruplay.tv.ui.components.tvActivateKeyEvent import com.miruplay.tv.player.AudioTrack import com.miruplay.tv.player.LibassSubtitleSurfaceView -import com.miruplay.tv.player.resolveDeviceGlEsMajorVersion -import com.miruplay.tv.player.shouldUseDedicatedExperimentalGlSurface import com.miruplay.tv.ui.theme.AnimeRed import com.miruplay.tv.ui.theme.DarkSurface import com.miruplay.tv.ui.theme.FocusBorder @@ -307,7 +305,6 @@ private fun PlayerScreenContent( val displaySubtitle by viewModel.displaySubtitle.collectAsStateWithLifecycle() val currentVideoSignalDescriptor by viewModel.currentVideoSignalDescriptor.collectAsStateWithLifecycle() val currentRenderRuleKey by viewModel.currentRenderRuleKey.collectAsStateWithLifecycle() - val currentToneMappingRuleSet by viewModel.currentToneMappingRuleSet.collectAsStateWithLifecycle() val currentRequestedBackend by viewModel.currentRequestedBackend.collectAsStateWithLifecycle() val currentActiveBackend by viewModel.currentActiveBackend.collectAsStateWithLifecycle() val fallbackReason by viewModel.fallbackReason.collectAsStateWithLifecycle() @@ -315,9 +312,6 @@ private fun PlayerScreenContent( val subtitleBackgroundTransparent by viewModel.subtitleBackgroundTransparent.collectAsStateWithLifecycle() val keepScreenOn = playbackState.keepsScreenOn() val view = LocalView.current - val deviceGlEsMajorVersion = remember(view.context) { - resolveDeviceGlEsMajorVersion(view.context) - } val playerFocusRequester = remember { FocusRequester() } val timelineFocusRequester = remember { FocusRequester() } val transportFocusRequester = remember { FocusRequester() } @@ -338,46 +332,8 @@ private fun PlayerScreenContent( } val screenOwnerToken = remember(playbackSource) { Any() } var hasStartedPlayback by remember(playbackSource) { mutableStateOf(false) } - var preferDedicatedExperimentalSurface by remember(playbackSource) { - mutableStateOf( - latchDedicatedExperimentalSurfaceForPlaybackSession( - wasAlreadyLatched = false, - deviceGlEsMajorVersion = deviceGlEsMajorVersion, - activeBackend = currentActiveBackend, - requestedBackend = currentRequestedBackend, - defaultBackend = formatAwarePreferences.defaultBackend, - ), - ) - } - LaunchedEffect( - currentActiveBackend, - currentRequestedBackend, - formatAwarePreferences.defaultBackend, - ) { - preferDedicatedExperimentalSurface = latchDedicatedExperimentalSurfaceForPlaybackSession( - wasAlreadyLatched = preferDedicatedExperimentalSurface, - deviceGlEsMajorVersion = deviceGlEsMajorVersion, - activeBackend = currentActiveBackend, - requestedBackend = currentRequestedBackend, - defaultBackend = formatAwarePreferences.defaultBackend, - ) - } - val playerViewHost = remember( - currentActiveBackend, - currentRequestedBackend, - hasStartedPlayback, - formatAwarePreferences.defaultBackend, - preferCapturableTextureView, - preferDedicatedExperimentalSurface, - ) { - resolvePlayerViewHost( - activeBackend = currentActiveBackend, - requestedBackend = currentRequestedBackend, - hasStartedPlayback = hasStartedPlayback, - defaultBackend = formatAwarePreferences.defaultBackend, - preferCapturableTextureView = preferCapturableTextureView, - preferDedicatedGlSurface = preferDedicatedExperimentalSurface, - ) + val playerViewHost = remember(preferCapturableTextureView) { + resolvePlayerViewHost(preferCapturableTextureView) } val context = LocalContext.current var openMenu by remember { mutableStateOf(null) } @@ -390,7 +346,6 @@ private fun PlayerScreenContent( var lastStandardDebugCaptureAttempt by remember(playbackSource) { mutableStateOf(null) } - val shouldShowExperimentalSurface = playerViewHost == PlayerViewHost.DedicatedGlSurface val shouldCaptureStandardDebugFrame = shouldScheduleStandardDebugCapture( pendingLabel = viewModel.pendingGlFrameCaptureLabel(), playbackState = playbackState, @@ -491,15 +446,10 @@ private fun PlayerScreenContent( } LaunchedEffect( - shouldShowExperimentalSurface, playbackState, currentPosition, playerViewRef, ) { - if (shouldShowExperimentalSurface) { - playerViewRef = null - return@LaunchedEffect - } val pendingLabel = viewModel.pendingGlFrameCaptureLabel() ?: return@LaunchedEffect if (!shouldCaptureStandardDebugFrame) { return@LaunchedEffect @@ -556,11 +506,10 @@ private fun PlayerScreenContent( playerViewHost, currentActiveBackend, currentRequestedBackend, - shouldShowExperimentalSurface, ) { Log.i( "PlayerScreen", - "Resolved video host host=$playerViewHost active=$currentActiveBackend requested=$currentRequestedBackend experimental=$shouldShowExperimentalSurface", + "Resolved video host host=$playerViewHost active=$currentActiveBackend requested=$currentRequestedBackend", ) } @@ -630,56 +579,7 @@ private fun PlayerScreenContent( val player = viewModel.getPlayer() val displayPlayer = remember(player) { player?.let(::SubtitleTransformingPlayer) } val usesNativeVideoHost = viewModel.usesVlcVideoLayout() - if (shouldShowExperimentalSurface) { - AndroidView( - factory = { context -> - Log.i( - "PlayerScreen", - "Creating GLVideoSurfaceView host active=$currentActiveBackend requested=$currentRequestedBackend hasPlayer=${player != null}", - ) - GLVideoSurfaceView(context).apply { - isClickable = true - isFocusable = false - isFocusableInTouchMode = false - setOnClickListener { viewModel.showControls() } - setOnFrameCaptured { label -> - viewModel.clearPendingGlFrameCaptureLabel(label) - } - bind( - player = player, - ruleSet = currentToneMappingRuleSet, - signalDescriptor = currentVideoSignalDescriptor, - ) - if (shouldCaptureStandardDebugFrame) { - viewModel.pendingGlFrameCaptureLabel()?.let { label -> - post { captureNextRenderedFrame(label) } - } - } - } - }, - update = { glView -> - Log.i( - "PlayerScreen", - "Updating GLVideoSurfaceView host active=$currentActiveBackend requested=$currentRequestedBackend hasPlayer=${player != null}", - ) - glView.setOnClickListener { viewModel.showControls() } - glView.setOnFrameCaptured { label -> - viewModel.clearPendingGlFrameCaptureLabel(label) - } - glView.bind( - player = player, - ruleSet = currentToneMappingRuleSet, - signalDescriptor = currentVideoSignalDescriptor, - ) - if (shouldCaptureStandardDebugFrame) { - viewModel.pendingGlFrameCaptureLabel()?.let { label -> - glView.post { glView.captureNextRenderedFrame(label) } - } - } - }, - modifier = Modifier.fillMaxSize(), - ) - } else if (usesNativeVideoHost) { + if (usesNativeVideoHost) { AndroidView( factory = { context -> FrameLayout(context).apply { @@ -910,55 +810,16 @@ internal fun shouldScheduleStandardDebugCapture( internal enum class PlayerViewHost(@LayoutRes val layoutResId: Int) { SurfaceView(R.layout.player_view_surface), SurfaceTextureView(R.layout.player_view_texture), - DedicatedGlSurface(0), } -internal fun resolvePlayerViewHost( - activeBackend: PlaybackRenderBackend, - requestedBackend: PlaybackRenderBackend, - hasStartedPlayback: Boolean = true, - defaultBackend: PlaybackRenderBackend = PlaybackRenderBackend.STANDARD_EXO, - preferCapturableTextureView: Boolean = false, - preferDedicatedGlSurface: Boolean = false, -): PlayerViewHost = - if (preferDedicatedGlSurface) { - PlayerViewHost.DedicatedGlSurface - } else if ( - !hasStartedPlayback && - defaultBackend == PlaybackRenderBackend.EXPERIMENTAL_GL - ) { - PlayerViewHost.DedicatedGlSurface - } else if ( - preferCapturableTextureView || - activeBackend == PlaybackRenderBackend.EXPERIMENTAL_GL || - requestedBackend == PlaybackRenderBackend.EXPERIMENTAL_GL - ) { - PlayerViewHost.SurfaceTextureView - } else { - PlayerViewHost.SurfaceView - } +internal fun resolvePlayerViewHost(preferCapturableTextureView: Boolean): PlayerViewHost = + if (preferCapturableTextureView) PlayerViewHost.SurfaceTextureView else PlayerViewHost.SurfaceView internal fun latchCapturableTextureViewForPlaybackSession( wasAlreadyLatched: Boolean, pendingLabel: String?, ): Boolean = wasAlreadyLatched || !pendingLabel.isNullOrBlank() -internal fun latchDedicatedExperimentalSurfaceForPlaybackSession( - wasAlreadyLatched: Boolean, - deviceGlEsMajorVersion: Int, - activeBackend: PlaybackRenderBackend, - requestedBackend: PlaybackRenderBackend, - defaultBackend: PlaybackRenderBackend, -): Boolean = - wasAlreadyLatched || ( - shouldUseDedicatedExperimentalGlSurface(deviceGlEsMajorVersion) && - ( - activeBackend == PlaybackRenderBackend.EXPERIMENTAL_GL || - requestedBackend == PlaybackRenderBackend.EXPERIMENTAL_GL || - defaultBackend == PlaybackRenderBackend.EXPERIMENTAL_GL - ) - ) - private tailrec fun Context.findActivity(): Activity? = when (this) { is Activity -> this is ContextWrapper -> baseContext.findActivity() diff --git a/ui-tv/src/main/kotlin/com/miruplay/tv/ui/settings/AddSourceScreen.kt b/ui-tv/src/main/kotlin/com/miruplay/tv/ui/settings/AddSourceScreen.kt index e1294d37..afb5774f 100644 --- a/ui-tv/src/main/kotlin/com/miruplay/tv/ui/settings/AddSourceScreen.kt +++ b/ui-tv/src/main/kotlin/com/miruplay/tv/ui/settings/AddSourceScreen.kt @@ -3366,7 +3366,7 @@ private fun PlaybackPanel( supportedPlaybackRenderBackends().forEach { backend -> ScanOptionChip( text = playbackBackendLabel(backend), - icon = if (backend == PlaybackRenderBackend.EXPERIMENTAL_GL) Icons.Filled.PhotoFilter else Icons.Filled.PlayArrow, + icon = Icons.Filled.PlayArrow, selected = formatAwareToneMappingPreferences.defaultBackend.normalizeSupportedBackend() == backend, enabled = true, onClick = { onPlaybackBackendSelected(backend) }, diff --git a/ui-tv/src/test/kotlin/com/miruplay/tv/ui/player/GLVideoSurfaceViewAttachmentPolicyTest.kt b/ui-tv/src/test/kotlin/com/miruplay/tv/ui/player/GLVideoSurfaceViewAttachmentPolicyTest.kt deleted file mode 100644 index c92d27cf..00000000 --- a/ui-tv/src/test/kotlin/com/miruplay/tv/ui/player/GLVideoSurfaceViewAttachmentPolicyTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.miruplay.tv.ui.player - -import android.view.Surface -import io.mockk.mockk -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test - -class GLVideoSurfaceViewAttachmentPolicyTest { - @Test - fun `decoder surface is not ready before minimum dimensions are reached`() { - val surface = mockk() - - assertFalse(isDecoderVideoSurfaceReady(surface, width = 1, height = 1080)) - assertFalse(isDecoderVideoSurfaceReady(surface, width = 1920, height = 1)) - assertFalse(isDecoderVideoSurfaceReady(surface, width = 3, height = 3)) - } - - @Test - fun `decoder surface becomes ready once width and height reach minimum size`() { - val surface = mockk() - - assertTrue(isDecoderVideoSurfaceReady(surface, width = 4, height = 4)) - assertTrue(isDecoderVideoSurfaceReady(surface, width = 1920, height = 1080)) - } - - @Test - fun `decoder surface is not ready without a surface instance`() { - assertFalse(isDecoderVideoSurfaceReady(surface = null, width = 1920, height = 1080)) - } -} diff --git a/web-control/src/main/kotlin/com/miruplay/tv/webcontrol/PlaybackDebugConfigMapping.kt b/web-control/src/main/kotlin/com/miruplay/tv/webcontrol/PlaybackDebugConfigMapping.kt index 0e71cfd4..c807d2e8 100644 --- a/web-control/src/main/kotlin/com/miruplay/tv/webcontrol/PlaybackDebugConfigMapping.kt +++ b/web-control/src/main/kotlin/com/miruplay/tv/webcontrol/PlaybackDebugConfigMapping.kt @@ -10,7 +10,6 @@ import com.miruplay.tv.player.LibVlcVoutMode internal fun playbackRenderBackendFromDebugValue(value: String?): PlaybackRenderBackend? = when (value.debugKey()) { "standardexo", "standard", "exo", "media3" -> PlaybackRenderBackend.STANDARD_EXO - "experimentalgl", "gl" -> PlaybackRenderBackend.EXPERIMENTAL_GL "experimentalmpvandroid", "mpvandroid" -> PlaybackRenderBackend.EXPERIMENTAL_MPV_EMBEDDED "experimentalmpvembedded", "mpvembedded", "embeddedmpv", "libmpv", "mpv" -> PlaybackRenderBackend.EXPERIMENTAL_MPV_EMBEDDED "experimentalijkplayer", "ijkplayer", "ijk" -> PlaybackRenderBackend.EXPERIMENTAL_IJKPLAYER diff --git a/web-control/src/test/kotlin/com/miruplay/tv/webcontrol/PlaybackDebugConfigMappingTest.kt b/web-control/src/test/kotlin/com/miruplay/tv/webcontrol/PlaybackDebugConfigMappingTest.kt index 0e07d103..f4581eb1 100644 --- a/web-control/src/test/kotlin/com/miruplay/tv/webcontrol/PlaybackDebugConfigMappingTest.kt +++ b/web-control/src/test/kotlin/com/miruplay/tv/webcontrol/PlaybackDebugConfigMappingTest.kt @@ -17,7 +17,7 @@ class PlaybackDebugConfigMappingTest { fun `backend parser accepts WebAPI friendly aliases`() { assertEquals(PlaybackRenderBackend.STANDARD_EXO, playbackRenderBackendFromDebugValue("exo")) assertEquals(PlaybackRenderBackend.EXPERIMENTAL_LIBVLC, playbackRenderBackendFromDebugValue("lib-vlc")) - assertEquals(PlaybackRenderBackend.EXPERIMENTAL_GL, playbackRenderBackendFromDebugValue("experimental_gl")) + assertNull(playbackRenderBackendFromDebugValue("experimental_gl")) assertEquals(PlaybackRenderBackend.EXPERIMENTAL_MPV_EMBEDDED, playbackRenderBackendFromDebugValue("mpvandroid")) assertEquals(PlaybackRenderBackend.EXPERIMENTAL_MPV_EMBEDDED, playbackRenderBackendFromDebugValue("libmpv")) assertEquals(PlaybackRenderBackend.EXPERIMENTAL_MPV_EMBEDDED, playbackRenderBackendFromDebugValue("mpv"))