diff --git a/android/app/src/androidTestDebug/kotlin/com/edde746/plezy/mpv/MpvLogLevelDeviceTest.kt b/android/app/src/androidTestDebug/kotlin/com/edde746/plezy/mpv/MpvLogLevelDeviceTest.kt new file mode 100644 index 000000000..d57aeb6f5 --- /dev/null +++ b/android/app/src/androidTestDebug/kotlin/com/edde746/plezy/mpv/MpvLogLevelDeviceTest.kt @@ -0,0 +1,146 @@ +package com.edde746.plezy.mpv + +import android.app.Instrumentation +import android.os.SystemClock +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.edde746.plezy.shared.PlayerDelegate +import java.io.File +import java.util.UUID +import java.util.concurrent.CountDownLatch +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MpvLogLevelDeviceTest { + @Test + fun normalLoggingPreservesErrorsAndCanToggleVerboseOffAgain() = withCore { instrumentation, core, logs -> + assertLogPhase(instrumentation, core, logs, "normal", expectInfo = false) + setLogLevel(instrumentation, core, "v") + assertLogPhase(instrumentation, core, logs, "verbose", expectInfo = true) + setLogLevel(instrumentation, core, "warn") + assertLogPhase(instrumentation, core, logs, "normal-again", expectInfo = false) + } + + @Test + fun verboseLoggingCanBeSelectedBeforeNativeInitialization() = withCore(initialLogLevel = "v") { instrumentation, core, logs -> + assertLogPhase(instrumentation, core, logs, "initial-verbose", expectInfo = true) + } + + @Test + fun rejectedInitialLogLevelDoesNotBlockTheNextPlayer() { + withCore(initialLogLevel = "not-a-level", initializationSucceeds = false) { _, _, _ -> } + withCore { instrumentation, core, logs -> + assertLogPhase(instrumentation, core, logs, "after-rejected-init", expectInfo = false) + } + } + + private fun withCore( + initialLogLevel: String = "warn", + initializationSucceeds: Boolean = true, + block: (Instrumentation, MpvPlayerCore, LinkedBlockingQueue>) -> Unit + ) { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val logs = LinkedBlockingQueue>() + val initialized = CountDownLatch(1) + val success = AtomicReference() + val core = AtomicReference() + instrumentation.runOnMainSync { + core.set(MpvPlayerCore(instrumentation.targetContext, audioOnly = true, initialLogLevel = initialLogLevel)) + core.get().delegate = object : PlayerDelegate { + override fun onPropertyChange(name: String, value: Any?) = Unit + + override fun onEvent(name: String, data: Map?) { + if (name == "log-message") { + logs.add((data?.get("level") as? String ?: "") to (data?.get("text") as? String ?: "")) + } + } + } + core.get().initialize { + success.set(it) + initialized.countDown() + } + } + try { + assertCompletes(initialized, "initialization") + assertEquals("Native MPV initialization result", initializationSucceeds, success.get()) + block(instrumentation, core.get(), logs) + } finally { + val disposed = CountDownLatch(1) + instrumentation.runOnMainSync { core.get().dispose(disposed::countDown) } + assertCompletes(disposed, "teardown") + } + } + + private fun setLogLevel(instrumentation: Instrumentation, core: MpvPlayerCore, level: String) { + val completed = CountDownLatch(1) + val result = AtomicReference>() + instrumentation.runOnMainSync { + core.setLogLevel(level) { + result.set(it) + completed.countDown() + } + } + assertCompletes(completed, "setLogLevel($level)") + result.get().getOrThrow() + } + + private fun command(instrumentation: Instrumentation, core: MpvPlayerCore, vararg args: String) { + val completed = CountDownLatch(1) + val success = AtomicReference() + instrumentation.runOnMainSync { + core.command(arrayOf(*args)) { + success.set(it) + completed.countDown() + } + } + assertCompletes(completed, args.first()) + assertTrue("MPV command failed: ${args.first()}", success.get()) + } + + private fun assertLogPhase( + instrumentation: Instrumentation, + core: MpvPlayerCore, + logs: LinkedBlockingQueue>, + phase: String, + expectInfo: Boolean + ) { + val token = "mpv-log-$phase-${UUID.randomUUID()}" + val infoMarker = "$token-info" + val errorMarker = "$token-missing" + val missingFile = File(instrumentation.targetContext.cacheDir, errorMarker) + command(instrumentation, core, "print-text", infoMarker) + command(instrumentation, core, "loadfile", missingFile.absolutePath, "replace") + + // The failed open is an error-level barrier in the same ordered log stream. + // Seeing it proves the preceding informational message was either delivered + // or filtered; no sleep is needed to assert that a quiet log stayed quiet. + val deadline = SystemClock.elapsedRealtime() + TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS) + var sawInfo = false + while (true) { + val remaining = deadline - SystemClock.elapsedRealtime() + assertTrue("Missing native error log in $phase", remaining > 0) + val log = logs.poll(remaining, TimeUnit.MILLISECONDS) + assertTrue("Missing native error log in $phase", log != null) + if (log!!.second.contains(infoMarker)) { + assertEquals("info", log.first) + sawInfo = true + } + if (log.first == "error" && log.second.contains(errorMarker)) break + } + assertEquals("Informational log visibility in $phase", expectInfo, sawInfo) + } + + private fun assertCompletes(latch: CountDownLatch, operation: String) { + assertTrue("Timed out during $operation", latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) + } + + private companion object { + const val TIMEOUT_SECONDS = 15L + } +} diff --git a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt index 101a53e41..a9596b237 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt @@ -46,6 +46,7 @@ class MpvPlayerCore private constructor( private val hardwareDecoding: Boolean, /** Subtitle "Render Resolution" as a fraction of the OSD plane's view size; see [OsdPlanePolicy]. */ private val osdRenderScale: Float, + private val initialLogLevel: String, private val propertyWriterOverride: (suspend (String, String) -> Unit)?, initializedForTesting: Boolean ) : SurfaceHolder.Callback, @@ -54,14 +55,15 @@ class MpvPlayerCore private constructor( context: Context, audioOnly: Boolean = false, hardwareDecoding: Boolean = true, - osdRenderScale: Float = 1f - ) : this(context, audioOnly, hardwareDecoding, osdRenderScale, null, false) + osdRenderScale: Float = 1f, + initialLogLevel: String = "warn" + ) : this(context, audioOnly, hardwareDecoding, osdRenderScale, initialLogLevel, null, false) internal constructor( context: Context, audioOnly: Boolean, propertyWriter: (suspend (String, String) -> Unit)? - ) : this(context, audioOnly, true, 1f, propertyWriter, true) + ) : this(context, audioOnly, true, 1f, "warn", propertyWriter, true) companion object { private const val TAG = "MpvPlayerCore" @@ -425,6 +427,7 @@ class MpvPlayerCore private constructor( (context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager)?.largeMemoryClass ?: 0 ) val p = MpvPlayer.create(context.applicationContext) { + setLogLevel(initialLogLevel) if (audioOnly) { // Pure audio core (all set before mpv_initialize, mirroring the // Windows/Linux audio instances): vid=no keeps embedded cover @@ -1559,6 +1562,35 @@ class MpvPlayerCore private constructor( } } + fun setLogLevel(level: String, onComplete: (Result) -> Unit) { + if (!isInitialized || disposing || !scope.isActive) { + onComplete(Result.failure(CancellationException("MPV core unavailable"))) + return + } + + // ATOMIC starts even if dispose cancels a queued write, so its channel + // result is completed; ensureActive prevents that write reaching JNI. + scope.launch(mpvWriteDispatcher, start = CoroutineStart.ATOMIC) { + val outcome = try { + ensureActive() + val p = player ?: throw CancellationException("MPV player unavailable") + p.setLogLevel(level) + Result.success(Unit) + } catch (error: Exception) { + Result.failure(error) + } + withContext(NonCancellable + Dispatchers.Main) { + onComplete( + if (disposing || !isInitialized) { + Result.failure(CancellationException("MPV core unavailable")) + } else { + outcome + } + ) + } + } + } + fun setProperty(name: String, value: String, onComplete: ((Result) -> Unit)? = null) { if (!isInitialized || disposing || !scope.isActive) { onComplete?.invoke(Result.failure(CancellationException("MPV core unavailable"))) diff --git a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt index ef5e639b1..686195dd5 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerPlugin.kt @@ -210,6 +210,7 @@ open class MpvPlayerPlugin( // ⅓ / ¼ of the surface); the same fraction the ExoPlayer overlay applies. // Absent from older callers and the audio-only core; full is the default. val subtitleRenderScale = call.argument("subtitleRenderScale")?.toFloat() ?: 1f + val logLevel = call.argument("logLevel") ?: "warn" // Video cores need the Activity (surface/view hierarchy); the audio-only // core is built on the application context so it can outlive it. val coreContext: Context? = if (audioOnly) applicationContext else activity @@ -268,7 +269,7 @@ open class MpvPlayerPlugin( } gen = ++sessionGeneration - core = MpvPlayerCore(coreContext, audioOnly, hardwareDecoding, subtitleRenderScale).apply { + core = MpvPlayerCore(coreContext, audioOnly, hardwareDecoding, subtitleRenderScale, logLevel).apply { delegate = this@MpvPlayerPlugin } playerCore = core @@ -483,15 +484,26 @@ open class MpvPlayerPlugin( } private fun handleSetLogLevel(call: MethodCall, result: MethodChannel.Result) { - if (call.argument("level") == null) { - result.error("INVALID_ARGS", "Missing 'level'", null) + val level = call.argument("level") as? String + if (level == null) { + result.error("INVALID_ARGS", "Missing or invalid 'level'", null) return } - result.error( - "UNSUPPORTED", - "Runtime mpv log level changes are not supported on Android", - null - ) + val core = playerCore + if (core?.isInitialized != true) { + completeMpvPropertyNotInitialized(result) + return + } + core.setLogLevel(level) { outcome -> + when (val failure = outcome.exceptionOrNull()) { + null -> result.success(null) + is CancellationException -> completeMpvPropertyNotInitialized(result) + else -> { + Log.w(tag, "MPV rejected log level change", failure) + result.error("SET_LOG_LEVEL_FAILED", "MPV log level change was rejected", null) + } + } + } } private fun handleSetVisible(call: MethodCall, result: MethodChannel.Result) { diff --git a/android/app/src/test/kotlin/com/edde746/plezy/mpv/MpvPlayerPluginTest.kt b/android/app/src/test/kotlin/com/edde746/plezy/mpv/MpvPlayerPluginTest.kt index 591a06fe0..caa2f79a5 100644 --- a/android/app/src/test/kotlin/com/edde746/plezy/mpv/MpvPlayerPluginTest.kt +++ b/android/app/src/test/kotlin/com/edde746/plezy/mpv/MpvPlayerPluginTest.kt @@ -758,15 +758,53 @@ class MpvPlayerPluginTest { } @Test - fun setLogLevelReportsUnsupported() { + fun setLogLevelWithoutCoreReportsNotInitializedForVideoAndAudio() { + for (plugin in listOf(MpvPlayerPlugin(), MpvAudioPlayerPlugin())) { + val result = RecordingResult() + + plugin.onMethodCall(MethodCall("setLogLevel", mapOf("level" to "warn")), result) + + assertEquals("NOT_INITIALIZED", result.errorCode) + assertEquals(1, result.completionCount) + assertNull(result.successValue) + } + } + + @Test + fun setLogLevelRejectsMissingOrNonStringLevel() { + for (level in listOf(null, 42)) { + val result = RecordingResult() + + MpvPlayerPlugin().onMethodCall(MethodCall("setLogLevel", mapOf("level" to level)), result) + + assertEquals("INVALID_ARGS", result.errorCode) + assertEquals(1, result.completionCount) + } + } + + @Test + fun disposeCompletesQueuedLogLevelChangeOnceWithoutAnActiveNativePlayer() { + val blockerStarted = CountDownLatch(1) + val releaseBlocker = CountDownLatch(1) + val core = testCore { _, _ -> + blockerStarted.countDown() + check(releaseBlocker.await(2, TimeUnit.SECONDS)) + } + val plugin = MpvPlayerPlugin() + installCore(plugin, core) val result = RecordingResult() + try { + core.setProperty("block", "value") + assertTrue(blockerStarted.await(1, TimeUnit.SECONDS)) + plugin.onMethodCall(MethodCall("setLogLevel", mapOf("level" to "v")), result) + core.dispose() + } finally { + releaseBlocker.countDown() + } + awaitCompletion(result) - MpvPlayerPlugin().onMethodCall( - MethodCall("setLogLevel", mapOf("level" to "warn")), - result - ) - - assertEquals("UNSUPPORTED", result.errorCode) + assertEquals("NOT_INITIALIZED", result.errorCode) + assertEquals(1, result.completionCount) assertNull(result.successValue) } diff --git a/android/libmpv/src/main/cpp/event.cpp b/android/libmpv/src/main/cpp/event.cpp index 219495e8e..203194f1b 100644 --- a/android/libmpv/src/main/cpp/event.cpp +++ b/android/libmpv/src/main/cpp/event.cpp @@ -99,7 +99,6 @@ void* event_thread(void* arg) { switch (mp_event->event_id) { case MPV_EVENT_LOG_MESSAGE: msg = (mpv_event_log_message*)mp_event->data; - ALOGV("[%s:%s] %s", msg->prefix, msg->level, msg->text); sendLogMessageToJava(env, msg); break; case MPV_EVENT_PROPERTY_CHANGE: @@ -113,12 +112,10 @@ void* event_thread(void* arg) { mpv_event_start_file* start_file = (mpv_event_start_file*)mp_event->data; has_source_id = start_file != NULL; source_id = start_file ? start_file->playlist_entry_id : 0; - ALOGV("event: %s\n", mpv_event_name(mp_event->event_id)); sendEventToJava(env, mp_event->event_id, source_id, has_source_id); break; } case MPV_EVENT_FILE_LOADED: - ALOGV("event: %s\n", mpv_event_name(mp_event->event_id)); sendEventToJava(env, mp_event->event_id, source_id, has_source_id); break; case MPV_EVENT_HOOK: @@ -129,7 +126,6 @@ void* event_thread(void* arg) { const bool has_position_seconds = mpv_get_property(g_mpv, "time-pos", MPV_FORMAT_DOUBLE, &position_seconds) >= 0 && std::isfinite(position_seconds); - ALOGV("event: %s\n", mpv_event_name(mp_event->event_id)); sendEventToJava(env, mp_event->event_id, source_id, has_source_id, position_seconds, has_position_seconds); break; } diff --git a/android/libmpv/src/main/cpp/log.h b/android/libmpv/src/main/cpp/log.h index 53d8bb11f..3a1eabe17 100644 --- a/android/libmpv/src/main/cpp/log.h +++ b/android/libmpv/src/main/cpp/log.h @@ -2,15 +2,8 @@ #include -#define DEBUG 1 - #define LOG_TAG "mpv" #define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) -#if DEBUG -#define ALOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) -#else -#define ALOGV(...) (void)0 -#endif void die(const char* msg); diff --git a/android/libmpv/src/main/cpp/main.cpp b/android/libmpv/src/main/cpp/main.cpp index 42536935e..209a5e7f9 100644 --- a/android/libmpv/src/main/cpp/main.cpp +++ b/android/libmpv/src/main/cpp/main.cpp @@ -28,6 +28,8 @@ jni_func(void, nativeCreate, jobject appctx); jni_func(void, nativeInit); jni_func(void, nativeDestroy); +jni_func(jint, nativeSetLogLevel, jstring level); + jni_func(void, nativeCommand, jobjectArray jarray); jni_func(void, nativeHookContinue, jlong id); }; @@ -37,6 +39,7 @@ mpv_handle* g_mpv; std::atomic g_event_thread_request_exit(false); static pthread_t event_thread_id; +static bool event_thread_started = false; static std::mutex g_lifecycle_mutex; static void prepare_environment(JNIEnv* env, jobject appctx) { @@ -58,9 +61,12 @@ jni_func(void, nativeCreate, jobject appctx) { if (g_mpv) { ALOGE("destroying leaked mpv instance"); leaked_mpv = g_mpv; - g_event_thread_request_exit = true; - mpv_wakeup(leaked_mpv); - pthread_join(event_thread_id, NULL); + if (event_thread_started) { + g_event_thread_request_exit = true; + mpv_wakeup(leaked_mpv); + pthread_join(event_thread_id, NULL); + event_thread_started = false; + } g_mpv = NULL; mpv_terminate_destroy(leaked_mpv); render_cleanup(env); @@ -72,10 +78,11 @@ jni_func(void, nativeCreate, jobject appctx) { return; } - mpv_request_log_messages(g_mpv, "v"); + mpv_request_log_messages(g_mpv, "warn"); } jni_func(void, nativeInit) { + std::lock_guard lock(g_lifecycle_mutex); if (!g_mpv) { die("mpv is not created"); return; @@ -97,20 +104,25 @@ jni_func(void, nativeInit) { die("thread create failed"); return; } + event_thread_started = true; pthread_setname_np(event_thread_id, "event_thread"); } jni_func(void, nativeDestroy) { std::lock_guard lock(g_lifecycle_mutex); if (!g_mpv) { - ALOGV("mpv destroy called but it's already destroyed"); return; } mpv_handle* local_mpv = g_mpv; - g_event_thread_request_exit = true; - mpv_wakeup(local_mpv); - pthread_join(event_thread_id, NULL); + // Configuration (including an invalid initial log level) can fail before + // nativeInit starts the event thread. + if (event_thread_started) { + g_event_thread_request_exit = true; + mpv_wakeup(local_mpv); + pthread_join(event_thread_id, NULL); + event_thread_started = false; + } g_mpv = NULL; @@ -120,6 +132,17 @@ jni_func(void, nativeDestroy) { render_cleanup(env); } +jni_func(jint, nativeSetLogLevel, jstring jlevel) { + std::lock_guard lock(g_lifecycle_mutex); + if (!g_mpv) return MPV_ERROR_UNINITIALIZED; + + const std::string level = java_string_to_utf8(env, jlevel); + if (env->ExceptionCheck()) return MPV_ERROR_NOMEM; + const int result = mpv_request_log_messages(g_mpv, level.c_str()); + if (result < 0) ALOGE("mpv_request_log_messages returned error %s", mpv_error_string(result)); + return result; +} + jni_func(void, nativeCommand, jobjectArray jarray) { CHECK_MPV_INIT(); diff --git a/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayer.kt b/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayer.kt index 3c0ecf315..960dace54 100644 --- a/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayer.kt +++ b/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayer.kt @@ -45,7 +45,9 @@ class MpvPlayer private constructor() : AutoCloseable { checkNotMainThread("MPV initialization") val player = MpvPlayer() // Atomically replace; mark old as closed so its background close() skips nativeDestroy - instance.getAndSet(player)?.also { it.closed = true } + synchronized(instance) { + instance.getAndSet(player)?.also { it.closed = true } + } // nativeCreate's safety net handles any leaked native session try { nativeCreate(context.applicationContext) @@ -156,6 +158,8 @@ class MpvPlayer private constructor() : AutoCloseable { @JvmStatic private external fun nativeCommand(cmd: Array) + @JvmStatic private external fun nativeSetLogLevel(level: String): Int + @JvmStatic private external fun nativeHookContinue(id: Long) @JvmStatic private external fun nativeSetOptionString(name: String, value: String): Int @@ -187,6 +191,14 @@ class MpvPlayer private constructor() : AutoCloseable { @JvmStatic private external fun nativeObserveProperty(name: String, format: Int) internal fun setOptionString(name: String, value: String): Int = nativeSetOptionString(name, value) + + internal fun requestLogMessages(level: String) { + checkNotMainThread("MPV log level change") + val result = nativeSetLogLevel(level) + if (result < 0) { + throw MpvException("Failed to set log level: error $result") + } + } } // The native event thread hands everything to unbounded channels: trySend @@ -249,6 +261,17 @@ class MpvPlayer private constructor() : AutoCloseable { withContext(Dispatchers.IO) { nativeCommand(args) } } + /** Called on the core's ordered IO writer, without suspending between writes. */ + fun setLogLevel(level: String) { + // Hold ownership through the native call: a retiring player's queued write + // must never change the subscription of the process-global successor. + synchronized(instance) { + checkNotClosed() + check(instance.get() === this) { "MpvPlayer is no longer active" } + requestLogMessages(level) + } + } + // Surface — not suspend, called from SurfaceHolder.Callback fun attachSurface(surface: Surface) { @@ -370,12 +393,14 @@ class MpvPlayer private constructor() : AutoCloseable { * Android main thread so a slow vendor decoder cannot stall the UI. */ override fun close() { - if (closed) return - checkNotMainThread("MPV destruction") - closed = true - // Only destroy native if we're still the active player. - // If create() already replaced us, nativeCreate's safety net handles native cleanup. - if (instance.compareAndSet(this, null)) { + val ownsNative = synchronized(instance) { + if (closed) return + checkNotMainThread("MPV destruction") + closed = true + // If create() replaced us, nativeCreate handles the leaked native session. + instance.compareAndSet(this, null) + } + if (ownsNative) { nativeDestroy() } // After nativeDestroy no callback can produce: closing the channels diff --git a/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayerConfig.kt b/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayerConfig.kt index dc8cdcae8..9e35e3147 100644 --- a/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayerConfig.kt +++ b/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayerConfig.kt @@ -7,4 +7,8 @@ class MpvPlayerConfig internal constructor() { throw MpvException("Failed to set option '$name' to '$value': error $result") } } + + fun setLogLevel(level: String) { + MpvPlayer.requestLogMessages(level) + } } diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 633520a55..a024798ac 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -802,7 +802,7 @@ repositoryURL = "https://github.com/edde746/mpv-build"; requirement = { kind = revision; - revision = 847fa34e8f9e2f72a9a465e5b90649b6e1287eb1; + revision = 8ec035121368149a5f23a47471350e87e9cd9859; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a0352f65a..69e97f2fc 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,7 +32,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/mpv-build", "state" : { - "revision" : "847fa34e8f9e2f72a9a465e5b90649b6e1287eb1" + "revision" : "8ec035121368149a5f23a47471350e87e9cd9859" } }, { diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index a0352f65a..69e97f2fc 100644 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,7 +32,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/mpv-build", "state" : { - "revision" : "847fa34e8f9e2f72a9a465e5b90649b6e1287eb1" + "revision" : "8ec035121368149a5f23a47471350e87e9cd9859" } }, { diff --git a/lib/mpv/player/player_native.dart b/lib/mpv/player/player_native.dart index 0de74c3ae..a01bf21ea 100644 --- a/lib/mpv/player/player_native.dart +++ b/lib/mpv/player/player_native.dart @@ -239,6 +239,8 @@ class PlayerNative extends PlayerBase { // to dispose-and-recreate the in-flight core, hanging playback (#930). Future? _initFuture; Future _audioStateTail = Future.value(); + String _requestedLogLevel = 'warn'; + Future _logLevelTail = Future.value(); Future? _disposeFuture; bool _disposing = false; @@ -272,6 +274,7 @@ class PlayerNative extends PlayerBase { 'subtitleRenderScale': SettingsService.instance .read(SettingsService.subtitleRenderResolution) .androidRenderScale, + if (Platform.isAndroid) 'logLevel': _requestedLogLevel, 'instanceId': nativeInstanceId, }); if (result != true) { @@ -893,6 +896,18 @@ class PlayerNative extends PlayerBase { @override Future setLogLevel(String level) async { if (_nativeCoreUnavailable) return; + if (Platform.isAndroid) { + // Carry the preference into native creation, even if another operation + // starts initialization before this ordered runtime write gets its turn. + _requestedLogLevel = level; + final request = _logLevelTail.then((_) async { + if (_nativeCoreUnavailable) return; + await _ensureInitialized(); + await invoke('setLogLevel', {'level': level}); + }); + _logLevelTail = request.catchError((Object _) {}); + return request; + } await _ensureInitialized(); await invoke('setLogLevel', {'level': level}); } diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 71c94ad5a..991510b0a 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -1283,7 +1283,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin final currentPlayer = Player(useExoPlayer: useExoPlayer, hardwareDecoding: enableHardwareDecoding); attemptPlayer = currentPlayer; if (!mounted || generation != _playerInitializationGeneration) return; - if (Platform.isAndroid && useExoPlayer) { + if (Platform.isAndroid) { await currentPlayer.setLogLevel(debugLoggingEnabled ? 'v' : 'warn'); if (!mounted || generation != _playerInitializationGeneration) return; } @@ -1314,7 +1314,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin _audioFocusFuture!.ignore(); } await currentPlayer.setProperty('msg-level', debugLoggingEnabled ? 'all=debug,ffmpeg/video=warn' : 'all=error'); - if (!Platform.isAndroid || useExoPlayer) { + if (!Platform.isAndroid) { await currentPlayer.setLogLevel(debugLoggingEnabled ? 'v' : 'warn'); } await currentPlayer.setProperty('hwdec', _getHwdecValue(enableHardwareDecoding)); diff --git a/lib/services/music/music_playback_service_impl.dart b/lib/services/music/music_playback_service_impl.dart index 5beea2c6d..f2bc1c185 100644 --- a/lib/services/music/music_playback_service_impl.dart +++ b/lib/services/music/music_playback_service_impl.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io' show Platform; import 'dart:math'; import 'package:flutter/foundation.dart' show ValueListenable, visibleForTesting; @@ -414,7 +415,8 @@ class MusicPlaybackServiceImpl extends MusicPlaybackService with WidgetsBindingO await CarUxRestrictionsService.instance.ensureResolved(); if (generation != _generation) return; } - final player = _ensurePlayer(); + final player = await _ensurePlayer(); + if (generation != _generation || _player != player) return; _ensureMediaControls(); // Re-asserted per open (cheap, idempotent): the native side drops the // background-mode opt-in when the user swipes the task away, so a @@ -642,13 +644,23 @@ class MusicPlaybackServiceImpl extends MusicPlaybackService with WidgetsBindingO // Player events // --------------------------------------------------------------------- - Player _ensurePlayer() { + Future _ensurePlayer() async { final existing = _player; - if (existing != null && !existing.disposed) return existing; - final player = _audioPlayerFactory(); - _player = player; - _wirePlayerStreams(player); - if (_volume != 100.0) unawaited(player.setVolume(_volume)); + final isNew = existing == null || existing.disposed; + final player = existing == null || existing.disposed ? _audioPlayerFactory() : existing; + if (isNew) { + _player = player; + _wirePlayerStreams(player); + } + if (Platform.isAndroid) { + // Configure logging before volume/focus/open can create the native core, + // and refresh it on later opens if the debug preference changed. + final debugLogging = SettingsService.instanceOrNull?.read(SettingsService.enableDebugLogging) ?? false; + await player.setLogLevel(debugLogging ? 'v' : 'warn'); + } + if (isNew && _player == player && !player.disposed && _volume != 100.0) { + unawaited(player.setVolume(_volume)); + } return player; } diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index e0a9fc43f..cce3eabeb 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -895,7 +895,7 @@ repositoryURL = "https://github.com/edde746/mpv-build"; requirement = { kind = revision; - revision = 847fa34e8f9e2f72a9a465e5b90649b6e1287eb1; + revision = 8ec035121368149a5f23a47471350e87e9cd9859; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7050fcac7..6d2ffb61c 100644 --- a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,7 +5,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/mpv-build", "state" : { - "revision" : "847fa34e8f9e2f72a9a465e5b90649b6e1287eb1" + "revision" : "8ec035121368149a5f23a47471350e87e9cd9859" } }, { diff --git a/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7050fcac7..6d2ffb61c 100644 --- a/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,7 +5,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/mpv-build", "state" : { - "revision" : "847fa34e8f9e2f72a9a465e5b90649b6e1287eb1" + "revision" : "8ec035121368149a5f23a47471350e87e9cd9859" } }, { diff --git a/mpv-build.lock.json b/mpv-build.lock.json index 62a1ddd81..0c857db93 100644 --- a/mpv-build.lock.json +++ b/mpv-build.lock.json @@ -4,23 +4,23 @@ "assetBase": "https://github.com/edde746/mpv-build/releases/download/binaries-android", "assets": { "arm64-v8a": { - "asset": "libmpv-android-1032c9038fa3-arm64-v8a.tar.gz", - "checksum": "2fe8e92cdf4f2e3c4e3041da6f85febe15d6253d1d3dcf37028d12530d9bcaa4" + "asset": "libmpv-android-23f2764e9e97-arm64-v8a.tar.gz", + "checksum": "a362deb1882fdb43a7627e6291012a8f523d07ca7c9059843979c57b84ff6ce3" }, "armeabi-v7a": { - "asset": "libmpv-android-1032c9038fa3-armeabi-v7a.tar.gz", - "checksum": "06debebb26c0160c20e033c2d556017671713d23a714d9f846314c71be4bc2c3" + "asset": "libmpv-android-23f2764e9e97-armeabi-v7a.tar.gz", + "checksum": "6c429d25c53f6d1df2d6f33d5b7360084d219e08bb63ab1c20af9ab25ad0675b" }, "x86": { - "asset": "libmpv-android-1032c9038fa3-x86.tar.gz", - "checksum": "956c03afd8a09cea0b44110cae5e691bf3c25d7525664504c256e6866254ecf9" + "asset": "libmpv-android-23f2764e9e97-x86.tar.gz", + "checksum": "2189aa580eb6455f1b55bbbbebae32ba092cb6e59defeac9a0cb1f327a31e121" }, "x86_64": { - "asset": "libmpv-android-1032c9038fa3-x86_64.tar.gz", - "checksum": "751fd32114a5871c79d7d343587cbd5d960d751894118574dbcfd89cc165b07f" + "asset": "libmpv-android-23f2764e9e97-x86_64.tar.gz", + "checksum": "bf0fb093397959ecc7d8cd6c447c11a664da2d7acef5b41b1f3a1e8db749e5bf" } }, - "key": "1032c9038fa3" + "key": "23f2764e9e97" }, "linux": { "assetBase": "https://github.com/edde746/mpv-build/releases/download/binaries-linux", @@ -51,7 +51,7 @@ "key": "2b33f3cb68c4" } }, - "commit": "847fa34e8f9e2f72a9a465e5b90649b6e1287eb1", + "commit": "8ec035121368149a5f23a47471350e87e9cd9859", "formatVersion": 1, "repo": "edde746/mpv-build" } diff --git a/test/mpv/player_native_bridge_test.dart b/test/mpv/player_native_bridge_test.dart index 3310a80b6..8bf31df0e 100644 --- a/test/mpv/player_native_bridge_test.dart +++ b/test/mpv/player_native_bridge_test.dart @@ -769,7 +769,7 @@ void main() { methodHandler: (call) async { if (call.method == 'initialize') return true; if (call.method == 'setLogLevel') { - throw PlatformException(code: 'UNSUPPORTED'); + throw PlatformException(code: 'SET_LOG_LEVEL_FAILED'); } return null; }, @@ -778,7 +778,7 @@ void main() { try { await expectLater( player.setLogLevel('warn'), - throwsA(isA().having((error) => error.code, 'code', 'UNSUPPORTED')), + throwsA(isA().having((error) => error.code, 'code', 'SET_LOG_LEVEL_FAILED')), ); } finally { await player.dispose(); @@ -787,28 +787,6 @@ void main() { ); }); - test('audio setLogLevel uses the dedicated native channel', () async { - MethodCall? logLevelCall; - await withMockPlayerChannels( - methodChannelName: 'com.plezy/mpv_audio_player', - eventChannelName: 'com.plezy/mpv_audio_player/events', - methodHandler: (call) async { - if (call.method == 'initialize') return true; - if (call.method == 'setLogLevel') logLevelCall = call; - return null; - }, - testBody: () async { - final player = PlayerNative.audio(); - try { - await player.setLogLevel('v'); - expect(logLevelCall?.arguments, {'level': 'v'}); - } finally { - await player.dispose(); - } - }, - ); - }); - test('Android mpv end-file error preserves native diagnostic message', () async { await withMockPlayerChannels( methodChannelName: 'com.plezy/mpv_player', diff --git a/tvos/Runner.xcodeproj/project.pbxproj b/tvos/Runner.xcodeproj/project.pbxproj index 0fa3935bd..9d5b8fa49 100644 --- a/tvos/Runner.xcodeproj/project.pbxproj +++ b/tvos/Runner.xcodeproj/project.pbxproj @@ -1175,7 +1175,7 @@ repositoryURL = "https://github.com/edde746/mpv-build"; requirement = { kind = revision; - revision = 847fa34e8f9e2f72a9a465e5b90649b6e1287eb1; + revision = 8ec035121368149a5f23a47471350e87e9cd9859; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/tvos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/tvos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a44d50e95..72852bbef 100644 --- a/tvos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/tvos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,7 +6,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/mpv-build", "state" : { - "revision" : "847fa34e8f9e2f72a9a465e5b90649b6e1287eb1" + "revision" : "8ec035121368149a5f23a47471350e87e9cd9859" } } ], diff --git a/tvos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/tvos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index a44d50e95..72852bbef 100644 --- a/tvos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/tvos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,7 +6,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/mpv-build", "state" : { - "revision" : "847fa34e8f9e2f72a9a465e5b90649b6e1287eb1" + "revision" : "8ec035121368149a5f23a47471350e87e9cd9859" } } ], diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index eff16479a..8a83daa01 100644 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -7,7 +7,6 @@ add_executable(${BINARY_NAME} WIN32 "utils.cpp" "win32_window.cpp" "mpv/display_mode_manager.cpp" - "mpv/hdr_probe.cpp" "mpv/mpv_player.cpp" "mpv/mpv_plugin.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" @@ -58,7 +57,6 @@ if(PLEZY_BUILD_MPV_PROPERTY_CONTRACT_TESTS) add_executable(mpv_player_property_contract_test "mpv/mpv_player.cpp" - "mpv/hdr_probe.cpp" "mpv/mpv_player_property_contract_test.cpp" ) apply_standard_settings(mpv_player_property_contract_test) diff --git a/windows/runner/mpv/hdr_probe.cpp b/windows/runner/mpv/hdr_probe.cpp deleted file mode 100644 index 6c03a94c5..000000000 --- a/windows/runner/mpv/hdr_probe.cpp +++ /dev/null @@ -1,247 +0,0 @@ -#include "hdr_probe.h" - -#include -#include -#include -#include - -namespace mpv { - -namespace { - -constexpr int kVerbatimChanges = 30; -constexpr int kSampleEvery = 50; - -// Node keys that feed pl_tone_map_params_equal / gamut_map_signature. -constexpr const char* kStaticKeys[] = { - "primaries", "gamma", "colormatrix", "colorlevels", - "pixelformat", "max-luma", "min-luma", "max-cll", - "max-fall", "sig-peak", "prim-red-x", "prim-red-y", - "prim-green-x", "prim-green-y", "prim-blue-x", "prim-blue-y", - "prim-white-x", "prim-white-y", "scene-max-r", "scene-max-g", - "scene-max-b", "scene-avg", "w", "h", -}; -constexpr const char* kDynamicKeys[] = {"max-pq-y", "avg-pq-y"}; - -bool Wanted(const char* key, bool dynamic_fields) { - if (dynamic_fields) { - for (const char* k : kDynamicKeys) { - if (std::strcmp(k, key) == 0) return true; - } - return false; - } - for (const char* k : kStaticKeys) { - if (std::strcmp(k, key) == 0) return true; - } - return false; -} - -void AppendValue(std::string& out, const mpv_node& node) { - char buf[64]; - switch (node.format) { - case MPV_FORMAT_STRING: - out += node.u.string ? node.u.string : ""; - break; - case MPV_FORMAT_INT64: - snprintf(buf, sizeof(buf), "%lld", static_cast(node.u.int64)); - out += buf; - break; - case MPV_FORMAT_DOUBLE: - // Exact float text: libplacebo compares these by exact equality, so a - // last-digit wobble is a real LUT invalidation. - snprintf(buf, sizeof(buf), "%.9g", node.u.double_); - out += buf; - break; - case MPV_FORMAT_FLAG: - out += node.u.flag ? "yes" : "no"; - break; - default: - out += "?"; - break; - } -} - -std::string SdrWhiteLevel(HMONITOR monitor) { - MONITORINFOEXW mi{}; - mi.cbSize = sizeof(mi); - if (!GetMonitorInfoW(monitor, &mi)) return "sdrwhite=?(monitorinfo)"; - - UINT32 paths = 0, modes = 0; - if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &paths, &modes) != ERROR_SUCCESS) { - return "sdrwhite=?(buffersizes)"; - } - std::vector path_info(paths); - std::vector mode_info(modes); - if (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &paths, path_info.data(), &modes, mode_info.data(), nullptr) != - ERROR_SUCCESS) { - return "sdrwhite=?(querydisplayconfig)"; - } - for (UINT32 i = 0; i < paths; ++i) { - DISPLAYCONFIG_SOURCE_DEVICE_NAME source{}; - source.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; - source.header.size = sizeof(source); - source.header.adapterId = path_info[i].sourceInfo.adapterId; - source.header.id = path_info[i].sourceInfo.id; - if (DisplayConfigGetDeviceInfo(&source.header) != ERROR_SUCCESS) continue; - if (wcscmp(source.viewGdiDeviceName, mi.szDevice) != 0) continue; - - DISPLAYCONFIG_SDR_WHITE_LEVEL white{}; - white.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL; - white.header.size = sizeof(white); - white.header.adapterId = path_info[i].targetInfo.adapterId; - white.header.id = path_info[i].targetInfo.id; - const LONG rc = DisplayConfigGetDeviceInfo(&white.header); - char buf[64]; - if (rc != ERROR_SUCCESS) { - snprintf(buf, sizeof(buf), "sdrwhite=?(rc=%ld)", rc); - } else { - snprintf(buf, sizeof(buf), "sdrwhite=%u(%.2fnits)", white.SDRWhiteLevel, white.SDRWhiteLevel / 1000.0 * 80.0); - } - return buf; - } - return "sdrwhite=?(no-path)"; -} - -} // namespace - -HdrProbe::HdrProbe(mpv_handle* mpv, HWND hwnd, Logger logger) - : mpv_(mpv), hwnd_(hwnd), logger_(std::move(logger)), last_summary_(std::chrono::steady_clock::now()) {} - -HdrProbe::~HdrProbe() { - if (factory_) factory_->Release(); -} - -void HdrProbe::Stream::Observe(const std::string& now, const Logger& log) { - if (now.empty() || now == last) return; - const bool first = last.empty(); - last = now; - if (first) { - log(std::string(name) + " initial: " + now); - return; - } - ++changes; - if (changes <= kVerbatimChanges || changes % kSampleEvery == 0) { - log(std::string(name) + " change #" + std::to_string(changes) + ": " + now); - } -} - -void HdrProbe::ReadParams(const char* property, Stream& static_stream, Stream* dynamic_stream) { - mpv_node node{}; - if (mpv_get_property(mpv_, property, MPV_FORMAT_NODE, &node) < 0) return; - std::string static_params; - std::string dynamic_params; - if (node.format == MPV_FORMAT_NODE_MAP) { - for (int i = 0; i < node.u.list->num; ++i) { - const char* key = node.u.list->keys[i]; - const bool is_static = Wanted(key, false); - if (!is_static && (!dynamic_stream || !Wanted(key, true))) continue; - std::string& out = is_static ? static_params : dynamic_params; - out += key; - out += '='; - AppendValue(out, node.u.list->values[i]); - out += ' '; - } - } - mpv_free_node_contents(&node); - static_stream.Observe(static_params, logger_); - if (dynamic_stream) dynamic_stream->Observe(dynamic_params, logger_); -} - -std::string HdrProbe::ReadDisplay() { - HMONITOR monitor = MonitorFromWindow(hwnd_, MONITOR_DEFAULTTONULL); - if (!monitor) return "monitor=none"; - - // Mirrors mp_dxgi_output_desc_from_hwnd: mpv drops and recreates its factory - // whenever IsCurrent() is false, and re-reads GetDesc1 on every frame. - if (!factory_ || !factory_->IsCurrent()) { - if (factory_) { - factory_->Release(); - factory_ = nullptr; - ++factory_recreations_; - } - if (FAILED(::CreateDXGIFactory1(IID_PPV_ARGS(&factory_)))) return "factory=failed"; - } - - std::string out; - bool found = false; - IDXGIAdapter1* adapter = nullptr; - for (UINT a = 0; !found && SUCCEEDED(factory_->EnumAdapters1(a, &adapter)); ++a) { - IDXGIOutput* output = nullptr; - for (UINT o = 0; !found && SUCCEEDED(adapter->EnumOutputs(o, &output)); ++o) { - DXGI_OUTPUT_DESC desc{}; - if (SUCCEEDED(output->GetDesc(&desc)) && desc.Monitor == monitor) { - found = true; - IDXGIOutput6* output6 = nullptr; - DXGI_OUTPUT_DESC1 desc1{}; - if (SUCCEEDED(output->QueryInterface(IID_PPV_ARGS(&output6))) && SUCCEEDED(output6->GetDesc1(&desc1))) { - char buf[320]; - snprintf( - buf, sizeof(buf), - "csp=%d bits=%u maxL=%.4g minL=%.4g maxFFL=%.4g R=%.5g,%.5g G=%.5g,%.5g B=%.5g,%.5g W=%.5g,%.5g", - static_cast(desc1.ColorSpace), desc1.BitsPerColor, desc1.MaxLuminance, desc1.MinLuminance, - desc1.MaxFullFrameLuminance, desc1.RedPrimary[0], desc1.RedPrimary[1], desc1.GreenPrimary[0], - desc1.GreenPrimary[1], desc1.BluePrimary[0], desc1.BluePrimary[1], desc1.WhitePoint[0], - desc1.WhitePoint[1]); - out = buf; - } else { - out = "desc1=unavailable"; - } - if (output6) output6->Release(); - } - output->Release(); - output = nullptr; - } - adapter->Release(); - adapter = nullptr; - } - if (!found) out = "output=not-found"; - out += ' '; - out += SdrWhiteLevel(monitor); - return out; -} - -void HdrProbe::OnFileLoaded() { - target_ = Stream{"target-params"}; - source_static_ = Stream{"source-params(static)"}; - source_dynamic_ = Stream{"source-params(dynamic)"}; - factory_recreations_ = 0; - last_summary_ = std::chrono::steady_clock::now(); -} - -void HdrProbe::Tick() { - const auto start = std::chrono::steady_clock::now(); - if (start < next_sample_) return; - // Nothing to key on until a video is being rendered. - int vo_configured = 0; - const int result = mpv_get_property(mpv_, "vo-configured", MPV_FORMAT_FLAG, &vo_configured); - next_sample_ = std::chrono::steady_clock::now() + std::chrono::milliseconds(300); - if (result < 0 || !vo_configured) return; - - ReadParams("video-target-params", target_); - // Both source streams describe the same frame, from one property snapshot. - ReadParams("video-out-params", source_static_, &source_dynamic_); - if (std::chrono::steady_clock::now() >= next_display_) { - display_.Observe(ReadDisplay(), logger_); - next_display_ = std::chrono::steady_clock::now() + std::chrono::milliseconds(1200); - } - - const auto now = std::chrono::steady_clock::now(); - // Schedule from completion: a stalled query must not trigger catch-up work. - next_sample_ = now + std::chrono::milliseconds(300); - if (now - last_summary_ >= std::chrono::seconds(10)) { - last_summary_ = now; - Summarize(); - } -} - -void HdrProbe::Summarize() { - char buf[256]; - snprintf( - buf, sizeof(buf), - "summary: target-params changes=%d source(static) changes=%d source(dynamic) changes=%d display " - "changes=%d dxgi-factory-recreated=%d", - target_.changes, source_static_.changes, source_dynamic_.changes, display_.changes, factory_recreations_); - logger_(buf); -} - -} // namespace mpv diff --git a/windows/runner/mpv/hdr_probe.h b/windows/runner/mpv/hdr_probe.h deleted file mode 100644 index 2152a7c77..000000000 --- a/windows/runner/mpv/hdr_probe.h +++ /dev/null @@ -1,73 +0,0 @@ -#ifndef MPV_HDR_PROBE_H_ -#define MPV_HDR_PROBE_H_ - -#include -#include -#include - -#include -#include -#include - -namespace mpv { - -// Diagnostic for #2191: watches the two inputs libplacebo keys its tone-map and -// gamut-map LUTs on and reports sampled changes into the app log. -// -// * mpv `video-target-params` (the destination colour space the renderer was -// handed: derived per frame from DXGI_OUTPUT_DESC1 and the DisplayConfig SDR -// white level) and `video-out-params` (the source frame's HDR metadata), -// polled no more often than every 300 ms. Sampling can miss changes between -// reads; the counts are not per-frame LUT invalidation counts. -// * The raw DXGI output description and SDR white level of the monitor the -// video window sits on, plus whether the DXGI factory still reports -// IsCurrent(); mpv re-reads all of these on every drawn frame. -// -// Output is rate limited: each change stream logs its first entries verbatim, -// then only every 50th, and a summary line lands every ~10 s while playing. -class HdrProbe { - public: - using Logger = std::function; - - HdrProbe(mpv_handle* mpv, HWND hwnd, Logger logger); - ~HdrProbe(); - - HdrProbe(const HdrProbe&) = delete; - HdrProbe& operator=(const HdrProbe&) = delete; - - // Called after every dequeued event or wait timeout, with no fixed cadence. - // Steady-clock deadlines bound the work independently of event traffic. - void Tick(); - - // Resets per-file state on file load so counts are per playback. - void OnFileLoaded(); - - private: - struct Stream { - const char* name; - std::string last; - int changes = 0; - void Observe(const std::string& now, const Logger& log); - }; - - void ReadParams(const char* property, Stream& static_stream, Stream* dynamic_stream = nullptr); - std::string ReadDisplay(); - void Summarize(); - - mpv_handle* mpv_; - HWND hwnd_; - Logger logger_; - IDXGIFactory1* factory_ = nullptr; - int factory_recreations_ = 0; - std::chrono::steady_clock::time_point next_sample_{}; - std::chrono::steady_clock::time_point next_display_{}; - std::chrono::steady_clock::time_point last_summary_; - Stream target_{"target-params"}; - Stream source_static_{"source-params(static)"}; - Stream source_dynamic_{"source-params(dynamic)"}; - Stream display_{"display"}; -}; - -} // namespace mpv - -#endif // MPV_HDR_PROBE_H_ diff --git a/windows/runner/mpv/mpv_player.cpp b/windows/runner/mpv/mpv_player.cpp index c6b2fff84..eda14439f 100644 --- a/windows/runner/mpv/mpv_player.cpp +++ b/windows/runner/mpv/mpv_player.cpp @@ -645,10 +645,6 @@ bool MpvPlayer::Initialize(HWND view) { // choosing to observe the device list. mpv_observe_property(mpv_, 0, "audio-device-list", MPV_FORMAT_NONE); - if (!audio_only_) { - hdr_probe_ = std::make_unique(mpv_, hwnd_, [this](const std::string& text) { LogHdrProbe(text); }); - } - // Start event loop. StartEventLoop(); @@ -657,9 +653,6 @@ bool MpvPlayer::Initialize(HWND view) { void MpvPlayer::Dispose() { StopEventLoop(); - // Event thread is gone, so nothing ticks the probe; drop it while mpv_ and - // hwnd_ are still valid (its destructor only releases the DXGI factory). - hdr_probe_.reset(); auto cancelled = pending_requests_.CancelAll(); for (auto& callback : cancelled.status) { @@ -802,14 +795,6 @@ void MpvPlayer::LogHdrPipelineOnce() { SendEvent("log-message", data); } -void MpvPlayer::LogHdrProbe(const std::string& text) { - flutter::EncodableMap data; - data[flutter::EncodableValue("prefix")] = flutter::EncodableValue("hdr-probe"); - data[flutter::EncodableValue("level")] = flutter::EncodableValue("info"); - data[flutter::EncodableValue("text")] = flutter::EncodableValue(text); - SendEvent("log-message", data); -} - void MpvPlayer::TryAudioReload(const char* reason, int attempt, uint64_t request_generation) { LogRecovery("issuing ao-reload (reason=" + std::string(reason) + ", attempt " + std::to_string(attempt) + ")"); const std::string reason_copy = reason; @@ -859,9 +844,8 @@ void MpvPlayer::EventLoop() { HandleMpvEvent(event); } // Idle waits are bounded at 100 ms; queued events can wake us sooner. - // Audio recovery and HDR sampling each own their elapsed-time deadlines. + // Audio recovery owns its elapsed-time deadlines. MaybeRunAudioRecovery(); - if (hdr_probe_) hdr_probe_->Tick(); } } @@ -923,7 +907,6 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) { // mpv log level, so the applied HDR pipeline options always land in an // uploaded log (#2191 was undiagnosable without this). LogHdrPipelineOnce(); - if (hdr_probe_) hdr_probe_->OnFileLoaded(); SendActiveSourceEvent("file-loaded"); break; } diff --git a/windows/runner/mpv/mpv_player.h b/windows/runner/mpv/mpv_player.h index 761975c12..474bbe91b 100644 --- a/windows/runner/mpv/mpv_player.h +++ b/windows/runner/mpv/mpv_player.h @@ -16,7 +16,6 @@ #include #include "../../../shared/mpv/mpv_player_common.h" -#include "hdr_probe.h" namespace mpv { struct InnerWindowSubclassState; @@ -141,10 +140,6 @@ class MpvPlayer { // tone-map LUT workaround so the first file load can log it. bool adreno_tone_map_workaround_ = false; bool hdr_config_logged_ = false; - // #2191 diagnostics: reports tone-map input churn (see hdr_probe.h). Owned - // by the player; ticked from the event thread, torn down before mpv. - std::unique_ptr hdr_probe_; - void LogHdrProbe(const std::string& text); void SetHDREnabled(bool enabled, StatusCallback callback = nullptr); };