From 98c6a09b41c07571811de6d316cd86d402c4f3bd Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:30:08 +0200 Subject: [PATCH] chore(player): remove temporary native playback diagnostics Production playback still carried recurring Windows HDR probes, Android subtitle profiling, and verbose mpv logs with debug logging disabled. Remove the Windows probe, pin the published mpv-build profiler cleanup, and honor Android debug logging preferences for video and music while preserving warnings and errors. --- .../plezy/mpv/MpvLogLevelDeviceTest.kt | 146 +++++++++++ .../com/edde746/plezy/mpv/MpvPlayerCore.kt | 38 ++- .../com/edde746/plezy/mpv/MpvPlayerPlugin.kt | 28 +- .../edde746/plezy/mpv/MpvPlayerPluginTest.kt | 52 +++- android/libmpv/src/main/cpp/event.cpp | 4 - android/libmpv/src/main/cpp/log.h | 7 - android/libmpv/src/main/cpp/main.cpp | 39 ++- .../com/edde746/plezy/libmpv/MpvPlayer.kt | 39 ++- .../edde746/plezy/libmpv/MpvPlayerConfig.kt | 4 + ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- lib/mpv/player/player_native.dart | 15 ++ lib/screens/video_player_screen.dart | 4 +- .../music/music_playback_service_impl.dart | 26 +- macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- mpv-build.lock.json | 20 +- test/mpv/player_native_bridge_test.dart | 26 +- tvos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- windows/runner/CMakeLists.txt | 2 - windows/runner/mpv/hdr_probe.cpp | 247 ------------------ windows/runner/mpv/hdr_probe.h | 73 ------ windows/runner/mpv/mpv_player.cpp | 19 +- windows/runner/mpv/mpv_player.h | 5 - 28 files changed, 371 insertions(+), 441 deletions(-) create mode 100644 android/app/src/androidTestDebug/kotlin/com/edde746/plezy/mpv/MpvLogLevelDeviceTest.kt delete mode 100644 windows/runner/mpv/hdr_probe.cpp delete mode 100644 windows/runner/mpv/hdr_probe.h 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); };