chore: add native formatting checks

This commit is contained in:
edde746
2026-05-01 05:56:49 +02:00
parent 9bd5732f2b
commit 024af35bf5
69 changed files with 8848 additions and 8633 deletions
+9
View File
@@ -0,0 +1,9 @@
BasedOnStyle: Google
AlignAfterOpenBracket: AlwaysBreak
ColumnLimit: 120
IndentWidth: 2
ContinuationIndentWidth: 4
DerivePointerAlignment: false
LineEnding: LF
PointerAlignment: Left
SortIncludes: true
+28
View File
@@ -0,0 +1,28 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{c,cc,cpp,h,hpp,m,mm}]
indent_style = space
indent_size = 2
[*.dart]
indent_style = space
indent_size = 2
[*.{kt,kts,java}]
indent_style = space
indent_size = 2
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ktlint_code_style = android_studio
ktlint_standard_max-line-length = disabled
ktlint_standard_no-wildcard-imports = disabled
ktlint_standard_property-naming = disabled
[*.swift]
indent_style = space
indent_size = 2
+23
View File
@@ -130,6 +130,29 @@ jobs:
echo "No tests found, skipping test execution" echo "No tests found, skipping test execution"
fi fi
native-format:
name: Native Formatting
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Swift
uses: swift-actions/setup-swift@v3
with:
swift-version: "6.2"
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: Verify native formatting
run: scripts/format_native.sh --check
dependency-check: dependency-check:
name: Dependency Validation name: Dependency Validation
runs-on: ubuntu-latest runs-on: ubuntu-latest
+10
View File
@@ -0,0 +1,10 @@
{
"indentation" : {
"spaces" : 2
},
"lineLength" : 120,
"maximumBlankLines" : 1,
"respectsExistingLineBreaks" : true,
"rules" : {},
"version" : 1
}
+5 -3
View File
@@ -10,7 +10,8 @@
## Development ## Development
- Follow Dart/Flutter conventions - Follow Dart/Flutter conventions
- Run `dart format .` to format your code (note: generated files like `*.g.dart` are excluded from CI checks) - Run `dart format .` to format Dart code (note: generated files like `*.g.dart` are excluded from CI checks)
- Run `scripts/format_native.sh --fix` to format Kotlin, Swift, C++, C, Objective-C, and native headers
- Run `flutter analyze` before submitting to check for issues - Run `flutter analyze` before submitting to check for issues
- Run `flutter test` if tests are available - Run `flutter test` if tests are available
- Test your changes thoroughly - Test your changes thoroughly
@@ -19,8 +20,9 @@
The project includes automated CI checks that run on all pull requests: The project includes automated CI checks that run on all pull requests:
1. **Code Formatting**: Ensures code follows Dart formatting standards 1. **Code Formatting**: Ensures code follows Dart and native formatting standards
- Run locally: `dart format .` to format all files - Run locally: `dart format .` to format Dart files
- Run locally: `scripts/format_native.sh --fix` to format native files
- Note: CI only checks non-generated files (excludes `.g.dart`, `.freezed.dart`) - Note: CI only checks non-generated files (excludes `.g.dart`, `.freezed.dart`)
- Generated files are reformatted automatically by build tools - Generated files are reformatted automatically by build tools
+2 -3
View File
@@ -1,5 +1,5 @@
import java.util.Properties
import java.io.FileInputStream import java.io.FileInputStream
import java.util.Properties
plugins { plugins {
id("com.android.application") id("com.android.application")
@@ -47,7 +47,7 @@ val doviAbis = mapOf(
"arm64-v8a" to "aarch64-linux-android", "arm64-v8a" to "aarch64-linux-android",
"armeabi-v7a" to "armv7-linux-androideabi", "armeabi-v7a" to "armv7-linux-androideabi",
"x86" to "i686-linux-android", "x86" to "i686-linux-android",
"x86_64" to "x86_64-linux-android", "x86_64" to "x86_64-linux-android"
) )
val downloadLibdovi by tasks.registering { val downloadLibdovi by tasks.registering {
@@ -166,7 +166,6 @@ tasks.matching { it.name.startsWith("pre") && it.name.endsWith("Build") }.config
dependsOn(downloadLibass) dependsOn(downloadLibass)
} }
dependencies { dependencies {
implementation(files(File(mpvDir, mpvAar))) implementation(files(File(mpvDir, mpvAar)))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
+12 -15
View File
@@ -1,5 +1,6 @@
#include <jni.h>
#include <android/log.h> #include <android/log.h>
#include <jni.h>
#include <cstring> #include <cstring>
#include <new> #include <new>
@@ -13,9 +14,8 @@
static const char* BRIDGE_VERSION = "1.0.0"; static const char* BRIDGE_VERSION = "1.0.0";
extern "C" JNIEXPORT jbyteArray JNICALL extern "C" JNIEXPORT jbyteArray JNICALL Java_com_edde746_plezy_exoplayer_DoviBridge_nativeConvertDv7RpuToDv81(
Java_com_edde746_plezy_exoplayer_DoviBridge_nativeConvertDv7RpuToDv81( JNIEnv* env, jclass, jbyteArray payload, jint mode) {
JNIEnv *env, jclass, jbyteArray payload, jint mode) {
#if !DOVI_REAL_LINKED #if !DOVI_REAL_LINKED
return nullptr; return nullptr;
#else #else
@@ -33,24 +33,24 @@ Java_com_edde746_plezy_exoplayer_DoviBridge_nativeConvertDv7RpuToDv81(
// Copy to native heap so libdovi never touches JVM heap memory. // Copy to native heap so libdovi never touches JVM heap memory.
// GetByteArrayElements on ART may return a direct heap pointer; any // GetByteArrayElements on ART may return a direct heap pointer; any
// out-of-bounds access by libdovi would corrupt adjacent JVM objects. // out-of-bounds access by libdovi would corrupt adjacent JVM objects.
auto *buf = new (std::nothrow) uint8_t[static_cast<size_t>(len)]; auto* buf = new (std::nothrow) uint8_t[static_cast<size_t>(len)];
if (buf == nullptr) return nullptr; if (buf == nullptr) return nullptr;
env->GetByteArrayRegion(payload, 0, len, reinterpret_cast<jbyte *>(buf)); env->GetByteArrayRegion(payload, 0, len, reinterpret_cast<jbyte*>(buf));
if (env->ExceptionCheck()) { if (env->ExceptionCheck()) {
delete[] buf; delete[] buf;
return nullptr; return nullptr;
} }
// Try dovi_parse_unspec62_nalu first (handles escaped NALs), fallback to dovi_parse_rpu // Try dovi_parse_unspec62_nalu first (handles escaped NALs), fallback to dovi_parse_rpu
DoviRpuOpaque *rpu = dovi_parse_unspec62_nalu(buf, static_cast<size_t>(len)); DoviRpuOpaque* rpu = dovi_parse_unspec62_nalu(buf, static_cast<size_t>(len));
if (rpu == nullptr) { if (rpu == nullptr) {
delete[] buf; delete[] buf;
return nullptr; return nullptr;
} }
const char *err = dovi_rpu_get_error(rpu); const char* err = dovi_rpu_get_error(rpu);
if (err != nullptr) { if (err != nullptr) {
// Fallback: try dovi_parse_rpu (raw RPU without NAL framing) // Fallback: try dovi_parse_rpu (raw RPU without NAL framing)
dovi_rpu_free(rpu); dovi_rpu_free(rpu);
@@ -80,7 +80,7 @@ Java_com_edde746_plezy_exoplayer_DoviBridge_nativeConvertDv7RpuToDv81(
} }
// Write back as UNSPEC62 NAL // Write back as UNSPEC62 NAL
const DoviData *out = dovi_write_unspec62_nalu(rpu); const DoviData* out = dovi_write_unspec62_nalu(rpu);
if (out == nullptr || out->data == nullptr || out->len == 0) { if (out == nullptr || out->data == nullptr || out->len == 0) {
err = dovi_rpu_get_error(rpu); err = dovi_rpu_get_error(rpu);
LOGW("RPU write failed: %s", err ? err : "unknown"); LOGW("RPU write failed: %s", err ? err : "unknown");
@@ -98,8 +98,7 @@ Java_com_edde746_plezy_exoplayer_DoviBridge_nativeConvertDv7RpuToDv81(
jbyteArray result = env->NewByteArray(static_cast<jsize>(out->len)); jbyteArray result = env->NewByteArray(static_cast<jsize>(out->len));
if (result != nullptr) { if (result != nullptr) {
env->SetByteArrayRegion(result, 0, static_cast<jsize>(out->len), env->SetByteArrayRegion(result, 0, static_cast<jsize>(out->len), reinterpret_cast<const jbyte*>(out->data));
reinterpret_cast<const jbyte *>(out->data));
} }
dovi_data_free(out); dovi_data_free(out);
@@ -110,8 +109,7 @@ Java_com_edde746_plezy_exoplayer_DoviBridge_nativeConvertDv7RpuToDv81(
} }
extern "C" JNIEXPORT jboolean JNICALL extern "C" JNIEXPORT jboolean JNICALL
Java_com_edde746_plezy_exoplayer_DoviBridge_nativeIsConversionPathReady( Java_com_edde746_plezy_exoplayer_DoviBridge_nativeIsConversionPathReady(JNIEnv*, jclass) {
JNIEnv *, jclass) {
#if DOVI_REAL_LINKED #if DOVI_REAL_LINKED
return JNI_TRUE; return JNI_TRUE;
#else #else
@@ -120,7 +118,6 @@ Java_com_edde746_plezy_exoplayer_DoviBridge_nativeIsConversionPathReady(
} }
extern "C" JNIEXPORT jstring JNICALL extern "C" JNIEXPORT jstring JNICALL
Java_com_edde746_plezy_exoplayer_DoviBridge_nativeGetBridgeVersion( Java_com_edde746_plezy_exoplayer_DoviBridge_nativeGetBridgeVersion(JNIEnv* env, jclass) {
JNIEnv *env, jclass) {
return env->NewStringUTF(BRIDGE_VERSION); return env->NewStringUTF(BRIDGE_VERSION);
} }
@@ -1,13 +1,13 @@
package com.edde746.plezy package com.edde746.plezy
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.app.AppOpsManager import android.app.AppOpsManager
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log import android.util.Log
import android.util.Rational import android.util.Rational
import android.view.KeyEvent import android.view.KeyEvent
@@ -15,16 +15,16 @@ import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterShellArgs
import io.flutter.embedding.android.RenderMode
import io.flutter.embedding.android.TransparencyMode
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import com.edde746.plezy.exoplayer.ExoPlayerPlugin import com.edde746.plezy.exoplayer.ExoPlayerPlugin
import com.edde746.plezy.mpv.MpvPlayerPlugin import com.edde746.plezy.mpv.MpvPlayerPlugin
import com.edde746.plezy.shared.ThemeHelper import com.edde746.plezy.shared.ThemeHelper
import com.edde746.plezy.watchnext.WatchNextPlugin import com.edde746.plezy.watchnext.WatchNextPlugin
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.RenderMode
import io.flutter.embedding.android.TransparencyMode
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterShellArgs
import io.flutter.plugin.common.MethodChannel
import java.io.File import java.io.File
class MainActivity : FlutterActivity() { class MainActivity : FlutterActivity() {
@@ -44,9 +44,7 @@ class MainActivity : FlutterActivity() {
private var autoPipWidth: Int = 16 private var autoPipWidth: Int = 16
private var autoPipHeight: Int = 9 private var autoPipHeight: Int = 9
private fun isAndroidTvDevice(): Boolean { private fun isAndroidTvDevice(): Boolean = packageManager.hasSystemFeature("android.software.leanback")
return packageManager.hasSystemFeature("android.software.leanback")
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// Apply persisted theme color to the window background before anything // Apply persisted theme color to the window background before anything
@@ -97,9 +95,13 @@ class MainActivity : FlutterActivity() {
content.removeViewAt(0) content.removeViewAt(0)
wrapper.addView(child) wrapper.addView(child)
} }
content.addView(wrapper, ViewGroup.LayoutParams( content.addView(
wrapper,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT)) ViewGroup.LayoutParams.MATCH_PARENT
)
)
// Handle Watch Next deep link from initial launch // Handle Watch Next deep link from initial launch
handleWatchNextIntent(intent) handleWatchNextIntent(intent)
@@ -134,13 +136,19 @@ class MainActivity : FlutterActivity() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val soc = Build.SOC_MODEL val soc = Build.SOC_MODEL
if (soc.startsWith("Tensor", ignoreCase = true) || if (soc.startsWith("Tensor", ignoreCase = true) ||
soc.startsWith("GS", ignoreCase = true)) return true soc.startsWith("GS", ignoreCase = true)
) {
return true
}
} }
// NVIDIA Tegra (Shield TV) // NVIDIA Tegra (Shield TV)
if (Build.MANUFACTURER.equals("NVIDIA", ignoreCase = true)) return true if (Build.MANUFACTURER.equals("NVIDIA", ignoreCase = true)) return true
// Huawei/HONOR Kirin SoCs use Mali GPUs // Huawei/HONOR Kirin SoCs use Mali GPUs
if (Build.MANUFACTURER.equals("Huawei", ignoreCase = true) || if (Build.MANUFACTURER.equals("Huawei", ignoreCase = true) ||
Build.MANUFACTURER.equals("HONOR", ignoreCase = true)) return true Build.MANUFACTURER.equals("HONOR", ignoreCase = true)
) {
return true
}
return false return false
} }
@@ -238,7 +246,7 @@ class MainActivity : FlutterActivity() {
watchNextPlugin = WatchNextPlugin() watchNextPlugin = WatchNextPlugin()
flutterEngine.plugins.add(watchNextPlugin!!) flutterEngine.plugins.add(watchNextPlugin!!)
MethodChannel( flutterEngine.dartExecutor.binaryMessenger, PIP_CHANNEL ).setMethodCallHandler { call, result -> MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PIP_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"isSupported" -> { "isSupported" -> {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isAndroidTvDevice()) result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isAndroidTvDevice())
@@ -301,7 +309,7 @@ class MainActivity : FlutterActivity() {
} }
} }
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean,newConfig: Configuration) { override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
flutterEngine?.let { engine -> flutterEngine?.let { engine ->
MethodChannel(engine.dartExecutor.binaryMessenger, PIP_CHANNEL).invokeMethod("onPipChanged", isInPictureInPictureMode) MethodChannel(engine.dartExecutor.binaryMessenger, PIP_CHANNEL).invokeMethod("onPipChanged", isInPictureInPictureMode)
@@ -317,7 +325,9 @@ class MainActivity : FlutterActivity() {
if (!isAndroidTvDevice() && if (!isAndroidTvDevice() &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.S && Build.VERSION.SDK_INT < Build.VERSION_CODES.S &&
autoPipReady && isPipPermissionGranted()) { autoPipReady &&
isPipPermissionGranted()
) {
try { try {
// Notify Flutter to prepare video filter before PiP // Notify Flutter to prepare video filter before PiP
flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger -> flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger ->
@@ -19,14 +19,16 @@ import androidx.media3.extractor.TrackOutput
*/ */
@androidx.media3.common.util.UnstableApi @androidx.media3.common.util.UnstableApi
class CuelessSeekExtractorWrapper( class CuelessSeekExtractorWrapper(
private val delegate: Extractor, private val delegate: Extractor
) : Extractor { ) : Extractor {
companion object { companion object {
private const val TAG = "CuelessSeek" private const val TAG = "CuelessSeek"
// MKV Cluster element ID: 0x1F43B675 (4-byte EBML Class-D ID) // MKV Cluster element ID: 0x1F43B675 (4-byte EBML Class-D ID)
private val CLUSTER_ID = byteArrayOf(0x1F, 0x43, 0xB6.toByte(), 0x75) private val CLUSTER_ID = byteArrayOf(0x1F, 0x43, 0xB6.toByte(), 0x75)
private const val SCAN_BUFFER_SIZE = 8192 private const val SCAN_BUFFER_SIZE = 8192
// Max bytes to scan for a Cluster boundary before giving up // Max bytes to scan for a Cluster boundary before giving up
private const val MAX_SCAN_BYTES = 1024 * 1024 // 1 MB private const val MAX_SCAN_BYTES = 1024 * 1024 // 1 MB
} }
@@ -120,7 +122,7 @@ class CuelessSeekExtractorWrapper(
* [SeekMap.Unseekable] with an approximate proportional SeekMap. * [SeekMap.Unseekable] with an approximate proportional SeekMap.
*/ */
private inner class SeekInterceptingOutput( private inner class SeekInterceptingOutput(
private val delegate: ExtractorOutput, private val delegate: ExtractorOutput
) : ExtractorOutput { ) : ExtractorOutput {
override fun track(id: Int, type: Int): TrackOutput = delegate.track(id, type) override fun track(id: Int, type: Int): TrackOutput = delegate.track(id, type)
@@ -147,7 +149,7 @@ class CuelessSeekExtractorWrapper(
* Used when the MKV has no Cues but has a known duration. * Used when the MKV has no Cues but has a known duration.
*/ */
private inner class ApproximateSeekMap( private inner class ApproximateSeekMap(
private val durationUs: Long, private val durationUs: Long
) : SeekMap { ) : SeekMap {
override fun isSeekable(): Boolean = true override fun isSeekable(): Boolean = true
@@ -28,7 +28,8 @@ object DoviBridge {
if (Build.VERSION.SDK_INT < minApi) return false if (Build.VERSION.SDK_INT < minApi) return false
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS) val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
return codecList.codecInfos.any { info -> return codecList.codecInfos.any { info ->
!info.isEncoder && info.supportedTypes.any { type -> !info.isEncoder &&
info.supportedTypes.any { type ->
type.equals("video/dolby-vision", ignoreCase = true) && type.equals("video/dolby-vision", ignoreCase = true) &&
info.getCapabilitiesForType(type).profileLevels.any { it.profile == profile } info.getCapabilitiesForType(type).profileLevels.any { it.profile == profile }
} }
@@ -28,7 +28,7 @@ import androidx.media3.extractor.TrackOutput
*/ */
class DoviConvertingTrackOutput( class DoviConvertingTrackOutput(
private val delegate: TrackOutput, private val delegate: TrackOutput,
private val dvMode: DvConversionMode = DvConversionMode.HEVC_STRIP, private val dvMode: DvConversionMode = DvConversionMode.HEVC_STRIP
) : TrackOutput { ) : TrackOutput {
companion object { companion object {
@@ -66,9 +66,12 @@ class DoviConvertingTrackOutput(
if (codecs != null && codecs.startsWith("dvhe.07")) { if (codecs != null && codecs.startsWith("dvhe.07")) {
conversionActive = true conversionActive = true
Log.i(TAG, "DV Profile 7 detected ($codecs), mode=$dvMode") Log.i(TAG, "DV Profile 7 detected ($codecs), mode=$dvMode")
Log.i(TAG, "Original format: mime=${format.sampleMimeType}, codecs=$codecs, " + Log.i(
TAG,
"Original format: mime=${format.sampleMimeType}, codecs=$codecs, " +
"initData=${format.initializationData.size} entries " + "initData=${format.initializationData.size} entries " +
"(${format.initializationData.mapIndexed { i, d -> "$i:${d.size}B" }.joinToString()})") "(${format.initializationData.mapIndexed { i, d -> "$i:${d.size}B" }.joinToString()})"
)
val newFormat = when (dvMode) { val newFormat = when (dvMode) {
DvConversionMode.DV81 -> { DvConversionMode.DV81 -> {
@@ -82,10 +85,11 @@ class DoviConvertingTrackOutput(
.setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION)
.setCodecs(newCodecs) .setCodecs(newCodecs)
.setInitializationData( .setInitializationData(
if (format.initializationData.isNotEmpty()) if (format.initializationData.isNotEmpty()) {
listOf(format.initializationData[0], dvConfigRecord) listOf(format.initializationData[0], dvConfigRecord)
else } else {
listOf(ByteArray(0), dvConfigRecord) listOf(ByteArray(0), dvConfigRecord)
}
) )
.build() .build()
} }
@@ -96,17 +100,21 @@ class DoviConvertingTrackOutput(
.setSampleMimeType(MimeTypes.VIDEO_H265) .setSampleMimeType(MimeTypes.VIDEO_H265)
.setCodecs(null) .setCodecs(null)
.setInitializationData( .setInitializationData(
if (format.initializationData.isNotEmpty()) if (format.initializationData.isNotEmpty()) {
listOf(format.initializationData[0]) listOf(format.initializationData[0])
else } else {
emptyList() emptyList()
}
) )
.build() .build()
} }
} }
Log.i(TAG, "Rewritten format: mime=${newFormat.sampleMimeType}, " + Log.i(
"codecs=${newFormat.codecs}, initData=${newFormat.initializationData.size} entries") TAG,
"Rewritten format: mime=${newFormat.sampleMimeType}, " +
"codecs=${newFormat.codecs}, initData=${newFormat.initializationData.size} entries"
)
delegate.format(newFormat) delegate.format(newFormat)
return return
} }
@@ -115,7 +123,10 @@ class DoviConvertingTrackOutput(
} }
override fun sampleData( override fun sampleData(
input: DataReader, length: Int, allowEndOfInput: Boolean, sampleDataPart: Int input: DataReader,
length: Int,
allowEndOfInput: Boolean,
sampleDataPart: Int
): Int { ): Int {
if (!conversionActive) { if (!conversionActive) {
return delegate.sampleData(input, length, allowEndOfInput, sampleDataPart) return delegate.sampleData(input, length, allowEndOfInput, sampleDataPart)
@@ -145,7 +156,11 @@ class DoviConvertingTrackOutput(
} }
override fun sampleMetadata( override fun sampleMetadata(
timeUs: Long, flags: Int, size: Int, offset: Int, cryptoData: TrackOutput.CryptoData? timeUs: Long,
flags: Int,
size: Int,
offset: Int,
cryptoData: TrackOutput.CryptoData?
) { ) {
if (!conversionActive || !buffering) { if (!conversionActive || !buffering) {
delegate.sampleMetadata(timeUs, flags, size, offset, cryptoData) delegate.sampleMetadata(timeUs, flags, size, offset, cryptoData)
@@ -198,14 +213,26 @@ class DoviConvertingTrackOutput(
} }
// Auto-detect: Annex B starts with 00 00 00 01 or 00 00 01 // Auto-detect: Annex B starts with 00 00 00 01 or 00 00 01
val isAnnexB = (dataLen >= 4 && sampleBuf[0] == 0.toByte() && sampleBuf[1] == 0.toByte() && val isAnnexB = (
sampleBuf[2] == 0.toByte() && sampleBuf[3] == 1.toByte()) || dataLen >= 4 &&
(dataLen >= 3 && sampleBuf[0] == 0.toByte() && sampleBuf[1] == 0.toByte() && sampleBuf[0] == 0.toByte() &&
sampleBuf[2] == 1.toByte()) sampleBuf[1] == 0.toByte() &&
sampleBuf[2] == 0.toByte() &&
sampleBuf[3] == 1.toByte()
) ||
(
dataLen >= 3 &&
sampleBuf[0] == 0.toByte() &&
sampleBuf[1] == 0.toByte() &&
sampleBuf[2] == 1.toByte()
)
if (sampleCount == 0L) { if (sampleCount == 0L) {
Log.d(TAG, "NAL format detected: ${if (isAnnexB) "Annex B" else "length-prefixed"}, " + Log.d(
"first bytes: ${sampleBuf.take(8).joinToString(" ") { "%02X".format(it) }}") TAG,
"NAL format detected: ${if (isAnnexB) "Annex B" else "length-prefixed"}, " +
"first bytes: ${sampleBuf.take(8).joinToString(" ") { "%02X".format(it) }}"
)
} }
if (isAnnexB) processAnnexBNals(dataLen) else processLengthPrefixedNals(dataLen) if (isAnnexB) processAnnexBNals(dataLen) else processLengthPrefixedNals(dataLen)
@@ -273,7 +300,8 @@ class DoviConvertingTrackOutput(
kept++ kept++
} else if (action == NalAction.CONVERT) { } else if (action == NalAction.CONVERT) {
val converted = DoviBridge.convertRpuNalu( val converted = DoviBridge.convertRpuNalu(
sampleBuf.copyOfRange(nalStart, nalStart + nalLen), LIBDOVI_MODE_TO_81 sampleBuf.copyOfRange(nalStart, nalStart + nalLen),
LIBDOVI_MODE_TO_81
) )
if (converted != null) { if (converted != null) {
normalizeLayerId(converted, 0) normalizeLayerId(converted, 0)
@@ -305,8 +333,11 @@ class DoviConvertingTrackOutput(
sampleCount++ sampleCount++
if (sampleCount <= 3 || (sampleCount % 500 == 0L)) { if (sampleCount <= 3 || (sampleCount % 500 == 0L)) {
Log.d(TAG, "Sample #$sampleCount (AnnexB): ${dataLen}B -> ${outputLen}B, " + Log.d(
"kept=$kept stripped=$stripped NALs") TAG,
"Sample #$sampleCount (AnnexB): ${dataLen}B -> ${outputLen}B, " +
"kept=$kept stripped=$stripped NALs"
)
} }
} }
@@ -342,7 +373,8 @@ class DoviConvertingTrackOutput(
kept++ kept++
} else if (action == NalAction.CONVERT) { } else if (action == NalAction.CONVERT) {
val converted = DoviBridge.convertRpuNalu( val converted = DoviBridge.convertRpuNalu(
sampleBuf.copyOfRange(nalStart, nalStart + nalLen), LIBDOVI_MODE_TO_81 sampleBuf.copyOfRange(nalStart, nalStart + nalLen),
LIBDOVI_MODE_TO_81
) )
if (converted != null) { if (converted != null) {
normalizeLayerId(converted, 0) normalizeLayerId(converted, 0)
@@ -367,8 +399,11 @@ class DoviConvertingTrackOutput(
sampleCount++ sampleCount++
if (sampleCount <= 3 || (sampleCount % 500 == 0L)) { if (sampleCount <= 3 || (sampleCount % 500 == 0L)) {
Log.d(TAG, "Sample #$sampleCount (LenPrefix): ${dataLen}B -> ${outputLen}B, " + Log.d(
"kept=$kept stripped=$stripped NALs") TAG,
"Sample #$sampleCount (LenPrefix): ${dataLen}B -> ${outputLen}B, " +
"kept=$kept stripped=$stripped NALs"
)
} }
} }
@@ -16,7 +16,7 @@ import androidx.media3.extractor.TrackOutput
class DoviExtractorOutputWrapper( class DoviExtractorOutputWrapper(
private val delegate: ExtractorOutput, private val delegate: ExtractorOutput,
private val dvMode: DvConversionMode, private val dvMode: DvConversionMode,
private val onVideoTrackWrapped: (DoviConvertingTrackOutput) -> Unit, private val onVideoTrackWrapped: (DoviConvertingTrackOutput) -> Unit
) : ExtractorOutput { ) : ExtractorOutput {
override fun track(id: Int, type: Int): TrackOutput { override fun track(id: Int, type: Int): TrackOutput {
val original = delegate.track(id, type) val original = delegate.track(id, type)
@@ -39,7 +39,7 @@ class DoviExtractorOutputWrapper(
*/ */
class DoviExtractorWrapper( class DoviExtractorWrapper(
private val delegate: Extractor, private val delegate: Extractor,
private val dvMode: DvConversionMode = DvConversionMode.HEVC_STRIP, private val dvMode: DvConversionMode = DvConversionMode.HEVC_STRIP
) : Extractor { ) : Extractor {
@Volatile var doviTrackOutput: DoviConvertingTrackOutput? = null @Volatile var doviTrackOutput: DoviConvertingTrackOutput? = null
@@ -51,8 +51,7 @@ class DoviExtractorWrapper(
delegate.init(DoviExtractorOutputWrapper(output, dvMode) { doviTrackOutput = it }) delegate.init(DoviExtractorOutputWrapper(output, dvMode) { doviTrackOutput = it })
} }
override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int = override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int = delegate.read(input, seekPosition)
delegate.read(input, seekPosition)
override fun seek(position: Long, timeUs: Long) = delegate.seek(position, timeUs) override fun seek(position: Long, timeUs: Long) = delegate.seek(position, timeUs)
@@ -7,7 +7,6 @@ import android.graphics.Color
import android.graphics.PixelFormat import android.graphics.PixelFormat
import android.graphics.Typeface import android.graphics.Typeface
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
@@ -24,32 +23,28 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes import androidx.media3.common.MimeTypes
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.text.CueGroup
import androidx.media3.common.TrackGroup import androidx.media3.common.TrackGroup
import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.Tracks import androidx.media3.common.Tracks
import androidx.media3.common.VideoSize import androidx.media3.common.VideoSize
import androidx.media3.common.text.CueGroup
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cronet.CronetDataSource import androidx.media3.datasource.cronet.CronetDataSource
import org.chromium.net.CronetEngine
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicLong
import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.RenderersFactory import androidx.media3.exoplayer.RenderersFactory
import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.extractor.DefaultExtractorsFactory import androidx.media3.extractor.DefaultExtractorsFactory
import androidx.media3.extractor.mkv.MatroskaExtractor
import androidx.media3.extractor.mp4.FragmentedMp4Extractor import androidx.media3.extractor.mp4.FragmentedMp4Extractor
import androidx.media3.extractor.mp4.Mp4Extractor import androidx.media3.extractor.mp4.Mp4Extractor
import androidx.media3.extractor.mkv.MatroskaExtractor
import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.CaptionStyleCompat import androidx.media3.ui.CaptionStyleCompat
import androidx.media3.ui.SubtitleView import androidx.media3.ui.SubtitleView
@@ -57,10 +52,12 @@ import com.edde746.plezy.shared.AudioFocusManager
import com.edde746.plezy.shared.FlutterOverlayHelper import com.edde746.plezy.shared.FlutterOverlayHelper
import com.edde746.plezy.shared.FrameRateManager import com.edde746.plezy.shared.FrameRateManager
import io.github.peerless2012.ass.media.AssHandler import io.github.peerless2012.ass.media.AssHandler
import io.github.peerless2012.ass.media.parser.AssSubtitleParserFactory import io.github.peerless2012.ass.media.parser.AssSubtitleParserFactory
import io.github.peerless2012.ass.media.type.AssRenderType import io.github.peerless2012.ass.media.type.AssRenderType
import io.github.peerless2012.ass.media.widget.AssSubtitleSurfaceView import io.github.peerless2012.ass.media.widget.AssSubtitleSurfaceView
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicLong
import org.chromium.net.CronetEngine
interface ExoPlayerDelegate : com.edde746.plezy.shared.PlayerDelegate { interface ExoPlayerDelegate : com.edde746.plezy.shared.PlayerDelegate {
@@ -95,15 +92,13 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
private var assGlCrashHandlerInstalled = false private var assGlCrashHandlerInstalled = false
private var cronetEngine: CronetEngine? = null private var cronetEngine: CronetEngine? = null
private fun getCronetEngine(context: Context): CronetEngine { private fun getCronetEngine(context: Context): CronetEngine = cronetEngine ?: synchronized(this) {
return cronetEngine ?: synchronized(this) {
cronetEngine ?: CronetEngine.Builder(context.applicationContext) cronetEngine ?: CronetEngine.Builder(context.applicationContext)
.enableHttp2(true) .enableHttp2(true)
.enableQuic(true) .enableQuic(true)
.build() .build()
.also { cronetEngine = it } .also { cronetEngine = it }
} }
}
private val cronetExecutor by lazy { Executors.newSingleThreadExecutor() } private val cronetExecutor by lazy { Executors.newSingleThreadExecutor() }
} }
@@ -127,6 +122,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
get() = tunnelingDisabledForAudioCodec || tunnelingDisabledForVideoCodec get() = tunnelingDisabledForAudioCodec || tunnelingDisabledForVideoCodec
private var currentTunneledPlayback: Boolean = false private var currentTunneledPlayback: Boolean = false
private var lastSeekable: Boolean? = null private var lastSeekable: Boolean? = null
@Volatile private var disposing: Boolean = false @Volatile private var disposing: Boolean = false
private var pendingStartPositionMs: Long = 0L private var pendingStartPositionMs: Long = 0L
private var pendingPlayWhenReady: Boolean? = null private var pendingPlayWhenReady: Boolean? = null
@@ -134,6 +130,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
// Frame watchdog: detects black screen (audio plays but 0 video frames rendered) // Frame watchdog: detects black screen (audio plays but 0 video frames rendered)
private var frameWatchdogRunnable: Runnable? = null private var frameWatchdogRunnable: Runnable? = null
private var frameWatchdogStartTime: Long = 0L private var frameWatchdogStartTime: Long = 0L
// Decoder hang detection: tracks gap between decoder init and first rendered frame // Decoder hang detection: tracks gap between decoder init and first rendered frame
private var decoderHangRunnable: Runnable? = null private var decoderHangRunnable: Runnable? = null
private var decoderInitName: String? = null private var decoderInitName: String? = null
@@ -151,6 +148,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
// FPS detection from frame timestamps (fallback when Format.frameRate is NO_VALUE) // FPS detection from frame timestamps (fallback when Format.frameRate is NO_VALUE)
@Volatile private var detectedFrameRate: Float = -1f @Volatile private var detectedFrameRate: Float = -1f
private val fpsTimestamps = LongArray(FPS_SAMPLE_COUNT) private val fpsTimestamps = LongArray(FPS_SAMPLE_COUNT)
@Volatile private var fpsTimestampCount = 0 @Volatile private var fpsTimestampCount = 0
// Audio focus // Audio focus
@@ -158,6 +156,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
// Track state for event emission // Track state for event emission
private var lastPosition: Long = 0 private var lastPosition: Long = 0
/** Position to use for fallback: max of current position and pending start position. */ /** Position to use for fallback: max of current position and pending start position. */
private val effectivePosition: Long get() = maxOf(lastPosition, pendingStartPositionMs) private val effectivePosition: Long get() = maxOf(lastPosition, pendingStartPositionMs)
private var lastDuration: Long = 0 private var lastDuration: Long = 0
@@ -184,9 +183,14 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
else -> Log.d(TAG, "[$prefix] $message") else -> Log.d(TAG, "[$prefix] $message")
} }
if (debugLoggingEnabled || level == "error" || level == "warn") { if (debugLoggingEnabled || level == "error" || level == "warn") {
delegate?.onEvent("log-message", mapOf( delegate?.onEvent(
"prefix" to prefix, "level" to level, "text" to message "log-message",
)) mapOf(
"prefix" to prefix,
"level" to level,
"text" to message
)
)
} }
} }
@@ -245,7 +249,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
// DV conversion state // DV conversion state
private var dvMode: DvConversionMode = DvConversionMode.DISABLED private var dvMode: DvConversionMode = DvConversionMode.DISABLED
private var dv7RetryAttempted = false private var dv7RetryAttempted = false
@Volatile private var activeDoviMkvWrapper: DoviExtractorWrapper? = null @Volatile private var activeDoviMkvWrapper: DoviExtractorWrapper? = null
@Volatile private var activeDoviMp4Wrapper: DoviExtractorWrapper? = null @Volatile private var activeDoviMp4Wrapper: DoviExtractorWrapper? = null
fun initialize(bufferSizeBytes: Int? = null, tunnelingEnabled: Boolean = true): Boolean { fun initialize(bufferSizeBytes: Int? = null, tunnelingEnabled: Boolean = true): Boolean {
@@ -256,8 +262,11 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
tunnelingUserEnabled = tunnelingEnabled tunnelingUserEnabled = tunnelingEnabled
this.dvMode = DoviBridge.getConversionMode() this.dvMode = DoviBridge.getConversionMode()
Log.i(TAG, "DV conversion: mode=$dvMode, bridge=${DoviBridge.isAvailable()}, " + Log.i(
"deviceDV7=${DoviBridge.deviceSupportsDvProfile7}, deviceDV8=${DoviBridge.deviceSupportsDvProfile8}") TAG,
"DV conversion: mode=$dvMode, bridge=${DoviBridge.isAvailable()}, " +
"deviceDV7=${DoviBridge.deviceSupportsDvProfile7}, deviceDV8=${DoviBridge.deviceSupportsDvProfile8}"
)
disposing = false disposing = false
try { try {
@@ -386,7 +395,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
emptyList() emptyList()
} else { } else {
MediaCodecSelector.DEFAULT.getDecoderInfos( MediaCodecSelector.DEFAULT.getDecoderInfos(
mimeType, requiresSecureDecoder, requiresTunnelingDecoder mimeType,
requiresSecureDecoder,
requiresTunnelingDecoder
) )
} }
} }
@@ -446,8 +457,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
.setSubtitleParserFactory(assParserFactory) .setSubtitleParserFactory(assParserFactory)
// Wrap text renderers with subtitle delay support // Wrap text renderers with subtitle delay support
val wrappedRenderersFactory = RenderersFactory { val wrappedRenderersFactory = RenderersFactory { eventHandler, videoListener, audioListener, textOutput, metadataOutput ->
eventHandler, videoListener, audioListener, textOutput, metadataOutput ->
renderersFactory.createRenderers(eventHandler, videoListener, audioListener, textOutput, metadataOutput) renderersFactory.createRenderers(eventHandler, videoListener, audioListener, textOutput, metadataOutput)
.map { if (it.trackType == C.TRACK_TYPE_TEXT) SubtitleDelayRenderer(it, subtitleDelayUs) else it } .map { if (it.trackType == C.TRACK_TYPE_TEXT) SubtitleDelayRenderer(it, subtitleDelayUs) else it }
.toTypedArray() .toTypedArray()
@@ -481,7 +491,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
setBufferDurationsMs(30_000, 60_000, 1_000, 5_000) setBufferDurationsMs(30_000, 60_000, 1_000, 5_000)
} }
}.build() }.build()
emitLog("info", "init", "Buffer: ${targetBufferBytes / 1024 / 1024}MB limit, available=${availableMB}MB, tunneling=${tunnelingUserEnabled}, dataSource=Cronet") emitLog("info", "init", "Buffer: ${targetBufferBytes / 1024 / 1024}MB limit, available=${availableMB}MB, tunneling=$tunnelingUserEnabled, dataSource=Cronet")
exoPlayer = ExoPlayer.Builder(activity) exoPlayer = ExoPlayer.Builder(activity)
.setTrackSelector(trackSelector!!) .setTrackSelector(trackSelector!!)
@@ -530,7 +540,8 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
val previousHandler = Thread.getDefaultUncaughtExceptionHandler() val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
if (thread.name.contains("AssTexRenderThread") && if (thread.name.contains("AssTexRenderThread") &&
throwable is UninitializedPropertyAccessException) { throwable is UninitializedPropertyAccessException
) {
Log.e(TAG, "ASS GL thread crash suppressed (EGL init failure)", throwable) Log.e(TAG, "ASS GL thread crash suppressed (EGL init failure)", throwable)
} else { } else {
previousHandler?.uncaughtException(thread, throwable) previousHandler?.uncaughtException(thread, throwable)
@@ -586,7 +597,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
} }
override fun surfaceChanged(holder: android.view.SurfaceHolder, format: Int, width: Int, height: Int) { override fun surfaceChanged(holder: android.view.SurfaceHolder, format: Int, width: Int, height: Int) {
emitLog("debug", "surface", "Changed: ${width}x${height}") emitLog("debug", "surface", "Changed: ${width}x$height")
} }
override fun surfaceDestroyed(holder: android.view.SurfaceHolder) { override fun surfaceDestroyed(holder: android.view.SurfaceHolder) {
@@ -774,11 +785,14 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
(error.message?.contains("Response code: 500") == true) (error.message?.contains("Response code: 500") == true)
if (isHttp500) { if (isHttp500) {
Log.w(TAG, "Server returned HTTP 500 - skipping MPV fallback (unrecoverable until server-side change)") Log.w(TAG, "Server returned HTTP 500 - skipping MPV fallback (unrecoverable until server-side change)")
delegate?.onEvent("end-file", mapOf( delegate?.onEvent(
"end-file",
mapOf(
"reason" to "error", "reason" to "error",
"message" to (error.message ?: "HTTP 500"), "message" to (error.message ?: "HTTP 500"),
"cause" to "server-http-500" "cause" to "server-http-500"
)) )
)
return return
} }
@@ -794,10 +808,13 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
if (handled) return if (handled) return
} }
delegate?.onEvent("end-file", mapOf( delegate?.onEvent(
"end-file",
mapOf(
"reason" to "error", "reason" to "error",
"message" to (error.message ?: "Unknown error") "message" to (error.message ?: "Unknown error")
)) )
)
} }
/** /**
@@ -821,7 +838,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
uri = uri, uri = uri,
headers = currentHeaders, headers = currentHeaders,
startPositionMs = lastPosition, startPositionMs = lastPosition,
autoPlay = true, autoPlay = true
) )
return true return true
} }
@@ -1033,7 +1050,8 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
if (!name.startsWith("OMX.google.") && if (!name.startsWith("OMX.google.") &&
!name.startsWith("c2.android.") && !name.startsWith("c2.android.") &&
!name.contains(".sw.") && !name.contains(".sw.") &&
!name.startsWith("c2.ffmpeg.")) { !name.startsWith("c2.ffmpeg.")
) {
Log.d(TAG, "Found hardware audio decoder for $mimeType: $name") Log.d(TAG, "Found hardware audio decoder for $mimeType: $name")
found = true found = true
break break
@@ -1065,7 +1083,8 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
if (name.startsWith("OMX.google.") || if (name.startsWith("OMX.google.") ||
name.startsWith("c2.android.") || name.startsWith("c2.android.") ||
name.contains(".sw.") || name.contains(".sw.") ||
name.startsWith("c2.ffmpeg.")) { name.startsWith("c2.ffmpeg.")
) {
continue // Skip software decoders continue // Skip software decoders
} }
val caps = info.getCapabilitiesForType(type) val caps = info.getCapabilitiesForType(type)
@@ -1294,8 +1313,14 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
// Public API // Public API
fun open(uri: String, headers: Map<String, String>?, startPositionMs: Long, autoPlay: Boolean, isLive: Boolean = false, fun open(
externalSubtitleList: List<Map<String, String?>>? = null) { uri: String,
headers: Map<String, String>?,
startPositionMs: Long,
autoPlay: Boolean,
isLive: Boolean = false,
externalSubtitleList: List<Map<String, String?>>? = null
) {
if (!isInitialized) return if (!isInitialized) return
stopFrameWatchdog() stopFrameWatchdog()
@@ -1571,8 +1596,11 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
Color.argb(bgAlpha, Color.red(it), Color.green(it), Color.blue(it)) Color.argb(bgAlpha, Color.red(it), Color.green(it), Color.blue(it))
} }
val edgeColor = Color.parseColor(borderColor) val edgeColor = Color.parseColor(borderColor)
val edgeType = if (borderSize > 0) CaptionStyleCompat.EDGE_TYPE_OUTLINE val edgeType = if (borderSize > 0) {
else CaptionStyleCompat.EDGE_TYPE_NONE CaptionStyleCompat.EDGE_TYPE_OUTLINE
} else {
CaptionStyleCompat.EDGE_TYPE_NONE
}
val typefaceStyle = when { val typefaceStyle = when {
bold && italic -> Typeface.BOLD_ITALIC bold && italic -> Typeface.BOLD_ITALIC
@@ -1580,8 +1608,11 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
italic -> Typeface.ITALIC italic -> Typeface.ITALIC
else -> Typeface.NORMAL else -> Typeface.NORMAL
} }
val typeface = if (typefaceStyle != Typeface.NORMAL) val typeface = if (typefaceStyle != Typeface.NORMAL) {
Typeface.create(Typeface.DEFAULT, typefaceStyle) else null Typeface.create(Typeface.DEFAULT, typefaceStyle)
} else {
null
}
val style = CaptionStyleCompat( val style = CaptionStyleCompat(
fgColor, fgColor,
@@ -1663,7 +1694,9 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
fun requestAudioFocus(): Boolean = audioFocusManager?.requestAudioFocus() ?: false fun requestAudioFocus(): Boolean = audioFocusManager?.requestAudioFocus() ?: false
fun abandonAudioFocus() { audioFocusManager?.abandonAudioFocus() } fun abandonAudioFocus() {
audioFocusManager?.abandonAudioFocus()
}
// Frame Rate Matching // Frame Rate Matching
@@ -1671,7 +1704,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
fps: Float, fps: Float,
videoDurationMs: Long, videoDurationMs: Long,
extraDelayMs: Long, extraDelayMs: Long,
onComplete: (switched: Boolean) -> Unit, onComplete: (switched: Boolean) -> Unit
) { ) {
val mgr = frameRateManager val mgr = frameRateManager
if (mgr == null) { if (mgr == null) {
@@ -1744,15 +1777,17 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
"isPlaying" to player.isPlaying, "isPlaying" to player.isPlaying,
"playbackState" to player.playbackState, "playbackState" to player.playbackState,
// DV conversion (query extractor's track output, which is set during extraction) // DV conversion (query extractor's track output, which is set during extraction)
*(activeDoviMkvWrapper?.doviTrackOutput *(
?: activeDoviMp4Wrapper?.doviTrackOutput).let { dovi -> activeDoviMkvWrapper?.doviTrackOutput
?: activeDoviMp4Wrapper?.doviTrackOutput
).let { dovi ->
arrayOf( arrayOf(
"dvConversionActive" to (dovi?.conversionActive == true), "dvConversionActive" to (dovi?.conversionActive == true),
"dvConversionMode" to dvMode.name, "dvConversionMode" to dvMode.name,
"dvStrippedNals" to (dovi?.strippedNalCount ?: 0L), "dvStrippedNals" to (dovi?.strippedNalCount ?: 0L),
"dvConvertedRpus" to (dovi?.convertedRpuCount ?: 0L), "dvConvertedRpus" to (dovi?.convertedRpuCount ?: 0L)
) )
}, }
) )
} }
@@ -1872,5 +1907,4 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
Log.d(TAG, "Disposed") Log.d(TAG, "Disposed")
} }
} }
@@ -16,8 +16,12 @@ import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, class ExoPlayerPlugin :
EventChannel.StreamHandler, ActivityAware, ExoPlayerDelegate { FlutterPlugin,
MethodChannel.MethodCallHandler,
EventChannel.StreamHandler,
ActivityAware,
ExoPlayerDelegate {
companion object { companion object {
private const val TAG = "ExoPlayerPlugin" private const val TAG = "ExoPlayerPlugin"
@@ -130,8 +134,11 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
"requestAudioFocus" -> handleRequestAudioFocus(result) "requestAudioFocus" -> handleRequestAudioFocus(result)
"abandonAudioFocus" -> handleAbandonAudioFocus(result) "abandonAudioFocus" -> handleAbandonAudioFocus(result)
"isInitialized" -> result.success( "isInitialized" -> result.success(
if (usingMpvFallback) mpvCore?.isInitialized ?: false if (usingMpvFallback) {
else playerCore?.isInitialized ?: false mpvCore?.isInitialized ?: false
} else {
playerCore?.isInitialized ?: false
}
) )
"getStats" -> handleGetStats(result) "getStats" -> handleGetStats(result)
"getPlayerType" -> result.success(if (usingMpvFallback) "mpv" else "exoplayer") "getPlayerType" -> result.success(if (usingMpvFallback) "mpv" else "exoplayer")
@@ -191,7 +198,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
} }
val success = playerCore?.initialize( val success = playerCore?.initialize(
bufferSizeBytes = bufferSizeBytes, bufferSizeBytes = bufferSizeBytes,
tunnelingEnabled = tunnelingEnabled, tunnelingEnabled = tunnelingEnabled
) ?: false ) ?: false
// Start hidden // Start hidden
@@ -440,12 +447,18 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
val onComplete: (Boolean) -> Unit = { switched -> result.success(switched) } val onComplete: (Boolean) -> Unit = { switched -> result.success(switched) }
if (usingMpvFallback) { if (usingMpvFallback) {
val core = mpvCore val core = mpvCore
if (core == null) result.success(false) if (core == null) {
else core.setVideoFrameRate(fps, duration, extraDelayMs, onComplete) result.success(false)
} else {
core.setVideoFrameRate(fps, duration, extraDelayMs, onComplete)
}
} else { } else {
val core = playerCore val core = playerCore
if (core == null) result.success(false) if (core == null) {
else core.setVideoFrameRate(fps, duration, extraDelayMs, onComplete) result.success(false)
} else {
core.setVideoFrameRate(fps, duration, extraDelayMs, onComplete)
}
} }
} }
@@ -604,7 +617,7 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
"cache-speed" to mpv.getProperty("cache-speed"), "cache-speed" to mpv.getProperty("cache-speed"),
"frame-drop-count" to mpv.getProperty("frame-drop-count"), "frame-drop-count" to mpv.getProperty("frame-drop-count"),
"decoder-frame-drop-count" to mpv.getProperty("decoder-frame-drop-count"), "decoder-frame-drop-count" to mpv.getProperty("decoder-frame-drop-count"),
"demuxer-cache-duration" to mpv.getProperty("demuxer-cache-duration"), "demuxer-cache-duration" to mpv.getProperty("demuxer-cache-duration")
) )
// Only query properties that require an active video track // Only query properties that require an active video track
@@ -695,10 +708,14 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
Log.i(TAG, "ExoPlayer error, switching to MPV fallback at ${positionMs}ms: $errorMessage") Log.i(TAG, "ExoPlayer error, switching to MPV fallback at ${positionMs}ms: $errorMessage")
if (debugLoggingEnabled) { if (debugLoggingEnabled) {
onEvent("log-message", mapOf( onEvent(
"prefix" to "fallback", "level" to "warn", "log-message",
mapOf(
"prefix" to "fallback",
"level" to "warn",
"text" to "Switching to MPV at ${positionMs}ms: $errorMessage" "text" to "Switching to MPV at ${positionMs}ms: $errorMessage"
)) )
)
} }
currentActivity.runOnUiThread { currentActivity.runOnUiThread {
@@ -8,6 +8,7 @@ import androidx.media3.common.PlaybackParameters
import androidx.media3.common.util.Clock import androidx.media3.common.util.Clock
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.Renderer
import androidx.media3.exoplayer.analytics.PlayerId import androidx.media3.exoplayer.analytics.PlayerId
import androidx.media3.exoplayer.audio.AudioOutput import androidx.media3.exoplayer.audio.AudioOutput
import androidx.media3.exoplayer.audio.AudioOutputProvider import androidx.media3.exoplayer.audio.AudioOutputProvider
@@ -16,7 +17,6 @@ import androidx.media3.exoplayer.audio.AudioTrackAudioOutputProvider
import androidx.media3.exoplayer.audio.DefaultAudioSink import androidx.media3.exoplayer.audio.DefaultAudioSink
import androidx.media3.exoplayer.audio.DefaultAudioTrackBufferSizeProvider import androidx.media3.exoplayer.audio.DefaultAudioTrackBufferSizeProvider
import androidx.media3.exoplayer.audio.ForwardingAudioSink import androidx.media3.exoplayer.audio.ForwardingAudioSink
import androidx.media3.exoplayer.Renderer
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
import kotlin.math.abs import kotlin.math.abs
@@ -155,7 +155,8 @@ private class PositionFixAudioSink(
// --- Suppress timestamp discontinuity errors --- // --- Suppress timestamp discontinuity errors ---
override fun setListener(listener: AudioSink.Listener) { override fun setListener(listener: AudioSink.Listener) {
super.setListener(@OptIn(UnstableApi::class) object : AudioSink.Listener { super.setListener(
@OptIn(UnstableApi::class) object : AudioSink.Listener {
override fun onPositionDiscontinuity() = listener.onPositionDiscontinuity() override fun onPositionDiscontinuity() = listener.onPositionDiscontinuity()
override fun onPositionAdvancing(playoutStartSystemTimeUs: Long) = listener.onPositionAdvancing(playoutStartSystemTimeUs) override fun onPositionAdvancing(playoutStartSystemTimeUs: Long) = listener.onPositionAdvancing(playoutStartSystemTimeUs)
override fun onUnderrun(bufferSize: Int, bufferSizeMs: Long, elapsedSinceLastFeedMs: Long) = listener.onUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs) override fun onUnderrun(bufferSize: Int, bufferSizeMs: Long, elapsedSinceLastFeedMs: Long) = listener.onUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)
@@ -175,7 +176,8 @@ private class PositionFixAudioSink(
} }
listener.onAudioSinkError(audioSinkError) listener.onAudioSinkError(audioSinkError)
} }
}) }
)
} }
override fun flush() { override fun flush() {
@@ -239,11 +241,9 @@ private class RawPositionOutputProvider(
private var cachedOutput: RawPositionAudioOutput? = null private var cachedOutput: RawPositionAudioOutput? = null
private var cachedConfig: AudioOutputProvider.OutputConfig? = null private var cachedConfig: AudioOutputProvider.OutputConfig? = null
override fun getFormatSupport(config: AudioOutputProvider.FormatConfig) = override fun getFormatSupport(config: AudioOutputProvider.FormatConfig) = delegate.getFormatSupport(config)
delegate.getFormatSupport(config)
override fun getOutputConfig(config: AudioOutputProvider.FormatConfig) = override fun getOutputConfig(config: AudioOutputProvider.FormatConfig) = delegate.getOutputConfig(config)
delegate.getOutputConfig(config)
override fun getAudioOutput(config: AudioOutputProvider.OutputConfig): AudioOutput { override fun getAudioOutput(config: AudioOutputProvider.OutputConfig): AudioOutput {
val cached = cachedOutput val cached = cachedOutput
@@ -267,11 +267,9 @@ private class RawPositionOutputProvider(
cachedOutput = output cachedOutput = output
} }
override fun addListener(listener: AudioOutputProvider.Listener) = override fun addListener(listener: AudioOutputProvider.Listener) = delegate.addListener(listener)
delegate.addListener(listener)
override fun removeListener(listener: AudioOutputProvider.Listener) = override fun removeListener(listener: AudioOutputProvider.Listener) = delegate.removeListener(listener)
delegate.removeListener(listener)
override fun setClock(clock: Clock) = delegate.setClock(clock) override fun setClock(clock: Clock) = delegate.setClock(clock)
@@ -300,8 +298,7 @@ private class RawPositionAudioOutput(
override fun pause() = delegate.pause() override fun pause() = delegate.pause()
@Throws(AudioOutput.WriteException::class) @Throws(AudioOutput.WriteException::class)
override fun write(buffer: ByteBuffer, size: Int, presentationTimeUs: Long) = override fun write(buffer: ByteBuffer, size: Int, presentationTimeUs: Long) = delegate.write(buffer, size, presentationTimeUs)
delegate.write(buffer, size, presentationTimeUs)
override fun flush() { override fun flush() {
rawPositionUs.set(Long.MIN_VALUE) rawPositionUs.set(Long.MIN_VALUE)
@@ -335,14 +332,11 @@ private class RawPositionAudioOutput(
override fun isStalled() = delegate.isStalled() override fun isStalled() = delegate.isStalled()
override fun addListener(listener: AudioOutput.Listener) = delegate.addListener(listener) override fun addListener(listener: AudioOutput.Listener) = delegate.addListener(listener)
override fun removeListener(listener: AudioOutput.Listener) = delegate.removeListener(listener) override fun removeListener(listener: AudioOutput.Listener) = delegate.removeListener(listener)
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) = override fun setPlaybackParameters(playbackParameters: PlaybackParameters) = delegate.setPlaybackParameters(playbackParameters)
delegate.setPlaybackParameters(playbackParameters) override fun setOffloadDelayPadding(delayInFrames: Int, paddingInFrames: Int) = delegate.setOffloadDelayPadding(delayInFrames, paddingInFrames)
override fun setOffloadDelayPadding(delayInFrames: Int, paddingInFrames: Int) =
delegate.setOffloadDelayPadding(delayInFrames, paddingInFrames)
override fun setOffloadEndOfStream() = delegate.setOffloadEndOfStream() override fun setOffloadEndOfStream() = delegate.setOffloadEndOfStream()
override fun setPlayerId(playerId: PlayerId) = delegate.setPlayerId(playerId) override fun setPlayerId(playerId: PlayerId) = delegate.setPlayerId(playerId)
override fun attachAuxEffect(effectId: Int) = delegate.attachAuxEffect(effectId) override fun attachAuxEffect(effectId: Int) = delegate.attachAuxEffect(effectId)
override fun setAuxEffectSendLevel(level: Float) = delegate.setAuxEffectSendLevel(level) override fun setAuxEffectSendLevel(level: Float) = delegate.setAuxEffectSendLevel(level)
override fun setPreferredDevice(preferredDevice: AudioDeviceInfo?) = override fun setPreferredDevice(preferredDevice: AudioDeviceInfo?) = delegate.setPreferredDevice(preferredDevice)
delegate.setPreferredDevice(preferredDevice)
} }
@@ -16,7 +16,7 @@ import java.util.zip.Inflater
* All buffers are reused across samples to minimize GC pressure on the hot path. * All buffers are reused across samples to minimize GC pressure on the hot path.
*/ */
class ZlibInflatingTrackOutput( class ZlibInflatingTrackOutput(
private val delegate: TrackOutput, private val delegate: TrackOutput
) : TrackOutput { ) : TrackOutput {
companion object { companion object {
@@ -40,7 +40,10 @@ class ZlibInflatingTrackOutput(
override fun format(format: Format) = delegate.format(format) override fun format(format: Format) = delegate.format(format)
override fun sampleData( override fun sampleData(
input: DataReader, length: Int, allowEndOfInput: Boolean, sampleDataPart: Int input: DataReader,
length: Int,
allowEndOfInput: Boolean,
sampleDataPart: Int
): Int { ): Int {
if (!active) return delegate.sampleData(input, length, allowEndOfInput, sampleDataPart) if (!active) return delegate.sampleData(input, length, allowEndOfInput, sampleDataPart)
@@ -64,7 +67,11 @@ class ZlibInflatingTrackOutput(
} }
override fun sampleMetadata( override fun sampleMetadata(
timeUs: Long, flags: Int, size: Int, offset: Int, cryptoData: TrackOutput.CryptoData? timeUs: Long,
flags: Int,
size: Int,
offset: Int,
cryptoData: TrackOutput.CryptoData?
) { ) {
if (!active || !buffering) { if (!active || !buffering) {
delegate.sampleMetadata(timeUs, flags, size, offset, cryptoData) delegate.sampleMetadata(timeUs, flags, size, offset, cryptoData)
@@ -6,9 +6,9 @@ import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.SeekMap import androidx.media3.extractor.SeekMap
import androidx.media3.extractor.TrackOutput import androidx.media3.extractor.TrackOutput
import androidx.media3.extractor.mkv.MatroskaExtractor import androidx.media3.extractor.mkv.MatroskaExtractor
import androidx.media3.extractor.text.SubtitleParser
import io.github.peerless2012.ass.media.AssHandler import io.github.peerless2012.ass.media.AssHandler
import io.github.peerless2012.ass.media.extractor.AssMatroskaExtractor import io.github.peerless2012.ass.media.extractor.AssMatroskaExtractor
import androidx.media3.extractor.text.SubtitleParser
/** /**
* Extends AssMatroskaExtractor to add support for MKV ContentCompAlgo 0 (zlib). * Extends AssMatroskaExtractor to add support for MKV ContentCompAlgo 0 (zlib).
@@ -21,7 +21,7 @@ import androidx.media3.extractor.text.SubtitleParser
*/ */
class ZlibMatroskaExtractor( class ZlibMatroskaExtractor(
subtitleParserFactory: SubtitleParser.Factory, subtitleParserFactory: SubtitleParser.Factory,
assHandler: AssHandler, assHandler: AssHandler
) : AssMatroskaExtractor(subtitleParserFactory, assHandler) { ) : AssMatroskaExtractor(subtitleParserFactory, assHandler) {
companion object { companion object {
@@ -94,7 +94,7 @@ class ZlibMatroskaExtractor(
* created track (called when we know a track uses zlib compression). * created track (called when we know a track uses zlib compression).
*/ */
private class ZlibExtractorOutputWrapper( private class ZlibExtractorOutputWrapper(
private val delegate: ExtractorOutput, private val delegate: ExtractorOutput
) : ExtractorOutput { ) : ExtractorOutput {
private var lastCreatedWrapper: ZlibInflatingTrackOutput? = null private var lastCreatedWrapper: ZlibInflatingTrackOutput? = null
@@ -31,13 +31,20 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
private var surfaceView: SurfaceView? = null private var surfaceView: SurfaceView? = null
private var surfaceContainer: android.widget.FrameLayout? = null private var surfaceContainer: android.widget.FrameLayout? = null
private var overlayLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null private var overlayLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
@Volatile private var disposing: Boolean = false @Volatile private var disposing: Boolean = false
@Volatile private var pendingSurface: Surface? = null @Volatile private var pendingSurface: Surface? = null
@Volatile private var attachedSurface: Surface? = null @Volatile private var attachedSurface: Surface? = null
private var placeholderImageReader: ImageReader? = null private var placeholderImageReader: ImageReader? = null
@Volatile private var placeholderSurface: Surface? = null @Volatile private var placeholderSurface: Surface? = null
@Volatile private var lastAppliedSurfaceSize: String? = null @Volatile private var lastAppliedSurfaceSize: String? = null
@Volatile private var lastKnownSurfaceWidth: Int = 0 @Volatile private var lastKnownSurfaceWidth: Int = 0
@Volatile private var lastKnownSurfaceHeight: Int = 0 @Volatile private var lastKnownSurfaceHeight: Int = 0
var delegate: PlayerDelegate? = null var delegate: PlayerDelegate? = null
var isInitialized: Boolean = false var isInitialized: Boolean = false
@@ -52,13 +59,21 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
// Audio focus // Audio focus
private var audioFocusManager: AudioFocusManager? = null private var audioFocusManager: AudioFocusManager? = null
@Volatile private var cachedPaused: Boolean = true @Volatile private var cachedPaused: Boolean = true
@Volatile private var pausedForSurfaceLoss: Boolean = false @Volatile private var pausedForSurfaceLoss: Boolean = false
@Volatile private var hasAttachedSurface: Boolean = false @Volatile private var hasAttachedSurface: Boolean = false
@Volatile private var attachedToPlaceholder: Boolean = false @Volatile private var attachedToPlaceholder: Boolean = false
@Volatile private var videoOutputRestoring: Boolean = false @Volatile private var videoOutputRestoring: Boolean = false
@Volatile private var deferredResumeRequested: Boolean = false @Volatile private var deferredResumeRequested: Boolean = false
@Volatile private var resumeBlockedByPublicPause: Boolean = false @Volatile private var resumeBlockedByPublicPause: Boolean = false
@Volatile private var videoOutputEpoch: Long = 0L @Volatile private var videoOutputEpoch: Long = 0L
private val videoOutputMutex = Mutex() private val videoOutputMutex = Mutex()
private var pendingVideoOutputDisableJob: Job? = null private var pendingVideoOutputDisableJob: Job? = null
@@ -122,8 +137,11 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
handler = handler, handler = handler,
onPause = { onPause = {
scope.launch { scope.launch {
try { player?.setProperty("pause", true) } try {
catch (e: Exception) { Log.w(TAG, "Failed to pause on focus loss", e) } player?.setProperty("pause", true)
} catch (e: Exception) {
Log.w(TAG, "Failed to pause on focus loss", e)
}
} }
}, },
onResume = { onResume = {
@@ -133,7 +151,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
) )
frameRateManager = FrameRateManager( frameRateManager = FrameRateManager(
activity = activity, activity = activity,
handler = handler, handler = handler
) )
// Create FrameLayout container for video // Create FrameLayout container for video
@@ -269,11 +287,14 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
private fun collectLogMessages(p: MpvPlayer) { private fun collectLogMessages(p: MpvPlayer) {
scope.launch(start = CoroutineStart.UNDISPATCHED) { scope.launch(start = CoroutineStart.UNDISPATCHED) {
p.logFlow.collect { msg -> p.logFlow.collect { msg ->
delegate?.onEvent("log-message", mapOf( delegate?.onEvent(
"log-message",
mapOf(
"prefix" to msg.prefix, "prefix" to msg.prefix,
"level" to msg.level.name.lowercase(), "level" to msg.level.name.lowercase(),
"text" to msg.text "text" to msg.text
)) )
)
} }
} }
} }
@@ -282,7 +303,9 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
fun requestAudioFocus(): Boolean = audioFocusManager?.requestAudioFocus() ?: false fun requestAudioFocus(): Boolean = audioFocusManager?.requestAudioFocus() ?: false
fun abandonAudioFocus() { audioFocusManager?.abandonAudioFocus() } fun abandonAudioFocus() {
audioFocusManager?.abandonAudioFocus()
}
// SurfaceHolder.Callback // SurfaceHolder.Callback
@@ -304,7 +327,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
} }
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
Log.d(TAG, "Surface changed: ${width}x${height}") Log.d(TAG, "Surface changed: ${width}x$height")
rememberSurfaceSize(width, height) rememberSurfaceSize(width, height)
refreshVideoOutput("surfaceChanged") refreshVideoOutput("surfaceChanged")
} }
@@ -327,18 +350,14 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
rememberSurfaceSize(sv.width, sv.height) rememberSurfaceSize(sv.width, sv.height)
} }
private fun currentCandidateSurface(): Surface? = private fun currentCandidateSurface(): Surface? = surfaceView?.holder?.surface?.takeIf { it.isValid }
surfaceView?.holder?.surface?.takeIf { it.isValid }
?: pendingSurface?.takeIf { it.isValid } ?: pendingSurface?.takeIf { it.isValid }
private fun hasAttachedRealSurface(): Boolean = private fun hasAttachedRealSurface(): Boolean = hasAttachedSurface && !attachedToPlaceholder && (attachedSurface?.isValid == true)
hasAttachedSurface && !attachedToPlaceholder && (attachedSurface?.isValid == true)
private fun hasReadyVideoOutput(): Boolean = private fun hasReadyVideoOutput(): Boolean = hasAttachedRealSurface() && !videoOutputRestoring
hasAttachedRealSurface() && !videoOutputRestoring
private fun isCurrentVideoOutputEpoch(epoch: Long): Boolean = private fun isCurrentVideoOutputEpoch(epoch: Long): Boolean = !disposing && epoch == videoOutputEpoch
!disposing && epoch == videoOutputEpoch
private fun isVideoOutputRefreshCurrent(epoch: Long): Boolean { private fun isVideoOutputRefreshCurrent(epoch: Long): Boolean {
if (disposing) return false if (disposing) return false
@@ -441,8 +460,11 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
rememberSurfaceSize(width, height) rememberSurfaceSize(width, height)
if (!hasReadyVideoOutput()) return if (!hasReadyVideoOutput()) return
scope.launch { scope.launch {
try { applySurfaceSizeInternal(p) } try {
catch (e: Exception) { Log.w(TAG, "Failed to apply surface size to MPV", e) } applySurfaceSizeInternal(p)
} catch (e: Exception) {
Log.w(TAG, "Failed to apply surface size to MPV", e)
}
} }
} }
@@ -452,7 +474,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
val height = lastKnownSurfaceHeight val height = lastKnownSurfaceHeight
if (width <= 0 || height <= 0) return if (width <= 0 || height <= 0) return
val size = "${width}x${height}" val size = "${width}x$height"
if (!force && size == lastAppliedSurfaceSize) return if (!force && size == lastAppliedSurfaceSize) return
p.setProperty("android-surface-size", size) p.setProperty("android-surface-size", size)
lastAppliedSurfaceSize = size lastAppliedSurfaceSize = size
@@ -612,8 +634,11 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
} }
} }
scope.launch { scope.launch {
try { player?.setProperty(name, value) } try {
catch (e: Exception) { Log.w(TAG, "setProperty($name) failed", e) } player?.setProperty(name, value)
} catch (e: Exception) {
Log.w(TAG, "setProperty($name) failed", e)
}
} }
} }
@@ -641,8 +666,11 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
fun command(args: Array<String>) { fun command(args: Array<String>) {
if (!isInitialized || disposing || args.isEmpty()) return if (!isInitialized || disposing || args.isEmpty()) return
scope.launch { scope.launch {
try { player?.command(*args) } try {
catch (e: Exception) { Log.w(TAG, "command failed", e) } player?.command(*args)
} catch (e: Exception) {
Log.w(TAG, "command failed", e)
}
} }
} }
@@ -712,7 +740,7 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback {
fps: Float, fps: Float,
videoDurationMs: Long, videoDurationMs: Long,
extraDelayMs: Long, extraDelayMs: Long,
onComplete: (switched: Boolean) -> Unit, onComplete: (switched: Boolean) -> Unit
) { ) {
val mgr = frameRateManager val mgr = frameRateManager
if (mgr == null) { if (mgr == null) {
@@ -10,8 +10,12 @@ import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, class MpvPlayerPlugin :
EventChannel.StreamHandler, ActivityAware, com.edde746.plezy.shared.PlayerDelegate { FlutterPlugin,
MethodChannel.MethodCallHandler,
EventChannel.StreamHandler,
ActivityAware,
com.edde746.plezy.shared.PlayerDelegate {
companion object { companion object {
private const val TAG = "MpvPlayerPlugin" private const val TAG = "MpvPlayerPlugin"
@@ -33,6 +37,7 @@ class MpvPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
// of each tearing down the in-flight core and starting their own — which // of each tearing down the in-flight core and starting their own — which
// was the root cause of #930. // was the root cause of #930.
private val pendingInitResults = mutableListOf<MethodChannel.Result>() private val pendingInitResults = mutableListOf<MethodChannel.Result>()
@Volatile private var isInitializing = false @Volatile private var isInitializing = false
// FlutterPlugin // FlutterPlugin
@@ -30,26 +30,24 @@ class FrameRateManager(
private var watchdogRunnable: Runnable? = null private var watchdogRunnable: Runnable? = null
private var pendingCompletion: ((switched: Boolean) -> Unit)? = null private var pendingCompletion: ((switched: Boolean) -> Unit)? = null
private fun getDisplayManager(): DisplayManager { private fun getDisplayManager(): DisplayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
return activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
}
/// Request a display frame-rate switch. Invokes [onComplete] once, either: // / Request a display frame-rate switch. Invokes [onComplete] once, either:
/// - immediately with `switched=false` when no switch is needed (invalid // / - immediately with `switched=false` when no switch is needed (invalid
/// fps, no matching mode, seamless fallback); or // / fps, no matching mode, seamless fallback); or
/// - after the real DisplayListener event + [DISPLAY_SETTLE_MS] + the // / - after the real DisplayListener event + [DISPLAY_SETTLE_MS] + the
/// caller's [extraDelayMs], with `switched=true`; or // / caller's [extraDelayMs], with `switched=true`; or
/// - via a watchdog with `switched=true` if the real event never arrives, // / - via a watchdog with `switched=true` if the real event never arrives,
/// so the caller doesn't hang. // / so the caller doesn't hang.
/// // /
/// The caller is responsible for pausing playback before calling and // / The caller is responsible for pausing playback before calling and
/// resuming it after [onComplete] fires. // / resuming it after [onComplete] fires.
fun setVideoFrameRate( fun setVideoFrameRate(
fps: Float, fps: Float,
videoDurationMs: Long, videoDurationMs: Long,
surface: Surface?, surface: Surface?,
extraDelayMs: Long, extraDelayMs: Long,
onComplete: (switched: Boolean) -> Unit, onComplete: (switched: Boolean) -> Unit
) { ) {
currentVideoFps = fps currentVideoFps = fps
if (fps <= 0f) { if (fps <= 0f) {
@@ -58,7 +56,7 @@ class FrameRateManager(
return return
} }
log("fps=$fps, duration=${videoDurationMs}ms, extraDelayMs=${extraDelayMs}, API=${Build.VERSION.SDK_INT}") log("fps=$fps, duration=${videoDurationMs}ms, extraDelayMs=$extraDelayMs, API=${Build.VERSION.SDK_INT}")
when { when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
@@ -157,7 +155,7 @@ class FrameRateManager(
surface: Surface, surface: Surface,
videoDurationMs: Long, videoDurationMs: Long,
extraDelayMs: Long, extraDelayMs: Long,
onComplete: (switched: Boolean) -> Unit, onComplete: (switched: Boolean) -> Unit
) { ) {
Log.d(TAG, "setFrameRateS: fps=$fps, duration=${videoDurationMs}ms") Log.d(TAG, "setFrameRateS: fps=$fps, duration=${videoDurationMs}ms")
@@ -192,7 +190,8 @@ class FrameRateManager(
for (rate in refreshRates) { for (rate in refreshRates) {
if (fps.toString().startsWith(rate.toString()) || if (fps.toString().startsWith(rate.toString()) ||
rate.toString().startsWith(fps.toString()) || rate.toString().startsWith(fps.toString()) ||
rate % fps == 0f) { rate % fps == 0f
) {
seamless = true seamless = true
break break
} }
@@ -233,6 +232,7 @@ class FrameRateManager(
private fun setFrameRateM(fps: Float, extraDelayMs: Long, onComplete: (switched: Boolean) -> Unit) { private fun setFrameRateM(fps: Float, extraDelayMs: Long, onComplete: (switched: Boolean) -> Unit) {
Log.d(TAG, "setFrameRateM: fps=$fps") Log.d(TAG, "setFrameRateM: fps=$fps")
val wm = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager val wm = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val display = wm.defaultDisplay val display = wm.defaultDisplay
if (display == null) { if (display == null) {
@@ -250,12 +250,14 @@ class FrameRateManager(
for (mode in supportedModes) { for (mode in supportedModes) {
if (mode.physicalHeight != currentMode.physicalHeight || if (mode.physicalHeight != currentMode.physicalHeight ||
mode.physicalWidth != currentMode.physicalWidth) { mode.physicalWidth != currentMode.physicalWidth
) {
continue continue
} }
if (BigDecimal(fps.toString()).setScale(1, RoundingMode.FLOOR) == if (BigDecimal(fps.toString()).setScale(1, RoundingMode.FLOOR) ==
BigDecimal(mode.refreshRate.toString()).setScale(1, RoundingMode.FLOOR)) { BigDecimal(mode.refreshRate.toString()).setScale(1, RoundingMode.FLOOR)
) {
modeToUse = mode modeToUse = mode
break break
} else if ((mode.refreshRate % fps).let { it < 0.1f || (fps - it) < 0.1f }) { } else if ((mode.refreshRate % fps).let { it < 0.1f || (fps - it) < 0.1f }) {
@@ -16,7 +16,9 @@ import java.util.concurrent.Executors
* Flutter plugin for Android TV Watch Next integration. * Flutter plugin for Android TV Watch Next integration.
* Syncs Plex "On Deck" content to the Android TV launcher's Watch Next row. * Syncs Plex "On Deck" content to the Android TV launcher's Watch Next row.
*/ */
class WatchNextPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { class WatchNextPlugin :
FlutterPlugin,
MethodChannel.MethodCallHandler {
companion object { companion object {
private const val TAG = "WatchNextPlugin" private const val TAG = "WatchNextPlugin"
@@ -38,8 +38,7 @@ class WatchNextProvider(private val context: Context) {
* Uses applyBatch to delete + insert in a single transaction so the * Uses applyBatch to delete + insert in a single transaction so the
* launcher receives one content-change notification with the full set. * launcher receives one content-change notification with the full set.
*/ */
fun syncWatchNextPrograms(items: List<WatchNextItem>): Boolean { fun syncWatchNextPrograms(items: List<WatchNextItem>): Boolean = try {
return try {
val ops = ArrayList<ContentProviderOperation>() val ops = ArrayList<ContentProviderOperation>()
ops.add( ops.add(
@@ -64,10 +63,8 @@ class WatchNextProvider(private val context: Context) {
Log.e(TAG, "Failed to sync Watch Next programs", e) Log.e(TAG, "Failed to sync Watch Next programs", e)
false false
} }
}
fun clearAll(): Boolean { fun clearAll(): Boolean = try {
return try {
context.contentResolver.delete( context.contentResolver.delete(
TvContractCompat.WatchNextPrograms.CONTENT_URI, TvContractCompat.WatchNextPrograms.CONTENT_URI,
null, null,
@@ -78,7 +75,6 @@ class WatchNextProvider(private val context: Context) {
Log.e(TAG, "Failed to clear Watch Next entries", e) Log.e(TAG, "Failed to clear Watch Next entries", e)
false false
} }
}
fun removeItem(contentId: String): Boolean { fun removeItem(contentId: String): Boolean {
return try { return try {
@@ -119,10 +115,11 @@ class WatchNextProvider(private val context: Context) {
} }
private fun buildProgram(item: WatchNextItem): WatchNextProgram { private fun buildProgram(item: WatchNextItem): WatchNextProgram {
val watchNextType = if (item.lastPlaybackPosition > 0) val watchNextType = if (item.lastPlaybackPosition > 0) {
TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE
else } else {
TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_NEXT TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_NEXT
}
val builder = WatchNextProgram.Builder() val builder = WatchNextProgram.Builder()
.setType(item.type) .setType(item.type)
+2 -2
View File
@@ -125,7 +125,7 @@ SPEC CHECKSUMS:
file_picker: 8fc6fe5e42585a217d44d22f79ec046cb8d81140 file_picker: 8fc6fe5e42585a217d44d22f79ec046cb8d81140
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937 in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
os_media_controls: 86dceab6245a5325af90fc0fdebe243c42d789b4 os_media_controls: 94cc278f5802b82b2d6373003aeb511f96718b27
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be
sentry_flutter: 31101687061fb85211ebab09ce6eb8db4e9ba74f sentry_flutter: 31101687061fb85211ebab09ce6eb8db4e9ba74f
@@ -133,7 +133,7 @@ SPEC CHECKSUMS:
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921 sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921
sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab
universal_gamepad: e10172778a8a399cce234494968f38724974919e universal_gamepad: 838bbb70d37d8c7c719038aa397214f2c4c4f866
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
+35 -29
View File
@@ -2,11 +2,11 @@ import AVKit
import UIKit import UIKit
#if os(tvOS) #if os(tvOS)
// tvOS stub: AVPictureInPictureController has different constraints on tvOS // tvOS stub: AVPictureInPictureController has different constraints on tvOS
// and is not supported by the Plezy flow. Provide a no-op shell so callers // and is not supported by the Plezy flow. Provide a no-op shell so callers
// in MpvPlayerPlugin compile unchanged; isSupported reports false so PiP is // in MpvPlayerPlugin compile unchanged; isSupported reports false so PiP is
// never attempted at runtime. // never attempted at runtime.
protocol MpvPipDelegate: AnyObject { protocol MpvPipDelegate: AnyObject {
func pipWillStart() func pipWillStart()
func pipDidStart() func pipDidStart()
func pipDidStop(restored: Bool) func pipDidStop(restored: Bool)
@@ -15,9 +15,9 @@ protocol MpvPipDelegate: AnyObject {
func pipSkip(byInterval seconds: Double) func pipSkip(byInterval seconds: Double)
var isPipPlaying: Bool { get } var isPipPlaying: Bool { get }
var pipDuration: Double { get } var pipDuration: Double { get }
} }
class MpvPipController: NSObject { class MpvPipController: NSObject {
static var isSupported: Bool { false } static var isSupported: Bool { false }
weak var delegate: MpvPipDelegate? weak var delegate: MpvPipDelegate?
var isPipActive: Bool { false } var isPipActive: Bool { false }
@@ -39,11 +39,11 @@ class MpvPipController: NSObject {
func flushLayer() {} func flushLayer() {}
func syncTimebase(currentTime: Double, isPlaying: Bool) {} func syncTimebase(currentTime: Double, isPlaying: Bool) {}
func teardown() {} func teardown() {}
} }
#else #else
/// Delegate to notify the plugin of PiP lifecycle events /// Delegate to notify the plugin of PiP lifecycle events
protocol MpvPipDelegate: AnyObject { protocol MpvPipDelegate: AnyObject {
/// Called when PiP is about to start (system or app-initiated) /// Called when PiP is about to start (system or app-initiated)
func pipWillStart() func pipWillStart()
func pipDidStart() func pipDidStart()
@@ -58,11 +58,11 @@ protocol MpvPipDelegate: AnyObject {
var isPipPlaying: Bool { get } var isPipPlaying: Bool { get }
/// Get total duration in seconds /// Get total duration in seconds
var pipDuration: Double { get } var pipDuration: Double { get }
} }
/// Encapsulates all iOS Picture-in-Picture logic using AVSampleBufferDisplayLayer. /// Encapsulates all iOS Picture-in-Picture logic using AVSampleBufferDisplayLayer.
/// Requires iOS 15+ for the ContentSource API; on older versions, `setup()` is a no-op. /// Requires iOS 15+ for the ContentSource API; on older versions, `setup()` is a no-op.
class MpvPipController: NSObject { class MpvPipController: NSObject {
// MARK: - Properties // MARK: - Properties
@@ -96,7 +96,8 @@ class MpvPipController: NSObject {
// The sample buffer layer must be in a visible view hierarchy for // The sample buffer layer must be in a visible view hierarchy for
// isPictureInPicturePossible to become true. // isPictureInPicturePossible to become true.
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first(where: { $0.isKeyWindow }) { let window = windowScene.windows.first(where: { $0.isKeyWindow })
{
let view = UIView(frame: window.bounds) let view = UIView(frame: window.bounds)
view.clipsToBounds = true view.clipsToBounds = true
view.isUserInteractionEnabled = false view.isUserInteractionEnabled = false
@@ -138,7 +139,7 @@ class MpvPipController: NSObject {
func pushBlankFrame(width: Int32 = 1920, height: Int32 = 1080) { func pushBlankFrame(width: Int32 = 1920, height: Int32 = 1080) {
var pixelBuffer: CVPixelBuffer? var pixelBuffer: CVPixelBuffer?
let attrs: [String: Any] = [ let attrs: [String: Any] = [
kCVPixelBufferIOSurfacePropertiesKey as String: [:], kCVPixelBufferIOSurfacePropertiesKey as String: [:]
] ]
let status = CVPixelBufferCreate( let status = CVPixelBufferCreate(
kCFAllocatorDefault, Int(width), Int(height), kCFAllocatorDefault, Int(width), Int(height),
@@ -187,8 +188,10 @@ class MpvPipController: NSObject {
guard let sb = sampleBuffer else { return } guard let sb = sampleBuffer else { return }
// Set DisplayImmediately so it shows regardless of timebase timing // Set DisplayImmediately so it shows regardless of timebase timing
if let attachments = CMSampleBufferGetSampleAttachmentsArray(sb, createIfNecessary: true) as? [NSMutableDictionary], if let attachments = CMSampleBufferGetSampleAttachmentsArray(
let dict = attachments.first { sb, createIfNecessary: true) as? [NSMutableDictionary],
let dict = attachments.first
{
dict[kCMSampleAttachmentKey_DisplayImmediately] = true dict[kCMSampleAttachmentKey_DisplayImmediately] = true
} }
@@ -259,7 +262,9 @@ class MpvPipController: NSObject {
attempts += 1 attempts += 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { tryStart() } DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { tryStart() }
} else { } else {
print("[MpvPipController] PiP not ready after \(attempts) retries (possible=\(possible), timebase=\(hasTimebase))") print(
"[MpvPipController] PiP not ready after \(attempts) retries (possible=\(possible), timebase=\(hasTimebase))"
)
completion(false) completion(false)
} }
} }
@@ -296,17 +301,17 @@ class MpvPipController: NSObject {
func flushLayer() { func flushLayer() {
sampleBufferLayer.flushAndRemoveImage() sampleBufferLayer.flushAndRemoveImage()
} }
} }
// MARK: - PiP Delegate Helper (iOS 15+) // MARK: - PiP Delegate Helper (iOS 15+)
/// Separate class conforming to AVPictureInPictureControllerDelegate and /// Separate class conforming to AVPictureInPictureControllerDelegate and
/// AVPictureInPictureSampleBufferPlaybackDelegate since these require iOS 15+ /// AVPictureInPictureSampleBufferPlaybackDelegate since these require iOS 15+
/// availability for the ContentSource-based delegate methods. /// availability for the ContentSource-based delegate methods.
@available(iOS 15.0, *) @available(iOS 15.0, *)
private class PipDelegateHelper: NSObject, AVPictureInPictureControllerDelegate, private class PipDelegateHelper: NSObject, AVPictureInPictureControllerDelegate,
AVPictureInPictureSampleBufferPlaybackDelegate AVPictureInPictureSampleBufferPlaybackDelegate
{ {
weak var controller: MpvPipController? weak var controller: MpvPipController?
private var isRestoring = false private var isRestoring = false
@@ -350,7 +355,8 @@ private class PipDelegateHelper: NSObject, AVPictureInPictureControllerDelegate,
func pictureInPictureController( func pictureInPictureController(
_ pictureInPictureController: AVPictureInPictureController, _ pictureInPictureController: AVPictureInPictureController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler:
@escaping (Bool) -> Void
) { ) {
print("[MpvPipController] PiP restore user interface") print("[MpvPipController] PiP restore user interface")
isRestoring = true isRestoring = true
@@ -407,6 +413,6 @@ private class PipDelegateHelper: NSObject, AVPictureInPictureControllerDelegate,
controller?.delegate?.pipSkip(byInterval: seconds) controller?.delegate?.pipSkip(byInterval: seconds)
completionHandler() completionHandler()
} }
} }
#endif // !os(tvOS) #endif // !os(tvOS)
+38 -14
View File
@@ -53,7 +53,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
// MARK: - FlutterStreamHandler // MARK: - FlutterStreamHandler
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
-> FlutterError?
{
self.eventSink = events self.eventSink = events
return nil return nil
} }
@@ -115,7 +117,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
private func unregisterSceneActivationObserver() { private func unregisterSceneActivationObserver() {
guard sceneActivationObserverRegistered else { return } guard sceneActivationObserverRegistered else { return }
NotificationCenter.default.removeObserver(self, name: UIScene.didActivateNotification, object: nil) NotificationCenter.default.removeObserver(
self, name: UIScene.didActivateNotification, object: nil)
sceneActivationObserverRegistered = false sceneActivationObserverRegistered = false
} }
@@ -127,7 +130,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
guard pendingInlineRestoreAfterPip, guard pendingInlineRestoreAfterPip,
let playerCore = playerCore, let playerCore = playerCore,
!playerCore.isPipActive, !playerCore.isPipActive,
!playerCore.isPipStarting else { return } !playerCore.isPipStarting
else { return }
print("[MpvPlayerPlugin] Restoring inline player after PiP") print("[MpvPlayerPlugin] Restoring inline player after PiP")
playerCore.setVisible(true) playerCore.setVisible(true)
@@ -186,15 +190,22 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
/// canStartPictureInPictureAutomaticallyFromInline + pipWillStart delegate. /// canStartPictureInPictureAutomaticallyFromInline + pipWillStart delegate.
private func enterPip(manual: Bool, result: FlutterResult? = nil) { private func enterPip(manual: Bool, result: FlutterResult? = nil) {
guard MpvPipController.isSupported else { guard MpvPipController.isSupported else {
result?(["success": false, "errorCode": "ios_version", "errorMessage": "Requires iOS 15.0+"]) result?([
"success": false, "errorCode": "ios_version", "errorMessage": "Requires iOS 15.0+",
])
return return
} }
guard playerCore != nil else { guard playerCore != nil else {
result?(["success": false, "errorCode": "failed", "errorMessage": "Player not initialized"]) result?([
"success": false, "errorCode": "failed", "errorMessage": "Player not initialized",
])
return return
} }
guard let pip = switchToPipAndPrepare() else { guard let pip = switchToPipAndPrepare() else {
result?(["success": false, "errorCode": "vo_switch_failed", "errorMessage": "Failed to switch VO"]) result?([
"success": false, "errorCode": "vo_switch_failed",
"errorMessage": "Failed to switch VO",
])
return return
} }
@@ -204,7 +215,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
result?(["success": true]) result?(["success": true])
} else { } else {
self?.cleanupPip(notify: false) self?.cleanupPip(notify: false)
result?(["success": false, "errorCode": "failed", "errorMessage": "PiP failed to start"]) result?([
"success": false, "errorCode": "failed", "errorMessage": "PiP failed to start",
])
} }
} }
} }
@@ -250,7 +263,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
private func startPipTimebaseSync() { private func startPipTimebaseSync() {
stopPipTimebaseSync() stopPipTimebaseSync()
pipTimebaseSyncTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in pipTimebaseSyncTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) {
[weak self] _ in
self?.syncPipTimebase() self?.syncPipTimebase()
} }
} }
@@ -276,7 +290,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
} }
guard let window = self.findKeyWindow() else { guard let window = self.findKeyWindow() else {
result(FlutterError(code: "NO_WINDOW", message: "Could not find key window", details: nil)) result(
FlutterError(
code: "NO_WINDOW", message: "Could not find key window", details: nil))
return return
} }
@@ -284,7 +300,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
core.delegate = self core.delegate = self
guard core.initialize(in: window) else { guard core.initialize(in: window) else {
result(FlutterError(code: "MPV_INIT_FAILED", message: "Failed to initialize MPV", details: nil)) result(
FlutterError(
code: "MPV_INIT_FAILED", message: "Failed to initialize MPV", details: nil))
return return
} }
@@ -313,8 +331,12 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
private func handleSetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) { private func handleSetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let name = args["name"] as? String, let name = args["name"] as? String,
let value = args["value"] as? String else { let value = args["value"] as? String
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' or 'value' argument", details: nil)) else {
result(
FlutterError(
code: "INVALID_ARGS", message: "Missing 'name' or 'value' argument",
details: nil))
return return
} }
@@ -332,7 +354,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
private func findKeyWindow() -> UIWindow? { private func findKeyWindow() -> UIWindow? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first(where: { $0.isKeyWindow }) else { let window = windowScene.windows.first(where: { $0.isKeyWindow })
else {
return nil return nil
} }
return window return window
@@ -363,7 +386,8 @@ extension MpvPlayerPlugin: MpvPipDelegate {
if isManualPipRequest { if isManualPipRequest {
isManualPipRequest = false isManualPipRequest = false
UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil) UIControl().sendAction(
#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
} }
} }
+20 -35
View File
@@ -1,8 +1,8 @@
#include "mpv_player.h" #include "mpv_player.h"
#include <flutter_linux/flutter_linux.h>
#include <epoxy/gl.h>
#include <epoxy/egl.h> #include <epoxy/egl.h>
#include <epoxy/gl.h>
#include <flutter_linux/flutter_linux.h>
#include <gdk/gdk.h> #include <gdk/gdk.h>
#ifdef GDK_WINDOWING_X11 #ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h> #include <gdk/gdkx.h>
@@ -24,9 +24,7 @@ namespace mpv {
MpvPlayer::MpvPlayer() {} MpvPlayer::MpvPlayer() {}
MpvPlayer::~MpvPlayer() { MpvPlayer::~MpvPlayer() { Dispose(); }
Dispose();
}
bool MpvPlayer::Initialize() { bool MpvPlayer::Initialize() {
if (mpv_) { if (mpv_) {
@@ -111,7 +109,7 @@ bool MpvPlayer::InitRenderContext() {
} }
EGLint num_configs = 0; EGLint num_configs = 0;
EGLint config_attribs[] = { EGL_CONFIG_ID, config_id, EGL_NONE }; EGLint config_attribs[] = {EGL_CONFIG_ID, config_id, EGL_NONE};
if (!eglChooseConfig(egl_display_, config_attribs, &config, 1, &num_configs) || num_configs == 0) { if (!eglChooseConfig(egl_display_, config_attribs, &config, 1, &num_configs) || num_configs == 0) {
g_warning("MPV: Failed to get Flutter's EGL config"); g_warning("MPV: Failed to get Flutter's EGL config");
return false; return false;
@@ -121,7 +119,8 @@ bool MpvPlayer::InitRenderContext() {
// GL state pollution // GL state pollution
eglBindAPI(EGL_OPENGL_ES_API); eglBindAPI(EGL_OPENGL_ES_API);
EGLint context_attribs[] = { EGLint context_attribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 2, EGL_CONTEXT_CLIENT_VERSION,
2,
EGL_NONE, EGL_NONE,
}; };
egl_context_ = eglCreateContext(egl_display_, config, EGL_NO_CONTEXT, context_attribs); egl_context_ = eglCreateContext(egl_display_, config, EGL_NO_CONTEXT, context_attribs);
@@ -142,8 +141,7 @@ bool MpvPlayer::InitRenderContext() {
}; };
mpv_render_param params[] = { mpv_render_param params[] = {
{MPV_RENDER_PARAM_API_TYPE, {MPV_RENDER_PARAM_API_TYPE, const_cast<char*>(MPV_RENDER_API_TYPE_OPENGL)},
const_cast<char*>(MPV_RENDER_API_TYPE_OPENGL)},
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params}, {MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params},
{MPV_RENDER_PARAM_INVALID, nullptr}, // slot for X11/Wayland display {MPV_RENDER_PARAM_INVALID, nullptr}, // slot for X11/Wayland display
{MPV_RENDER_PARAM_INVALID, nullptr}, {MPV_RENDER_PARAM_INVALID, nullptr},
@@ -170,8 +168,7 @@ bool MpvPlayer::InitRenderContext() {
eglMakeCurrent(egl_display_, flutter_draw, flutter_read, flutter_context); eglMakeCurrent(egl_display_, flutter_draw, flutter_read, flutter_context);
if (err < 0) { if (err < 0) {
g_warning("MPV: mpv_render_context_create() failed: %s", g_warning("MPV: mpv_render_context_create() failed: %s", mpv_error_string(err));
mpv_error_string(err));
eglDestroyContext(egl_display_, egl_context_); eglDestroyContext(egl_display_, egl_context_);
egl_context_ = EGL_NO_CONTEXT; egl_context_ = EGL_NO_CONTEXT;
return false; return false;
@@ -287,8 +284,7 @@ void MpvPlayer::Command(const std::vector<std::string>& args) {
mpv_command(mpv_, c_args.data()); mpv_command(mpv_, c_args.data());
} }
void MpvPlayer::CommandAsync(const std::vector<std::string>& args, void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallback callback) {
CommandCallback callback) {
if (disposed_ || !mpv_) { if (disposed_ || !mpv_) {
if (callback) callback(0); if (callback) callback(0);
return; return;
@@ -336,9 +332,7 @@ std::string MpvPlayer::GetProperty(const std::string& name) {
return result; return result;
} }
void MpvPlayer::ObserveProperty(const std::string& name, void MpvPlayer::ObserveProperty(const std::string& name, const std::string& format, int id) {
const std::string& format,
int id) {
if (disposed_ || !mpv_) return; if (disposed_ || !mpv_) return;
if (observed_properties_.find(name) != observed_properties_.end()) { if (observed_properties_.find(name) != observed_properties_.end()) {
@@ -403,8 +397,7 @@ void MpvPlayer::OnMpvWakeup(void* ctx) {
} }
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
}, },
player, player, nullptr);
nullptr);
} }
void MpvPlayer::OnMpvRenderUpdate(void* ctx) { void MpvPlayer::OnMpvRenderUpdate(void* ctx) {
@@ -482,12 +475,9 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
g_message("MPV [%s] %s: %s", msg->level, msg->prefix, msg->text); g_message("MPV [%s] %s: %s", msg->level, msg->prefix, msg->text);
FlValue* data = fl_value_new_map(); FlValue* data = fl_value_new_map();
fl_value_set_string_take(data, "prefix", fl_value_set_string_take(data, "prefix", fl_value_new_string(SanitizeUtf8(msg->prefix).c_str()));
fl_value_new_string(SanitizeUtf8(msg->prefix).c_str())); fl_value_set_string_take(data, "level", fl_value_new_string(SanitizeUtf8(msg->level).c_str()));
fl_value_set_string_take(data, "level", fl_value_set_string_take(data, "text", fl_value_new_string(SanitizeUtf8(msg->text).c_str()));
fl_value_new_string(SanitizeUtf8(msg->level).c_str()));
fl_value_set_string_take(data, "text",
fl_value_new_string(SanitizeUtf8(msg->text).c_str()));
SendEvent("log-message", data); SendEvent("log-message", data);
fl_value_unref(data); fl_value_unref(data);
break; break;
@@ -499,8 +489,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
switch (prop->format) { switch (prop->format) {
case MPV_FORMAT_STRING: case MPV_FORMAT_STRING:
node.u.string = node.u.string = prop->data ? *static_cast<char**>(prop->data) : nullptr;
prop->data ? *static_cast<char**>(prop->data) : nullptr;
break; break;
case MPV_FORMAT_FLAG: case MPV_FORMAT_FLAG:
node.u.flag = prop->data ? *static_cast<int*>(prop->data) : 0; node.u.flag = prop->data ? *static_cast<int*>(prop->data) : 0;
@@ -527,13 +516,11 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
case MPV_EVENT_END_FILE: { case MPV_EVENT_END_FILE: {
auto* end = static_cast<mpv_event_end_file*>(event->data); auto* end = static_cast<mpv_event_end_file*>(event->data);
FlValue* data = fl_value_new_map(); FlValue* data = fl_value_new_map();
fl_value_set_string_take(data, "reason", fl_value_set_string_take(data, "reason", fl_value_new_int(static_cast<int>(end->reason)));
fl_value_new_int(static_cast<int>(end->reason)));
if (end->reason == MPV_END_FILE_REASON_ERROR) { if (end->reason == MPV_END_FILE_REASON_ERROR) {
fl_value_set_string_take(data, "error", fl_value_set_string_take(data, "error", fl_value_new_int(static_cast<int>(end->error)));
fl_value_new_int(static_cast<int>(end->error))); fl_value_set_string_take(
fl_value_set_string_take(data, "message", data, "message", fl_value_new_string(SanitizeUtf8(mpv_error_string(end->error)).c_str()));
fl_value_new_string(SanitizeUtf8(mpv_error_string(end->error)).c_str()));
} }
SendEvent("end-file", data); SendEvent("end-file", data);
fl_value_unref(data); fl_value_unref(data);
@@ -578,9 +565,7 @@ FlValue* MpvPlayer::NodeToFlValue(mpv_node* node) {
case MPV_FORMAT_NODE_MAP: { case MPV_FORMAT_NODE_MAP: {
FlValue* map = fl_value_new_map(); FlValue* map = fl_value_new_map();
for (int i = 0; i < node->u.list->num; i++) { for (int i = 0; i < node->u.list->num; i++) {
fl_value_set_string_take( fl_value_set_string_take(map, node->u.list->keys[i], NodeToFlValue(&node->u.list->values[i]));
map, node->u.list->keys[i],
NodeToFlValue(&node->u.list->values[i]));
} }
return map; return map;
} }
+4 -5
View File
@@ -1,12 +1,12 @@
#ifndef MPV_PLAYER_H_ #ifndef MPV_PLAYER_H_
#define MPV_PLAYER_H_ #define MPV_PLAYER_H_
#include <epoxy/egl.h>
#include <epoxy/gl.h>
#include <gtk/gtk.h>
#include <mpv/client.h> #include <mpv/client.h>
#include <mpv/render.h> #include <mpv/render.h>
#include <mpv/render_gl.h> #include <mpv/render_gl.h>
#include <gtk/gtk.h>
#include <epoxy/gl.h>
#include <epoxy/egl.h>
#include <atomic> #include <atomic>
#include <functional> #include <functional>
@@ -84,8 +84,7 @@ class MpvPlayer {
std::string GetProperty(const std::string& name); std::string GetProperty(const std::string& name);
/// Observes an mpv property for changes. /// Observes an mpv property for changes.
void ObserveProperty(const std::string& name, const std::string& format, void ObserveProperty(const std::string& name, const std::string& format, int id);
int id);
/// Renders a frame to the specified FBO. /// Renders a frame to the specified FBO.
void Render(int width, int height, int fbo = 0); void Render(int width, int height, int fbo = 0);
+54 -106
View File
@@ -1,8 +1,9 @@
#include "mpv_plugin.h" #include "mpv_plugin.h"
#include "mpv_texture.h"
#include <cstring> #include <cstring>
#include "mpv_texture.h"
struct _MpvPlugin { struct _MpvPlugin {
GObject parent_instance; GObject parent_instance;
@@ -20,9 +21,7 @@ struct _MpvPlugin {
G_DEFINE_TYPE(MpvPlugin, mpv_plugin, G_TYPE_OBJECT) G_DEFINE_TYPE(MpvPlugin, mpv_plugin, G_TYPE_OBJECT)
// Forward declarations // Forward declarations
static void mpv_plugin_handle_method_call(FlMethodChannel* channel, static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data);
FlMethodCall* method_call,
gpointer user_data);
static void send_event(MpvPlugin* self, FlValue* event) { static void send_event(MpvPlugin* self, FlValue* event) {
if (self->event_channel) { if (self->event_channel) {
@@ -43,8 +42,7 @@ static void mpv_plugin_dispose(GObject* object) {
if (self->texture) { if (self->texture) {
mpv_texture_dispose(self->texture); mpv_texture_dispose(self->texture);
if (self->texture_registrar) { if (self->texture_registrar) {
fl_texture_registrar_unregister_texture(self->texture_registrar, fl_texture_registrar_unregister_texture(self->texture_registrar, FL_TEXTURE(self->texture));
FL_TEXTURE(self->texture));
} }
g_object_unref(self->texture); g_object_unref(self->texture);
self->texture = nullptr; self->texture = nullptr;
@@ -62,9 +60,7 @@ static void mpv_plugin_dispose(GObject* object) {
G_OBJECT_CLASS(mpv_plugin_parent_class)->dispose(object); G_OBJECT_CLASS(mpv_plugin_parent_class)->dispose(object);
} }
static void mpv_plugin_class_init(MpvPluginClass* klass) { static void mpv_plugin_class_init(MpvPluginClass* klass) { G_OBJECT_CLASS(klass)->dispose = mpv_plugin_dispose; }
G_OBJECT_CLASS(klass)->dispose = mpv_plugin_dispose;
}
static void mpv_plugin_init(MpvPlugin* self) { static void mpv_plugin_init(MpvPlugin* self) {
self->visible = FALSE; self->visible = FALSE;
@@ -77,26 +73,17 @@ MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar) {
MpvPlugin* self = MPV_PLUGIN(g_object_new(MPV_PLUGIN_TYPE, nullptr)); MpvPlugin* self = MPV_PLUGIN(g_object_new(MPV_PLUGIN_TYPE, nullptr));
self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar)); self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar));
self->texture_registrar = self->texture_registrar = fl_plugin_registrar_get_texture_registrar(registrar);
fl_plugin_registrar_get_texture_registrar(registrar);
self->player = std::make_unique<mpv::MpvPlayer>(); self->player = std::make_unique<mpv::MpvPlayer>();
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
self->method_channel = fl_method_channel_new( self->method_channel = fl_method_channel_new(
fl_plugin_registrar_get_messenger(registrar), fl_plugin_registrar_get_messenger(registrar), "com.plezy/mpv_player", FL_METHOD_CODEC(codec));
"com.plezy/mpv_player",
FL_METHOD_CODEC(codec));
fl_method_channel_set_method_call_handler( fl_method_channel_set_method_call_handler(self->method_channel, mpv_plugin_handle_method_call, self, nullptr);
self->method_channel,
mpv_plugin_handle_method_call,
self,
nullptr);
self->event_channel = fl_event_channel_new( self->event_channel = fl_event_channel_new(
fl_plugin_registrar_get_messenger(registrar), fl_plugin_registrar_get_messenger(registrar), "com.plezy/mpv_player/events", FL_METHOD_CODEC(codec));
"com.plezy/mpv_player/events",
FL_METHOD_CODEC(codec));
return self; return self;
} }
@@ -104,14 +91,10 @@ MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar) {
// Static reference to keep the plugin alive. // Static reference to keep the plugin alive.
static MpvPlugin* g_mpv_plugin = nullptr; static MpvPlugin* g_mpv_plugin = nullptr;
void mpv_plugin_register_with_registrar(FlPluginRegistrar* registrar) { void mpv_plugin_register_with_registrar(FlPluginRegistrar* registrar) { g_mpv_plugin = mpv_plugin_new(registrar); }
g_mpv_plugin = mpv_plugin_new(registrar);
}
/// Method call handler. /// Method call handler.
static void mpv_plugin_handle_method_call(FlMethodChannel* channel, static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data) {
FlMethodCall* method_call,
gpointer user_data) {
(void)channel; (void)channel;
MpvPlugin* self = MPV_PLUGIN(user_data); MpvPlugin* self = MPV_PLUGIN(user_data);
const gchar* method = fl_method_call_get_name(method_call); const gchar* method = fl_method_call_get_name(method_call);
@@ -122,8 +105,8 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
if (strcmp(method, "initialize") == 0) { if (strcmp(method, "initialize") == 0) {
if (self->initialized && self->texture) { if (self->initialized && self->texture) {
// Already initialized — return existing texture ID // Already initialized — return existing texture ID
response = FL_METHOD_RESPONSE(fl_method_success_response_new( response =
fl_value_new_int(mpv_texture_get_id(self->texture)))); FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_int(mpv_texture_get_id(self->texture))));
} else { } else {
// Create player if it was disposed or doesn't exist // Create player if it was disposed or doesn't exist
if (!self->player || self->player->IsDisposed()) { if (!self->player || self->player->IsDisposed()) {
@@ -133,11 +116,9 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
if (self->player->Initialize()) { if (self->player->Initialize()) {
// Create the FlTextureGL and register it // Create the FlTextureGL and register it
FlView* view = fl_plugin_registrar_get_view(self->registrar); FlView* view = fl_plugin_registrar_get_view(self->registrar);
self->texture = mpv_texture_new( self->texture = mpv_texture_new(self->player.get(), self->texture_registrar, view);
self->player.get(), self->texture_registrar, view);
fl_texture_registrar_register_texture( fl_texture_registrar_register_texture(self->texture_registrar, FL_TEXTURE(self->texture));
self->texture_registrar, FL_TEXTURE(self->texture));
// Create the render context eagerly — mpv needs it BEFORE any // Create the render context eagerly — mpv needs it BEFORE any
// file is loaded, otherwise VO init fails with "No render context // file is loaded, otherwise VO init fails with "No render context
@@ -146,23 +127,19 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
// Set redraw callback: when mpv has a frame, mark texture available // Set redraw callback: when mpv has a frame, mark texture available
MpvTexture* tex = self->texture; MpvTexture* tex = self->texture;
self->player->SetRedrawCallback([tex]() { self->player->SetRedrawCallback([tex]() { mpv_texture_mark_frame_available(tex); });
mpv_texture_mark_frame_available(tex);
});
self->initialized = TRUE; self->initialized = TRUE;
// Set up event callback // Set up event callback
self->player->SetEventCallback([self](FlValue* event) { self->player->SetEventCallback([self](FlValue* event) { send_event(self, event); });
send_event(self, event);
});
// Return the texture ID for the Dart Texture widget // Return the texture ID for the Dart Texture widget
response = FL_METHOD_RESPONSE(fl_method_success_response_new( response =
fl_value_new_int(mpv_texture_get_id(self->texture)))); FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_int(mpv_texture_get_id(self->texture))));
} else { } else {
response = FL_METHOD_RESPONSE(fl_method_error_response_new( response =
"INIT_FAILED", "Failed to initialize MPV player", nullptr)); FL_METHOD_RESPONSE(fl_method_error_response_new("INIT_FAILED", "Failed to initialize MPV player", nullptr));
} }
} }
} else if (strcmp(method, "dispose") == 0) { } else if (strcmp(method, "dispose") == 0) {
@@ -171,8 +148,7 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
// during player disposal. // during player disposal.
if (self->texture) { if (self->texture) {
mpv_texture_dispose(self->texture); mpv_texture_dispose(self->texture);
fl_texture_registrar_unregister_texture(self->texture_registrar, fl_texture_registrar_unregister_texture(self->texture_registrar, FL_TEXTURE(self->texture));
FL_TEXTURE(self->texture));
g_object_unref(self->texture); g_object_unref(self->texture);
self->texture = nullptr; self->texture = nullptr;
} }
@@ -186,14 +162,11 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
} else if (strcmp(method, "command") == 0) { } else if (strcmp(method, "command") == 0) {
if (!self->player || !self->initialized) { if (!self->player || !self->initialized) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new( response = FL_METHOD_RESPONSE(fl_method_error_response_new("NOT_INITIALIZED", "Player not initialized", nullptr));
"NOT_INITIALIZED", "Player not initialized", nullptr));
} else { } else {
FlValue* args_value = fl_value_lookup_string(args, "args"); FlValue* args_value = fl_value_lookup_string(args, "args");
if (args_value == nullptr || if (args_value == nullptr || fl_value_get_type(args_value) != FL_VALUE_TYPE_LIST) {
fl_value_get_type(args_value) != FL_VALUE_TYPE_LIST) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'args' list", nullptr));
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"INVALID_ARGS", "Missing 'args' list", nullptr));
} else { } else {
std::vector<std::string> command_args; std::vector<std::string> command_args;
size_t len = fl_value_get_length(args_value); size_t len = fl_value_get_length(args_value);
@@ -207,8 +180,8 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
self->player->CommandAsync(command_args, [method_call](int error) { self->player->CommandAsync(command_args, [method_call](int error) {
g_autoptr(FlMethodResponse) async_response = nullptr; g_autoptr(FlMethodResponse) async_response = nullptr;
if (error < 0) { if (error < 0) {
async_response = FL_METHOD_RESPONSE(fl_method_error_response_new( async_response =
"COMMAND_FAILED", "MPV command failed", nullptr)); FL_METHOD_RESPONSE(fl_method_error_response_new("COMMAND_FAILED", "MPV command failed", nullptr));
} else { } else {
async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
} }
@@ -220,37 +193,28 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
} }
} else if (strcmp(method, "setProperty") == 0) { } else if (strcmp(method, "setProperty") == 0) {
if (!self->player || !self->initialized) { if (!self->player || !self->initialized) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new( response = FL_METHOD_RESPONSE(fl_method_error_response_new("NOT_INITIALIZED", "Player not initialized", nullptr));
"NOT_INITIALIZED", "Player not initialized", nullptr));
} else { } else {
FlValue* name_value = fl_value_lookup_string(args, "name"); FlValue* name_value = fl_value_lookup_string(args, "name");
FlValue* value_value = fl_value_lookup_string(args, "value"); FlValue* value_value = fl_value_lookup_string(args, "value");
if (name_value == nullptr || if (name_value == nullptr || fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) {
fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'name'", nullptr));
response = FL_METHOD_RESPONSE(fl_method_error_response_new( } else if (value_value == nullptr || fl_value_get_type(value_value) != FL_VALUE_TYPE_STRING) {
"INVALID_ARGS", "Missing 'name'", nullptr)); response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'value'", nullptr));
} else if (value_value == nullptr ||
fl_value_get_type(value_value) != FL_VALUE_TYPE_STRING) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"INVALID_ARGS", "Missing 'value'", nullptr));
} else { } else {
self->player->SetProperty(fl_value_get_string(name_value), self->player->SetProperty(fl_value_get_string(name_value), fl_value_get_string(value_value));
fl_value_get_string(value_value));
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
} }
} }
} else if (strcmp(method, "setLogLevel") == 0) { } else if (strcmp(method, "setLogLevel") == 0) {
if (!self->player || !self->initialized) { if (!self->player || !self->initialized) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new( response = FL_METHOD_RESPONSE(fl_method_error_response_new("NOT_INITIALIZED", "Player not initialized", nullptr));
"NOT_INITIALIZED", "Player not initialized", nullptr));
} else { } else {
FlValue* level_value = fl_value_lookup_string(args, "level"); FlValue* level_value = fl_value_lookup_string(args, "level");
if (level_value == nullptr || if (level_value == nullptr || fl_value_get_type(level_value) != FL_VALUE_TYPE_STRING) {
fl_value_get_type(level_value) != FL_VALUE_TYPE_STRING) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'level'", nullptr));
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"INVALID_ARGS", "Missing 'level'", nullptr));
} else { } else {
self->player->SetLogLevel(fl_value_get_string(level_value)); self->player->SetLogLevel(fl_value_get_string(level_value));
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
@@ -258,51 +222,38 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
} }
} else if (strcmp(method, "getProperty") == 0) { } else if (strcmp(method, "getProperty") == 0) {
if (!self->player || !self->initialized) { if (!self->player || !self->initialized) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new( response = FL_METHOD_RESPONSE(fl_method_error_response_new("NOT_INITIALIZED", "Player not initialized", nullptr));
"NOT_INITIALIZED", "Player not initialized", nullptr));
} else { } else {
FlValue* name_value = fl_value_lookup_string(args, "name"); FlValue* name_value = fl_value_lookup_string(args, "name");
if (name_value == nullptr || if (name_value == nullptr || fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) {
fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'name'", nullptr));
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"INVALID_ARGS", "Missing 'name'", nullptr));
} else { } else {
std::string value = std::string value = self->player->GetProperty(fl_value_get_string(name_value));
self->player->GetProperty(fl_value_get_string(name_value));
if (value.empty()) { if (value.empty()) {
response = response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
} else { } else {
response = FL_METHOD_RESPONSE(fl_method_success_response_new( response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_string(value.c_str())));
fl_value_new_string(value.c_str())));
} }
} }
} }
} else if (strcmp(method, "observeProperty") == 0) { } else if (strcmp(method, "observeProperty") == 0) {
if (!self->player || !self->initialized) { if (!self->player || !self->initialized) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new( response = FL_METHOD_RESPONSE(fl_method_error_response_new("NOT_INITIALIZED", "Player not initialized", nullptr));
"NOT_INITIALIZED", "Player not initialized", nullptr));
} else { } else {
FlValue* name_value = fl_value_lookup_string(args, "name"); FlValue* name_value = fl_value_lookup_string(args, "name");
FlValue* format_value = fl_value_lookup_string(args, "format"); FlValue* format_value = fl_value_lookup_string(args, "format");
FlValue* id_value = fl_value_lookup_string(args, "id"); FlValue* id_value = fl_value_lookup_string(args, "id");
if (name_value == nullptr || if (name_value == nullptr || fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) {
fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'name'", nullptr));
response = FL_METHOD_RESPONSE(fl_method_error_response_new( } else if (format_value == nullptr || fl_value_get_type(format_value) != FL_VALUE_TYPE_STRING) {
"INVALID_ARGS", "Missing 'name'", nullptr)); response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'format'", nullptr));
} else if (format_value == nullptr || } else if (id_value == nullptr || fl_value_get_type(id_value) != FL_VALUE_TYPE_INT) {
fl_value_get_type(format_value) != FL_VALUE_TYPE_STRING) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'id'", nullptr));
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"INVALID_ARGS", "Missing 'format'", nullptr));
} else if (id_value == nullptr ||
fl_value_get_type(id_value) != FL_VALUE_TYPE_INT) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"INVALID_ARGS", "Missing 'id'", nullptr));
} else { } else {
self->player->ObserveProperty(fl_value_get_string(name_value), self->player->ObserveProperty(
fl_value_get_string(format_value), fl_value_get_string(name_value), fl_value_get_string(format_value),
static_cast<int>(fl_value_get_int(id_value))); static_cast<int>(fl_value_get_int(id_value)));
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
} }
@@ -310,10 +261,8 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
} else if (strcmp(method, "setVisible") == 0) { } else if (strcmp(method, "setVisible") == 0) {
FlValue* visible_value = fl_value_lookup_string(args, "visible"); FlValue* visible_value = fl_value_lookup_string(args, "visible");
if (visible_value == nullptr || if (visible_value == nullptr || fl_value_get_type(visible_value) != FL_VALUE_TYPE_BOOL) {
fl_value_get_type(visible_value) != FL_VALUE_TYPE_BOOL) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'visible'", nullptr));
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"INVALID_ARGS", "Missing 'visible'", nullptr));
} else { } else {
self->visible = fl_value_get_bool(visible_value); self->visible = fl_value_get_bool(visible_value);
@@ -331,8 +280,7 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
} else if (strcmp(method, "isInitialized") == 0) { } else if (strcmp(method, "isInitialized") == 0) {
gboolean initialized = self->player && self->initialized; gboolean initialized = self->player && self->initialized;
response = FL_METHOD_RESPONSE( response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(initialized)));
fl_method_success_response_new(fl_value_new_bool(initialized)));
} else { } else {
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
} }
+13 -25
View File
@@ -1,7 +1,7 @@
#include "mpv_texture.h" #include "mpv_texture.h"
#include <epoxy/gl.h>
#include <epoxy/egl.h> #include <epoxy/egl.h>
#include <epoxy/gl.h>
// EGLImage extension function pointers // EGLImage extension function pointers
typedef EGLImageKHR (*PFNEGLCREATEIMAGEKHRPROC)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*); typedef EGLImageKHR (*PFNEGLCREATEIMAGEKHRPROC)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
@@ -17,7 +17,8 @@ static void init_egl_image_extensions() {
if (!initialized) { if (!initialized) {
_eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR"); _eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
_eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR"); _eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
_glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES"); _glEGLImageTargetTexture2DOES =
(PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
initialized = true; initialized = true;
} }
} }
@@ -84,19 +85,16 @@ static void ensure_textures(MpvTexture* self, int32_t w, int32_t h) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
GL_UNSIGNED_BYTE, nullptr);
glGenFramebuffers(1, &self->mpv_fbo); glGenFramebuffers(1, &self->mpv_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, self->mpv_fbo); glBindFramebuffer(GL_FRAMEBUFFER, self->mpv_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->mpv_texture, 0);
GL_TEXTURE_2D, self->mpv_texture, 0);
// Create EGLImage from mpv's texture for cross-context sharing // Create EGLImage from mpv's texture for cross-context sharing
EGLint image_attribs[] = { EGL_NONE }; EGLint image_attribs[] = {EGL_NONE};
self->egl_image = _eglCreateImageKHR( self->egl_image = _eglCreateImageKHR(
egl_display, egl_context, EGL_GL_TEXTURE_2D_KHR, egl_display, egl_context, EGL_GL_TEXTURE_2D_KHR, (EGLClientBuffer)(uintptr_t)self->mpv_texture, image_attribs);
(EGLClientBuffer)(uintptr_t)self->mpv_texture, image_attribs);
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
@@ -121,12 +119,8 @@ static void ensure_textures(MpvTexture* self, int32_t w, int32_t h) {
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
} }
static gboolean mpv_texture_populate(FlTextureGL* gl_texture, static gboolean mpv_texture_populate(
uint32_t* target, FlTextureGL* gl_texture, uint32_t* target, uint32_t* name, uint32_t* width, uint32_t* height, GError** error) {
uint32_t* name,
uint32_t* width,
uint32_t* height,
GError** error) {
MpvTexture* self = MPV_TEXTURE(gl_texture); MpvTexture* self = MPV_TEXTURE(gl_texture);
if (!self->player) { if (!self->player) {
@@ -137,8 +131,7 @@ static gboolean mpv_texture_populate(FlTextureGL* gl_texture,
// since Flutter's GL context is current here. // since Flutter's GL context is current here.
if (!self->player->HasRenderContext()) { if (!self->player->HasRenderContext()) {
if (!self->player->InitRenderContext()) { if (!self->player->InitRenderContext()) {
g_set_error(error, g_quark_from_static_string("mpv"), 0, g_set_error(error, g_quark_from_static_string("mpv"), 0, "Failed to create mpv render context");
"Failed to create mpv render context");
return FALSE; return FALSE;
} }
} }
@@ -201,9 +194,7 @@ static void mpv_texture_init(MpvTexture* self) {
self->height = 0; self->height = 0;
} }
MpvTexture* mpv_texture_new(mpv::MpvPlayer* player, MpvTexture* mpv_texture_new(mpv::MpvPlayer* player, FlTextureRegistrar* registrar, FlView* view) {
FlTextureRegistrar* registrar,
FlView* view) {
init_egl_image_extensions(); init_egl_image_extensions();
MpvTexture* self = MPV_TEXTURE(g_object_new(MPV_TEXTURE_TYPE, nullptr)); MpvTexture* self = MPV_TEXTURE(g_object_new(MPV_TEXTURE_TYPE, nullptr));
self->player = player; self->player = player;
@@ -214,8 +205,7 @@ MpvTexture* mpv_texture_new(mpv::MpvPlayer* player,
void mpv_texture_mark_frame_available(MpvTexture* self) { void mpv_texture_mark_frame_available(MpvTexture* self) {
if (self && self->registrar) { if (self && self->registrar) {
fl_texture_registrar_mark_texture_frame_available( fl_texture_registrar_mark_texture_frame_available(self->registrar, FL_TEXTURE(self));
self->registrar, FL_TEXTURE(self));
} }
} }
@@ -268,6 +258,4 @@ void mpv_texture_dispose(MpvTexture* self) {
self->view = nullptr; self->view = nullptr;
} }
int64_t mpv_texture_get_id(MpvTexture* self) { int64_t mpv_texture_get_id(MpvTexture* self) { return fl_texture_get_id(FL_TEXTURE(self)); }
return fl_texture_get_id(FL_TEXTURE(self));
}
+1 -3
View File
@@ -12,9 +12,7 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE(MpvTexture, mpv_texture, MPV, TEXTURE, FlTextureGL) G_DECLARE_FINAL_TYPE(MpvTexture, mpv_texture, MPV, TEXTURE, FlTextureGL)
/// Creates a new MpvTexture that renders mpv video to an offscreen FBO. /// Creates a new MpvTexture that renders mpv video to an offscreen FBO.
MpvTexture* mpv_texture_new(mpv::MpvPlayer* player, MpvTexture* mpv_texture_new(mpv::MpvPlayer* player, FlTextureRegistrar* registrar, FlView* view);
FlTextureRegistrar* registrar,
FlView* view);
/// Notifies Flutter that a new frame is available. /// Notifies Flutter that a new frame is available.
void mpv_texture_mark_frame_available(MpvTexture* self); void mpv_texture_mark_frame_available(MpvTexture* self);
+9 -18
View File
@@ -1,6 +1,7 @@
#include "my_application.h" #include "my_application.h"
#include <flutter_linux/flutter_linux.h> #include <flutter_linux/flutter_linux.h>
#include "flutter/generated_plugin_registrant.h" #include "flutter/generated_plugin_registrant.h"
#include "mpv/mpv_plugin.h" #include "mpv/mpv_plugin.h"
@@ -15,8 +16,7 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate. // Implements GApplication::activate.
static void my_application_activate(GApplication* application) { static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application); MyApplication* self = MY_APPLICATION(application);
GtkWindow* window = GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Default to traditional titlebar. Set GTK_CSD=1 to use a header bar. // Default to traditional titlebar. Set GTK_CSD=1 to use a header bar.
gboolean use_header_bar = FALSE; gboolean use_header_bar = FALSE;
@@ -38,8 +38,7 @@ static void my_application_activate(GApplication* application) {
// Create the Flutter view (opaque — no overlay needed). // Create the Flutter view (opaque — no overlay needed).
g_autoptr(FlDartProject) project = fl_dart_project_new(); g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
self->dart_entrypoint_arguments);
self->flutter_view = fl_view_new(project); self->flutter_view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(self->flutter_view)); gtk_widget_show(GTK_WIDGET(self->flutter_view));
@@ -50,8 +49,7 @@ static void my_application_activate(GApplication* application) {
// Register the MPV plugin (uses FlTextureGL — no overlay/GtkGLArea needed). // Register the MPV plugin (uses FlTextureGL — no overlay/GtkGLArea needed).
FlPluginRegistrar* registrar = FlPluginRegistrar* registrar =
fl_plugin_registry_get_registrar_for_plugin(FL_PLUGIN_REGISTRY(self->flutter_view), fl_plugin_registry_get_registrar_for_plugin(FL_PLUGIN_REGISTRY(self->flutter_view), "MpvPlugin");
"MpvPlugin");
mpv_plugin_register_with_registrar(registrar); mpv_plugin_register_with_registrar(registrar);
gtk_widget_show(GTK_WIDGET(window)); gtk_widget_show(GTK_WIDGET(window));
@@ -59,9 +57,7 @@ static void my_application_activate(GApplication* application) {
} }
// Implements GApplication::local_command_line. // Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application); MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name. // Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
@@ -98,22 +94,17 @@ static void my_application_dispose(GObject* object) {
static void my_application_class_init(MyApplicationClass* klass) { static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup; G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose; G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
} }
static void my_application_init(MyApplication* self) { static void my_application_init(MyApplication* self) { self->flutter_view = nullptr; }
self->flutter_view = nullptr;
}
MyApplication* my_application_new() { MyApplication* my_application_new() {
g_set_prgname(APPLICATION_ID); g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(), return MY_APPLICATION(g_object_new(
"application-id", APPLICATION_ID, my_application_get_type(), "application-id", APPLICATION_ID, "flags", G_APPLICATION_NON_UNIQUE, nullptr));
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
} }
+1 -2
View File
@@ -3,8 +3,7 @@
#include <gtk/gtk.h> #include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, GtkApplication)
GtkApplication)
/** /**
* my_application_new: * my_application_new:
+4 -2
View File
@@ -24,10 +24,12 @@ class MainFlutterWindow: NSWindow {
self.toolbar = toolbar self.toolbar = toolbar
// Register MPV player plugin for video playback // Register MPV player plugin for video playback
MpvPlayerPlugin.register(with: flutterViewController.registrar(forPlugin: "MpvPlayerPlugin")) MpvPlayerPlugin.register(
with: flutterViewController.registrar(forPlugin: "MpvPlayerPlugin"))
// Register window utils plugin for dynamic titlebar/fullscreen control from Dart // Register window utils plugin for dynamic titlebar/fullscreen control from Dart
WindowUtilsPlugin.register(with: flutterViewController.registrar(forPlugin: "WindowUtilsPlugin")) WindowUtilsPlugin.register(
with: flutterViewController.registrar(forPlugin: "WindowUtilsPlugin"))
WindowUtilsPlugin.setWindow(self) WindowUtilsPlugin.setWindow(self)
// Set custom traffic light positions using centralized values from plugin // Set custom traffic light positions using centralized values from plugin
+44 -18
View File
@@ -55,7 +55,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
// MARK: - FlutterStreamHandler // MARK: - FlutterStreamHandler
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
-> FlutterError?
{
self.eventSink = events self.eventSink = events
print("[MpvPlayerPlugin] Event stream connected") print("[MpvPlayerPlugin] Event stream connected")
return nil return nil
@@ -132,14 +134,22 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
pip.setAutoStart(ready) pip.setAutoStart(ready)
if ready { if ready {
// Observe app resigning active to auto-enter PiP // Observe app resigning active to auto-enter PiP
NotificationCenter.default.removeObserver(self, name: NSApplication.didResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(
NotificationCenter.default.addObserver(self, selector: #selector(appDidResignActive), name: NSApplication.didResignActiveNotification, object: nil) self, name: NSApplication.didResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(
self, selector: #selector(appDidResignActive),
name: NSApplication.didResignActiveNotification, object: nil)
// Observe app becoming active to auto-exit PiP // Observe app becoming active to auto-exit PiP
NotificationCenter.default.removeObserver(self, name: NSApplication.didBecomeActiveNotification, object: nil) NotificationCenter.default.removeObserver(
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: NSApplication.didBecomeActiveNotification, object: nil) self, name: NSApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(
self, selector: #selector(appDidBecomeActive),
name: NSApplication.didBecomeActiveNotification, object: nil)
} else { } else {
NotificationCenter.default.removeObserver(self, name: NSApplication.didResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(
NotificationCenter.default.removeObserver(self, name: NSApplication.didBecomeActiveNotification, object: nil) self, name: NSApplication.didResignActiveNotification, object: nil)
NotificationCenter.default.removeObserver(
self, name: NSApplication.didBecomeActiveNotification, object: nil)
} }
} }
result(nil) result(nil)
@@ -152,7 +162,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
/// No VO switching mpv keeps rendering to the same Metal layer. /// No VO switching mpv keeps rendering to the same Metal layer.
private func enterPip(manual: Bool, result: FlutterResult? = nil) { private func enterPip(manual: Bool, result: FlutterResult? = nil) {
guard let playerCore = playerCore else { guard let playerCore = playerCore else {
result?(["success": false, "errorCode": "failed", "errorMessage": "Player not initialized"]) result?([
"success": false, "errorCode": "failed", "errorMessage": "Player not initialized",
])
return return
} }
guard let metalLayer = playerCore.videoLayer else { guard let metalLayer = playerCore.videoLayer else {
@@ -173,7 +185,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
// Get video dimensions for aspect ratio // Get video dimensions for aspect ratio
var aspectRatio = NSSize(width: 16, height: 9) // default var aspectRatio = NSSize(width: 16, height: 9) // default
if let w = playerCore.getProperty("width"), let h = playerCore.getProperty("height"), if let w = playerCore.getProperty("width"), let h = playerCore.getProperty("height"),
let width = Double(w), let height = Double(h), width > 0 && height > 0 { let width = Double(w), let height = Double(h), width > 0 && height > 0
{
aspectRatio = NSSize(width: width, height: height) aspectRatio = NSSize(width: width, height: height)
} }
@@ -191,7 +204,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
let pc = playerCore, let pc = playerCore,
!pc.isPipActive, !pc.isPipActive,
!pc.isPaused, !pc.isPaused,
pipController?.autoPipEnabled == true else { return } pipController?.autoPipEnabled == true
else { return }
print("[MpvPlayerPlugin] Auto-PiP: app resigned active, entering PiP") print("[MpvPlayerPlugin] Auto-PiP: app resigned active, entering PiP")
enterPip(manual: false) enterPip(manual: false)
} }
@@ -222,7 +236,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
// Find the Flutter window // Find the Flutter window
guard let (window, _, _) = self.findFlutterWindow() else { guard let (window, _, _) = self.findFlutterWindow() else {
print("[MpvPlayerPlugin] Failed to find Flutter window") print("[MpvPlayerPlugin] Failed to find Flutter window")
result(FlutterError(code: "NO_WINDOW", message: "Could not find Flutter window", details: nil)) result(
FlutterError(
code: "NO_WINDOW", message: "Could not find Flutter window", details: nil))
return return
} }
@@ -232,7 +248,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
guard core.initialize(in: window) else { guard core.initialize(in: window) else {
print("[MpvPlayerPlugin] Failed to initialize MPV") print("[MpvPlayerPlugin] Failed to initialize MPV")
result(FlutterError(code: "MPV_INIT_FAILED", message: "Failed to initialize MPV", details: nil)) result(
FlutterError(
code: "MPV_INIT_FAILED", message: "Failed to initialize MPV", details: nil))
return return
} }
@@ -255,8 +273,10 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
} }
self.pipController = nil self.pipController = nil
self.autoPipEnabled = false self.autoPipEnabled = false
NotificationCenter.default.removeObserver(self, name: NSApplication.didResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(
NotificationCenter.default.removeObserver(self, name: NSApplication.didBecomeActiveNotification, object: nil) self, name: NSApplication.didResignActiveNotification, object: nil)
NotificationCenter.default.removeObserver(
self, name: NSApplication.didBecomeActiveNotification, object: nil)
self.playerCore?.dispose() self.playerCore?.dispose()
self.playerCore = nil self.playerCore = nil
print("[MpvPlayerPlugin] Disposed") print("[MpvPlayerPlugin] Disposed")
@@ -267,8 +287,12 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
private func handleSetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) { private func handleSetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let name = args["name"] as? String, let name = args["name"] as? String,
let value = args["value"] as? String else { let value = args["value"] as? String
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' or 'value' argument", details: nil)) else {
result(
FlutterError(
code: "INVALID_ARGS", message: "Missing 'name' or 'value' argument",
details: nil))
return return
} }
@@ -289,7 +313,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
for window in NSApplication.shared.windows { for window in NSApplication.shared.windows {
if window is MainFlutterWindow, if window is MainFlutterWindow,
let contentView = window.contentView, let contentView = window.contentView,
let contentVC = window.contentViewController { let contentVC = window.contentViewController
{
let flutterView = contentVC.view let flutterView = contentVC.view
return (window, contentView, flutterView) return (window, contentView, flutterView)
} }
@@ -298,7 +323,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
// Fallback // Fallback
for window in NSApplication.shared.windows { for window in NSApplication.shared.windows {
if let contentView = window.contentView, if let contentView = window.contentView,
let contentVC = window.contentViewController { let contentVC = window.contentViewController
{
let flutterView = contentVC.view let flutterView = contentVC.view
return (window, contentView, flutterView) return (window, contentView, flutterView)
} }
+13 -13
View File
@@ -6,14 +6,14 @@
@interface PIPViewController : NSViewController @interface PIPViewController : NSViewController
@property (nonatomic, copy, nullable) NSString *name; @property(nonatomic, copy, nullable) NSString* name;
@property (nonatomic, weak, nullable) id<PIPViewControllerDelegate> delegate; @property(nonatomic, weak, nullable) id<PIPViewControllerDelegate> delegate;
@property (nonatomic, weak, nullable) NSWindow *replacementWindow; @property(nonatomic, weak, nullable) NSWindow* replacementWindow;
@property (nonatomic) NSRect replacementRect; @property(nonatomic) NSRect replacementRect;
@property (nonatomic) bool playing; @property(nonatomic) bool playing;
@property (nonatomic) NSSize aspectRatio; @property(nonatomic) NSSize aspectRatio;
- (void)presentViewControllerAsPictureInPicture:(NSViewController *)viewController; - (void)presentViewControllerAsPictureInPicture:(NSViewController*)viewController;
@end @end
@@ -21,12 +21,12 @@
@optional @optional
// macOS 10.12-10.14 // macOS 10.12-10.14
- (BOOL)pipShouldClose:(PIPViewController *)pip; - (BOOL)pipShouldClose:(PIPViewController*)pip;
// macOS 10.15+ // macOS 10.15+
- (void)pipWillClose:(PIPViewController *)pip; - (void)pipWillClose:(PIPViewController*)pip;
- (void)pipDidClose:(PIPViewController *)pip; - (void)pipDidClose:(PIPViewController*)pip;
- (void)pipActionPlay:(PIPViewController *)pip; - (void)pipActionPlay:(PIPViewController*)pip;
- (void)pipActionPause:(PIPViewController *)pip; - (void)pipActionPause:(PIPViewController*)pip;
- (void)pipActionStop:(PIPViewController *)pip; - (void)pipActionStop:(PIPViewController*)pip;
@end @end
+5 -2
View File
@@ -11,7 +11,7 @@ class WindowDelegate: NSObject, NSWindowDelegate {
.fullScreen, .fullScreen,
.autoHideToolbar, .autoHideToolbar,
.autoHideMenuBar, .autoHideMenuBar,
.autoHideDock .autoHideDock,
] ]
// MARK: - Private Helpers // MARK: - Private Helpers
@@ -22,7 +22,10 @@ class WindowDelegate: NSObject, NSWindowDelegate {
// MARK: - NSWindowDelegate // MARK: - NSWindowDelegate
func window(_ window: NSWindow, willUseFullScreenPresentationOptions proposedOptions: NSApplication.PresentationOptions) -> NSApplication.PresentationOptions { func window(
_ window: NSWindow,
willUseFullScreenPresentationOptions proposedOptions: NSApplication.PresentationOptions
) -> NSApplication.PresentationOptions {
return fullScreenPresentationOptions return fullScreenPresentationOptions
} }
+20 -10
View File
@@ -42,7 +42,10 @@ class ForwardingToolbar: NSToolbar, NSToolbarDelegate {
toolbarDefaultItemIdentifiers(toolbar) toolbarDefaultItemIdentifiers(toolbar)
} }
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { func toolbar(
_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool
) -> NSToolbarItem? {
if itemIdentifier == NSToolbarItem.Identifier("ForwardingItem") { if itemIdentifier == NSToolbarItem.Identifier("ForwardingItem") {
let item = NSToolbarItem(itemIdentifier: itemIdentifier) let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.isBordered = false // Remove the rounded box appearance item.isBordered = false // Remove the rounded box appearance
@@ -69,7 +72,7 @@ class WindowUtilsPlugin: NSObject, FlutterPlugin {
private static let customButtonPositions: [(NSWindow.ButtonType, CGPoint)] = [ private static let customButtonPositions: [(NSWindow.ButtonType, CGPoint)] = [
(.closeButton, CGPoint(x: 20, y: 21)), (.closeButton, CGPoint(x: 20, y: 21)),
(.miniaturizeButton, CGPoint(x: 40, y: 21)), (.miniaturizeButton, CGPoint(x: 40, y: 21)),
(.zoomButton, CGPoint(x: 60, y: 21)) (.zoomButton, CGPoint(x: 60, y: 21)),
] ]
static func register(with registrar: FlutterPluginRegistrar) { static func register(with registrar: FlutterPluginRegistrar) {
@@ -170,24 +173,31 @@ class WindowUtilsPlugin: NSObject, FlutterPlugin {
action: (NSButton, NSView) -> Void action: (NSButton, NSView) -> Void
) { ) {
guard let button = window.standardWindowButton(buttonType), guard let button = window.standardWindowButton(buttonType),
let superview = button.superview else { return } let superview = button.superview
else { return }
action(button, superview) action(button, superview)
} }
private func positionConstraints(for button: NSButton, in superview: NSView) -> [NSLayoutConstraint] { private func positionConstraints(for button: NSButton, in superview: NSView)
-> [NSLayoutConstraint]
{
superview.constraints.filter { constraint in superview.constraints.filter { constraint in
((constraint.firstItem as? NSButton) == button || (constraint.secondItem as? NSButton) == button) && ((constraint.firstItem as? NSButton) == button
(constraint.firstAttribute == .left || constraint.firstAttribute == .leading || || (constraint.secondItem as? NSButton) == button)
constraint.firstAttribute == .top || constraint.firstAttribute == .centerY) && (constraint.firstAttribute == .left || constraint.firstAttribute == .leading
|| constraint.firstAttribute == .top || constraint.firstAttribute == .centerY)
} }
} }
private func overrideButtonPosition(window: NSWindow, buttonType: NSWindow.ButtonType, offset: CGPoint) { private func overrideButtonPosition(
window: NSWindow, buttonType: NSWindow.ButtonType, offset: CGPoint
) {
withButton(buttonType, in: window) { button, superview in withButton(buttonType, in: window) { button, superview in
// Store original constraints if not already stored // Store original constraints if not already stored
if originalButtonConstraints[buttonType] == nil { if originalButtonConstraints[buttonType] == nil {
let constraints = superview.constraints.filter { constraint in let constraints = superview.constraints.filter { constraint in
(constraint.firstItem as? NSButton) == button || (constraint.secondItem as? NSButton) == button (constraint.firstItem as? NSButton) == button
|| (constraint.secondItem as? NSButton) == button
} }
originalButtonConstraints[buttonType] = constraints originalButtonConstraints[buttonType] = constraints
} }
@@ -200,7 +210,7 @@ class WindowUtilsPlugin: NSObject, FlutterPlugin {
// Add new positioning constraints // Add new positioning constraints
superview.addConstraints([ superview.addConstraints([
button.leftAnchor.constraint(equalTo: superview.leftAnchor, constant: offset.x), button.leftAnchor.constraint(equalTo: superview.leftAnchor, constant: offset.x),
button.topAnchor.constraint(equalTo: superview.topAnchor, constant: offset.y) button.topAnchor.constraint(equalTo: superview.topAnchor, constant: offset.y),
]) ])
superview.layoutSubtreeIfNeeded() superview.layoutSubtreeIfNeeded()
} }
+15 -3
View File
@@ -59,7 +59,19 @@ else
rm -f "$out" rm -f "$out"
fi fi
# 2. flutter analyze (mirrors ci.yml "Analyze code") # 2. Native formatting
section "native format"
out="$(mktemp)"
if scripts/format_native.sh --check >"$out" 2>&1; then
ok "native files correctly formatted"
else
fail "native formatting issues"
sed 's/^/ /' "$out"
FAILED=1
fi
rm -f "$out"
# 3. flutter analyze (mirrors ci.yml "Analyze code")
section "flutter analyze" section "flutter analyze"
out="$(mktemp)" out="$(mktemp)"
flutter analyze >"$out" 2>&1 || true flutter analyze >"$out" 2>&1 || true
@@ -76,7 +88,7 @@ else
fi fi
rm -f "$out" rm -f "$out"
# 3. Unused code (mirrors ci.yml "Check for unused code") # 4. Unused code (mirrors ci.yml "Check for unused code")
section "dart_code_linter: unused code" section "dart_code_linter: unused code"
if ! have_dart_code_linter; then if ! have_dart_code_linter; then
skip "dart_code_linter unresolved — run 'flutter pub get'" skip "dart_code_linter unresolved — run 'flutter pub get'"
@@ -93,7 +105,7 @@ else
rm -f "$out" rm -f "$out"
fi fi
# 4. Unused files (mirrors ci.yml "Check for unused files") # 5. Unused files (mirrors ci.yml "Check for unused files")
section "dart_code_linter: unused files" section "dart_code_linter: unused files"
if ! have_dart_code_linter; then if ! have_dart_code_linter; then
skip "dart_code_linter unresolved — run 'flutter pub get'" skip "dart_code_linter unresolved — run 'flutter pub get'"
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT"
MODE="check"
case "${1:---check}" in
--check) MODE="check" ;;
--fix|--write) MODE="fix" ;;
-h|--help)
echo "Usage: scripts/format_native.sh [--check|--fix]"
exit 0
;;
*)
echo "Unknown argument: $1" >&2
echo "Usage: scripts/format_native.sh [--check|--fix]" >&2
exit 2
;;
esac
KTLINT_VERSION="${KTLINT_VERSION:-1.5.0}"
KTLINT_BIN="$ROOT/.dart_tool/native-format/ktlint-$KTLINT_VERSION"
has_command() {
command -v "$1" >/dev/null 2>&1
}
run_clang_format() {
if has_command xcrun && xcrun --find clang-format >/dev/null 2>&1; then
xcrun clang-format "$@"
elif has_command clang-format; then
clang-format "$@"
else
echo "clang-format not found. Install clang-format or Xcode command line tools." >&2
return 127
fi
}
run_swift_format() {
if has_command xcrun && xcrun --find swift-format >/dev/null 2>&1; then
xcrun swift-format "$@"
elif has_command swift-format; then
swift-format "$@"
elif has_command swift && swift format --help >/dev/null 2>&1; then
swift format "$@"
else
echo "swift-format not found. Install Swift 6+, swift-format, or Xcode 16+." >&2
return 127
fi
}
ensure_ktlint() {
if [ -x "$KTLINT_BIN" ]; then
return 0
fi
if ! has_command curl; then
echo "curl not found. Install curl to download ktlint." >&2
return 127
fi
if ! has_command java; then
echo "java not found. Install JDK 17+ to run ktlint." >&2
return 127
fi
mkdir -p "$(dirname "$KTLINT_BIN")"
curl -fsSL "https://github.com/pinterest/ktlint/releases/download/$KTLINT_VERSION/ktlint" -o "$KTLINT_BIN"
chmod +x "$KTLINT_BIN"
}
append_native_files() {
while IFS= read -r -d '' file; do
case "$file" in
android/app/src/main/cpp/include/*) continue ;;
android/app/src/main/java/io/flutter/plugins/*) continue ;;
ios/Flutter/*|macos/Flutter/*|tvos/Flutter/*) continue ;;
linux/flutter/*|windows/flutter/*) continue ;;
tvos/Runner/Plugins/*) continue ;;
*/GeneratedPluginRegistrant.*|*/generated_plugin_registrant.*) continue ;;
esac
case "$file" in
*.kt|*.kts) ktlint_files+=("$file") ;;
*.swift) swift_files+=("$file") ;;
*.c|*.cc|*.cpp|*.h|*.hpp|*.m|*.mm) clang_files+=("$file") ;;
esac
done < <(git ls-files -z -- "$@")
}
ktlint_files=()
swift_files=()
clang_files=()
append_native_files \
'android/**/*.kt' 'android/**/*.kts' \
'ios/**/*.swift' 'macos/**/*.swift' 'tvos/**/*.swift' 'shared/**/*.swift' \
'android/**/*.[ch]' 'android/**/*.cc' 'android/**/*.cpp' 'android/**/*.hpp' \
'ios/**/*.[hm]' 'ios/**/*.mm' \
'macos/**/*.[hm]' 'macos/**/*.mm' \
'tvos/**/*.[hm]' 'tvos/**/*.mm' \
'linux/**/*.[ch]' 'linux/**/*.cc' 'linux/**/*.cpp' 'linux/**/*.hpp' \
'windows/**/*.[ch]' 'windows/**/*.cc' 'windows/**/*.cpp' 'windows/**/*.hpp' \
'shared/**/*.[ch]' 'shared/**/*.cc' 'shared/**/*.cpp' 'shared/**/*.hpp'
FAILED=0
if [ "${#ktlint_files[@]}" -gt 0 ]; then
ensure_ktlint
if [ "$MODE" = "fix" ]; then
"$KTLINT_BIN" -F "${ktlint_files[@]}"
else
"$KTLINT_BIN" "${ktlint_files[@]}" || FAILED=1
fi
else
echo "No Kotlin files found."
fi
if [ "${#swift_files[@]}" -gt 0 ]; then
if [ "$MODE" = "fix" ]; then
run_swift_format format --configuration "$ROOT/.swift-format" --in-place "${swift_files[@]}"
else
swift_failed=0
for file in "${swift_files[@]}"; do
tmp="$(mktemp)"
run_swift_format format --configuration "$ROOT/.swift-format" "$file" >"$tmp"
if ! cmp -s "$file" "$tmp"; then
if [ "$swift_failed" -eq 0 ]; then
echo "Swift files need formatting:"
fi
echo " $file"
swift_failed=1
fi
rm -f "$tmp"
done
if [ "$swift_failed" -ne 0 ]; then
FAILED=1
fi
fi
else
echo "No Swift files found."
fi
if [ "${#clang_files[@]}" -gt 0 ]; then
if [ "$MODE" = "fix" ]; then
run_clang_format -i "${clang_files[@]}"
else
run_clang_format --dry-run --Werror "${clang_files[@]}" || FAILED=1
fi
else
echo "No C/C++/Obj-C files found."
fi
if [ "$FAILED" -ne 0 ]; then
echo "Native formatting issues found. Run: scripts/format_native.sh --fix" >&2
exit 1
fi
if [ "$MODE" = "check" ]; then
echo "Native formatting passed."
else
echo "Native formatting applied."
fi
@@ -3,9 +3,9 @@ import Libmpv
import QuartzCore import QuartzCore
#if os(iOS) || os(tvOS) #if os(iOS) || os(tvOS)
import UIKit import UIKit
#elseif os(macOS) #elseif os(macOS)
import Cocoa import Cocoa
#endif #endif
protocol MpvPlayerDelegate: AnyObject { protocol MpvPlayerDelegate: AnyObject {
@@ -87,7 +87,9 @@ class MpvPlayerCoreBase: NSObject {
var lastSigPeak = 0.0 var lastSigPeak = 0.0
/// Properties that must still flow to Dart while backgrounded (state-critical). /// Properties that must still flow to Dart while backgrounded (state-critical).
private static let criticalProperties: Set<String> = ["pause", "eof-reached", "paused-for-cache"] private static let criticalProperties: Set<String> = [
"pause", "eof-reached", "paused-for-cache",
]
let queue = DispatchQueue(label: "mpv", qos: .userInitiated) let queue = DispatchQueue(label: "mpv", qos: .userInitiated)
private let queueKey = DispatchSpecificKey<Void>() private let queueKey = DispatchSpecificKey<Void>()
@@ -517,7 +519,8 @@ class MpvPlayerCoreBase: NSObject {
var dictionary = [String: Any]() var dictionary = [String: Any]()
for index in 0..<Int(list.num) { for index in 0..<Int(list.num) {
if let key = list.keys?[index].map({ safeString($0) }), if let key = list.keys?[index].map({ safeString($0) }),
let value = convertNode(list.values[index]) { let value = convertNode(list.values[index])
{
dictionary[key] = value dictionary[key] = value
} }
} }
@@ -1,7 +1,7 @@
#if os(iOS) || os(tvOS) #if os(iOS) || os(tvOS)
import Flutter import Flutter
#elseif os(macOS) #elseif os(macOS)
import FlutterMacOS import FlutterMacOS
#endif #endif
/// Protocol for shared MpvPlayerPlugin method handlers across iOS, tvOS, and macOS. /// Protocol for shared MpvPlayerPlugin method handlers across iOS, tvOS, and macOS.
@@ -20,8 +20,11 @@ extension MpvPluginShared {
func handleGetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) { func handleGetProperty(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let name = args["name"] as? String else { let name = args["name"] as? String
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name' argument", details: nil)) else {
result(
FlutterError(code: "INVALID_ARGS", message: "Missing 'name' argument", details: nil)
)
return return
} }
result(coreBase?.getProperty(name)) result(coreBase?.getProperty(name))
@@ -31,8 +34,12 @@ extension MpvPluginShared {
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let name = args["name"] as? String, let name = args["name"] as? String,
let format = args["format"] as? String, let format = args["format"] as? String,
let id = args["id"] as? Int else { let id = args["id"] as? Int
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'name', 'format', or 'id' argument", details: nil)) else {
result(
FlutterError(
code: "INVALID_ARGS", message: "Missing 'name', 'format', or 'id' argument",
details: nil))
return return
} }
@@ -43,8 +50,11 @@ extension MpvPluginShared {
func handleCommand(call: FlutterMethodCall, result: @escaping FlutterResult) { func handleCommand(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let commandArgs = args["args"] as? [String] else { let commandArgs = args["args"] as? [String]
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'args' argument", details: nil)) else {
result(
FlutterError(code: "INVALID_ARGS", message: "Missing 'args' argument", details: nil)
)
return return
} }
@@ -53,15 +63,20 @@ extension MpvPluginShared {
case .success: case .success:
result(nil) result(nil)
case .failure(let error): case .failure(let error):
result(FlutterError(code: "COMMAND_FAILED", message: error.localizedDescription, details: nil)) result(
FlutterError(
code: "COMMAND_FAILED", message: error.localizedDescription, details: nil))
} }
} ?? result(nil) } ?? result(nil)
} }
func handleSetVisible(call: FlutterMethodCall, result: @escaping FlutterResult) { func handleSetVisible(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let visible = args["visible"] as? Bool else { let visible = args["visible"] as? Bool
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'visible' argument", details: nil)) else {
result(
FlutterError(
code: "INVALID_ARGS", message: "Missing 'visible' argument", details: nil))
return return
} }
@@ -81,7 +96,8 @@ extension MpvPluginShared {
func handleSetLogLevel(call: FlutterMethodCall, result: @escaping FlutterResult) { func handleSetLogLevel(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let level = args["level"] as? String else { let level = args["level"] as? String
else {
result(FlutterError(code: "INVALID_ARGS", message: "Missing 'level'", details: nil)) result(FlutterError(code: "INVALID_ARGS", message: "Missing 'level'", details: nil))
return return
} }
+2 -1
View File
@@ -1,9 +1,10 @@
#ifndef SANITIZE_UTF8_H_ #ifndef SANITIZE_UTF8_H_
#define SANITIZE_UTF8_H_ #define SANITIZE_UTF8_H_
#include <simdutf.h>
#include <cstring> #include <cstring>
#include <string> #include <string>
#include <simdutf.h>
// Sanitize a C string that may contain invalid UTF-8 sequences. // Sanitize a C string that may contain invalid UTF-8 sequences.
// Uses simdutf for SIMD-accelerated validation (fast path for valid strings), // Uses simdutf for SIMD-accelerated validation (fast path for valid strings),
+28 -49
View File
@@ -30,11 +30,10 @@ static void CALLBACK SaveTimerProc(HWND, UINT, UINT_PTR, DWORD) {
// Write a WINDOWPLACEMENT struct directly to the registry. // Write a WINDOWPLACEMENT struct directly to the registry.
static void WriteWindowPlacement(const WINDOWPLACEMENT& wp) { static void WriteWindowPlacement(const WINDOWPLACEMENT& wp) {
HKEY hKey; HKEY hKey;
if (RegCreateKeyExW(HKEY_CURRENT_USER, kWindowPlacementKey, 0, nullptr, if (RegCreateKeyExW(
REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey, HKEY_CURRENT_USER, kWindowPlacementKey, 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey,
nullptr) == ERROR_SUCCESS) { nullptr) == ERROR_SUCCESS) {
RegSetValueExW(hKey, kWindowPlacementValue, 0, REG_BINARY, RegSetValueExW(hKey, kWindowPlacementValue, 0, REG_BINARY, reinterpret_cast<const BYTE*>(&wp), sizeof(wp));
reinterpret_cast<const BYTE*>(&wp), sizeof(wp));
RegCloseKey(hKey); RegCloseKey(hKey);
} }
} }
@@ -51,17 +50,15 @@ static void SaveWindowPlacement(HWND hwnd) {
// Returns whether the window should be maximized // Returns whether the window should be maximized
static bool LoadWindowPlacement(HWND hwnd) { static bool LoadWindowPlacement(HWND hwnd) {
HKEY hKey; HKEY hKey;
if (RegOpenKeyExW(HKEY_CURRENT_USER, kWindowPlacementKey, 0, KEY_READ, if (RegOpenKeyExW(HKEY_CURRENT_USER, kWindowPlacementKey, 0, KEY_READ, &hKey) != ERROR_SUCCESS) return false;
&hKey) != ERROR_SUCCESS)
return false;
WINDOWPLACEMENT wp{}; WINDOWPLACEMENT wp{};
wp.length = sizeof(wp); wp.length = sizeof(wp);
DWORD size = sizeof(wp); DWORD size = sizeof(wp);
bool wasMaximized = false; bool wasMaximized = false;
if (RegQueryValueExW(hKey, kWindowPlacementValue, nullptr, nullptr, if (RegQueryValueExW(hKey, kWindowPlacementValue, nullptr, nullptr, reinterpret_cast<BYTE*>(&wp), &size) ==
reinterpret_cast<BYTE*>(&wp), &size) == ERROR_SUCCESS && ERROR_SUCCESS &&
size == sizeof(wp)) { size == sizeof(wp)) {
// Prevent restoring as minimized // Prevent restoring as minimized
if (wp.showCmd == SW_SHOWMINIMIZED) wp.showCmd = SW_SHOWNORMAL; if (wp.showCmd == SW_SHOWMINIMIZED) wp.showCmd = SW_SHOWNORMAL;
@@ -80,8 +77,7 @@ static void DebounceSaveWindowPlacement(HWND hwnd) {
g_saveTimerId = SetTimer(nullptr, 0, 500, SaveTimerProc); // 500ms debounce g_saveTimerId = SetTimer(nullptr, 0, 500, SaveTimerProc); // 500ms debounce
} }
FlutterWindow::FlutterWindow(const flutter::DartProject& project) FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {}
: project_(project) {}
FlutterWindow::~FlutterWindow() {} FlutterWindow::~FlutterWindow() {}
@@ -94,8 +90,8 @@ bool FlutterWindow::OnCreate() {
// The size here must match the window dimensions to avoid unnecessary surface // The size here must match the window dimensions to avoid unnecessary surface
// creation / destruction in the startup path. // creation / destruction in the startup path.
flutter_controller_ = std::make_unique<flutter::FlutterViewController>( flutter_controller_ =
frame.right - frame.left, frame.bottom - frame.top, project_); std::make_unique<flutter::FlutterViewController>(frame.right - frame.left, frame.bottom - frame.top, project_);
// Ensure that basic setup of the controller was successful. // Ensure that basic setup of the controller was successful.
if (!flutter_controller_->engine() || !flutter_controller_->view()) { if (!flutter_controller_->engine() || !flutter_controller_->view()) {
return false; return false;
@@ -104,8 +100,7 @@ bool FlutterWindow::OnCreate() {
// Register mpv player plugin. // Register mpv player plugin.
OutputDebugStringA("FlutterWindow: About to register MpvPlayerPlugin\n"); OutputDebugStringA("FlutterWindow: About to register MpvPlayerPlugin\n");
MpvPlayerPluginRegisterWithRegistrar( MpvPlayerPluginRegisterWithRegistrar(flutter_controller_->engine()->GetRegistrarForPlugin("MpvPlayerPlugin"));
flutter_controller_->engine()->GetRegistrarForPlugin("MpvPlayerPlugin"));
OutputDebugStringA("FlutterWindow: MpvPlayerPlugin registered\n"); OutputDebugStringA("FlutterWindow: MpvPlayerPlugin registered\n");
RegisterWindowChannel(); RegisterWindowChannel();
@@ -116,9 +111,8 @@ bool FlutterWindow::OnCreate() {
HWND hwnd = GetHandle(); HWND hwnd = GetHandle();
bool maximized = LoadWindowPlacement(hwnd); bool maximized = LoadWindowPlacement(hwnd);
flutter_controller_->engine()->SetNextFrameCallback([this, maximized]() { flutter_controller_->engine()->SetNextFrameCallback(
::ShowWindow(this->GetHandle(), maximized ? SW_SHOWMAXIMIZED : SW_SHOWNORMAL); [this, maximized]() { ::ShowWindow(this->GetHandle(), maximized ? SW_SHOWMAXIMIZED : SW_SHOWNORMAL); });
});
// Flutter can complete the first frame before the "show window" callback is // Flutter can complete the first frame before the "show window" callback is
// registered. The following call ensures a frame is pending to ensure the // registered. The following call ensures a frame is pending to ensure the
@@ -152,14 +146,10 @@ void FlutterWindow::OnDestroy() {
} }
LRESULT LRESULT
FlutterWindow::MessageHandler(HWND hwnd, UINT const message, FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept {
WPARAM const wparam,
LPARAM const lparam) noexcept {
// Give Flutter, including plugins, an opportunity to handle window messages. // Give Flutter, including plugins, an opportunity to handle window messages.
if (flutter_controller_) { if (flutter_controller_) {
std::optional<LRESULT> result = std::optional<LRESULT> result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam);
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
lparam);
if (result) { if (result) {
return *result; return *result;
} }
@@ -196,20 +186,16 @@ FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void FlutterWindow::RegisterWindowChannel() { void FlutterWindow::RegisterWindowChannel() {
auto messenger = flutter_controller_->engine()->messenger(); auto messenger = flutter_controller_->engine()->messenger();
window_channel_ = window_channel_ = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>( messenger, "plezy/window", &flutter::StandardMethodCodec::GetInstance());
messenger, "plezy/window",
&flutter::StandardMethodCodec::GetInstance());
window_channel_->SetMethodCallHandler( window_channel_->SetMethodCallHandler([this](
[this](const flutter::MethodCall<flutter::EncodableValue>& call, const flutter::MethodCall<flutter::EncodableValue>& call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
result) {
const std::string& name = call.method_name(); const std::string& name = call.method_name();
if (name == "setFullScreen") { if (name == "setFullScreen") {
bool value = false; bool value = false;
if (const auto* args = if (const auto* args = std::get_if<flutter::EncodableMap>(call.arguments())) {
std::get_if<flutter::EncodableMap>(call.arguments())) {
auto it = args->find(flutter::EncodableValue("isFullScreen")); auto it = args->find(flutter::EncodableValue("isFullScreen"));
if (it != args->end()) { if (it != args->end()) {
if (const bool* b = std::get_if<bool>(&it->second)) value = *b; if (const bool* b = std::get_if<bool>(&it->second)) value = *b;
@@ -227,9 +213,7 @@ void FlutterWindow::RegisterWindowChannel() {
void FlutterWindow::NotifyFullScreenChanged() { void FlutterWindow::NotifyFullScreenChanged() {
if (!window_channel_) return; if (!window_channel_) return;
window_channel_->InvokeMethod( window_channel_->InvokeMethod("onFullScreenChanged", std::make_unique<flutter::EncodableValue>(is_fullscreen_));
"onFullScreenChanged",
std::make_unique<flutter::EncodableValue>(is_fullscreen_));
} }
void FlutterWindow::SetNativeFullScreen(bool fullscreen) { void FlutterWindow::SetNativeFullScreen(bool fullscreen) {
@@ -255,8 +239,7 @@ void FlutterWindow::SetNativeFullScreen(bool fullscreen) {
POINT center{(wr.left + wr.right) / 2, (wr.top + wr.bottom) / 2}; POINT center{(wr.left + wr.right) / 2, (wr.top + wr.bottom) / 2};
MONITORINFO mi{}; MONITORINFO mi{};
mi.cbSize = sizeof(mi); mi.cbSize = sizeof(mi);
if (!::GetMonitorInfoW(::MonitorFromPoint(center, MONITOR_DEFAULTTONEAREST), if (!::GetMonitorInfoW(::MonitorFromPoint(center, MONITOR_DEFAULTTONEAREST), &mi)) {
&mi)) {
g_suppressPlacementSave = false; g_suppressPlacementSave = false;
return; return;
} }
@@ -271,17 +254,14 @@ void FlutterWindow::SetNativeFullScreen(bool fullscreen) {
// Strip frame/caption. Stripping WS_OVERLAPPEDWINDOW alone is enough to // Strip frame/caption. Stripping WS_OVERLAPPEDWINDOW alone is enough to
// make the following SetWindowPos use the given rect exactly — no need // make the following SetWindowPos use the given rect exactly — no need
// to ShowWindow(SW_SHOWNORMAL) first (would cause a second relayout). // to ShowWindow(SW_SHOWNORMAL) first (would cause a second relayout).
::SetWindowLongPtr( ::SetWindowLongPtr(hwnd, GWL_STYLE, style_before_fullscreen_ & ~WS_OVERLAPPEDWINDOW);
hwnd, GWL_STYLE, style_before_fullscreen_ & ~WS_OVERLAPPEDWINDOW);
::SetWindowLongPtr( ::SetWindowLongPtr(
hwnd, GWL_EXSTYLE, hwnd, GWL_EXSTYLE,
ex_style_before_fullscreen_ & ex_style_before_fullscreen_ & ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE));
~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE |
WS_EX_STATICEDGE));
const RECT& r = mi.rcMonitor; const RECT& r = mi.rcMonitor;
::SetWindowPos(hwnd, HWND_TOP, r.left, r.top, r.right - r.left, ::SetWindowPos(
r.bottom - r.top, hwnd, HWND_TOP, r.left, r.top, r.right - r.left, r.bottom - r.top,
SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOACTIVATE); SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOACTIVATE);
is_fullscreen_ = true; is_fullscreen_ = true;
@@ -298,9 +278,8 @@ void FlutterWindow::SetNativeFullScreen(bool fullscreen) {
} }
// Force a frame refresh so restored chrome paints. // Force a frame refresh so restored chrome paints.
::SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, ::SetWindowPos(
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | hwnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
SWP_FRAMECHANGED);
is_fullscreen_ = false; is_fullscreen_ = false;
placement_before_fullscreen_ = {}; placement_before_fullscreen_ = {};
+2 -4
View File
@@ -21,8 +21,7 @@ class FlutterWindow : public Win32Window {
// Win32Window: // Win32Window:
bool OnCreate() override; bool OnCreate() override;
void OnDestroy() override; void OnDestroy() override;
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override;
LPARAM const lparam) noexcept override;
private: private:
// The project to run. // The project to run.
@@ -32,8 +31,7 @@ class FlutterWindow : public Win32Window {
std::unique_ptr<flutter::FlutterViewController> flutter_controller_; std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
// Method channel exposing window controls to Dart (plezy/window). // Method channel exposing window controls to Dart (plezy/window).
std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> window_channel_;
window_channel_;
// Fullscreen state tracking for monitor-aware native fullscreen. // Fullscreen state tracking for monitor-aware native fullscreen.
// Maximize state lives inside `placement_before_fullscreen_.showCmd`. // Maximize state lives inside `placement_before_fullscreen_.showCmd`.
+4 -6
View File
@@ -6,8 +6,8 @@
#include "mpv/display_mode_manager.h" #include "mpv/display_mode_manager.h"
#include "utils.h" #include "utils.h"
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, int APIENTRY
_In_ wchar_t *command_line, _In_ int show_command) { wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t* command_line, _In_ int show_command) {
// Single instance enforcement // Single instance enforcement
HANDLE mutex = CreateMutex(nullptr, TRUE, L"com.edde746.Plezy.SingleInstance"); HANDLE mutex = CreateMutex(nullptr, TRUE, L"com.edde746.Plezy.SingleInstance");
if (GetLastError() == ERROR_ALREADY_EXISTS) { if (GetLastError() == ERROR_ALREADY_EXISTS) {
@@ -33,8 +33,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
flutter::DartProject project(L"data"); flutter::DartProject project(L"data");
project.set_ui_thread_policy(flutter::UIThreadPolicy::RunOnSeparateThread); project.set_ui_thread_policy(flutter::UIThreadPolicy::RunOnSeparateThread);
std::vector<std::string> command_line_arguments = std::vector<std::string> command_line_arguments = GetCommandLineArguments();
GetCommandLineArguments();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
@@ -47,8 +46,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
window.SetQuitOnClose(true); window.SetQuitOnClose(true);
// Recover display mode if a prior crash left it changed. // Recover display mode if a prior crash left it changed.
mpv::DisplayModeManager::RecoverIfNeeded( mpv::DisplayModeManager::RecoverIfNeeded(::GetAncestor(window.GetHandle(), GA_ROOT));
::GetAncestor(window.GetHandle(), GA_ROOT));
::MSG msg; ::MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0)) { while (::GetMessage(&msg, nullptr, 0, 0)) {
+44 -68
View File
@@ -1,9 +1,9 @@
#include "display_mode_manager.h" #include "display_mode_manager.h"
#include "sdk_26100.h"
#include <cmath>
#include <algorithm> #include <algorithm>
#include <cmath>
#include "sdk_26100.h"
namespace mpv { namespace mpv {
@@ -44,14 +44,12 @@ std::vector<DISPLAYCONFIG_PATH_INFO> DisplayModeManager::GetDisplayConfigPaths()
// Retry loop for ERROR_INSUFFICIENT_BUFFER (Kodi pattern). // Retry loop for ERROR_INSUFFICIENT_BUFFER (Kodi pattern).
do { do {
if (GetDisplayConfigBufferSizes(flags, &path_count, &mode_count) != ERROR_SUCCESS) if (GetDisplayConfigBufferSizes(flags, &path_count, &mode_count) != ERROR_SUCCESS) return {};
return {};
paths.resize(path_count); paths.resize(path_count);
modes.resize(mode_count); modes.resize(mode_count);
result = QueryDisplayConfig(flags, &path_count, paths.data(), result = QueryDisplayConfig(flags, &path_count, paths.data(), &mode_count, modes.data(), nullptr);
&mode_count, modes.data(), nullptr);
} while (result == ERROR_INSUFFICIENT_BUFFER); } while (result == ERROR_INSUFFICIENT_BUFFER);
if (result != ERROR_SUCCESS) return {}; if (result != ERROR_SUCCESS) return {};
@@ -60,8 +58,7 @@ std::vector<DISPLAYCONFIG_PATH_INFO> DisplayModeManager::GetDisplayConfigPaths()
return paths; return paths;
} }
std::optional<DisplayConfigId> DisplayModeManager::GetDisplayTargetId( std::optional<DisplayConfigId> DisplayModeManager::GetDisplayTargetId(const std::wstring& gdi_device_name) {
const std::wstring& gdi_device_name) {
// Follows Kodi's GetDisplayTargetId: iterate QueryDisplayConfig paths, // Follows Kodi's GetDisplayTargetId: iterate QueryDisplayConfig paths,
// match via DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME.viewGdiDeviceName. // match via DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME.viewGdiDeviceName.
DISPLAYCONFIG_SOURCE_DEVICE_NAME source = {}; DISPLAYCONFIG_SOURCE_DEVICE_NAME source = {};
@@ -72,8 +69,7 @@ std::optional<DisplayConfigId> DisplayModeManager::GetDisplayTargetId(
source.header.adapterId = path.sourceInfo.adapterId; source.header.adapterId = path.sourceInfo.adapterId;
source.header.id = path.sourceInfo.id; source.header.id = path.sourceInfo.id;
if (DisplayConfigGetDeviceInfo(&source.header) == ERROR_SUCCESS && if (DisplayConfigGetDeviceInfo(&source.header) == ERROR_SUCCESS && gdi_device_name == source.viewGdiDeviceName) {
gdi_device_name == source.viewGdiDeviceName) {
return DisplayConfigId{path.targetInfo.adapterId, path.targetInfo.id}; return DisplayConfigId{path.targetInfo.adapterId, path.targetInfo.id};
} }
} }
@@ -116,9 +112,13 @@ std::vector<DisplayMode> DisplayModeManager::EnumerateDisplayModes(HWND window)
if (a.height != b.height) return a.height < b.height; if (a.height != b.height) return a.height < b.height;
return a.refresh_rate < b.refresh_rate; return a.refresh_rate < b.refresh_rate;
}); });
modes.erase(std::unique(modes.begin(), modes.end(), [](const DisplayMode& a, const DisplayMode& b) { modes.erase(
std::unique(
modes.begin(), modes.end(),
[](const DisplayMode& a, const DisplayMode& b) {
return a.width == b.width && a.height == b.height && a.refresh_rate == b.refresh_rate; return a.width == b.width && a.height == b.height && a.refresh_rate == b.refresh_rate;
}), modes.end()); }),
modes.end());
return modes; return modes;
} }
@@ -145,12 +145,10 @@ void DisplayModeManager::SaveOriginalMode(HWND window) {
original_devmode_ = {}; original_devmode_ = {};
original_devmode_.dmSize = sizeof(original_devmode_); original_devmode_.dmSize = sizeof(original_devmode_);
EnumDisplaySettingsW(original_device_name_.c_str(), ENUM_CURRENT_SETTINGS, EnumDisplaySettingsW(original_device_name_.c_str(), ENUM_CURRENT_SETTINGS, &original_devmode_);
&original_devmode_);
} }
bool DisplayModeManager::SetDisplayMode(HWND window, DWORD width, DWORD height, bool DisplayModeManager::SetDisplayMode(HWND window, DWORD width, DWORD height, DWORD refresh_rate) {
DWORD refresh_rate) {
std::wstring device_name = GetMonitorDeviceName(window); std::wstring device_name = GetMonitorDeviceName(window);
if (device_name.empty()) return false; if (device_name.empty()) return false;
@@ -175,25 +173,21 @@ bool DisplayModeManager::SetDisplayMode(HWND window, DWORD width, DWORD height,
DEVMODEW registry_dm = {}; DEVMODEW registry_dm = {};
registry_dm.dmSize = sizeof(registry_dm); registry_dm.dmSize = sizeof(registry_dm);
if (EnumDisplaySettingsW(device_name.c_str(), ENUM_REGISTRY_SETTINGS, &registry_dm)) { if (EnumDisplaySettingsW(device_name.c_str(), ENUM_REGISTRY_SETTINGS, &registry_dm)) {
LONG rc = ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, LONG rc = ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, CDS_UPDATEREGISTRY | CDS_NORESET, nullptr);
CDS_UPDATEREGISTRY | CDS_NORESET, nullptr);
if (rc == DISP_CHANGE_SUCCESSFUL) { if (rc == DISP_CHANGE_SUCCESSFUL) {
rc = ChangeDisplaySettingsExW(device_name.c_str(), nullptr, nullptr, rc = ChangeDisplaySettingsExW(device_name.c_str(), nullptr, nullptr, CDS_FULLSCREEN, nullptr);
CDS_FULLSCREEN, nullptr);
if (rc == DISP_CHANGE_SUCCESSFUL) changed = true; if (rc == DISP_CHANGE_SUCCESSFUL) changed = true;
// Restore original registry settings. // Restore original registry settings.
registry_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; registry_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS;
ChangeDisplaySettingsExW(device_name.c_str(), &registry_dm, nullptr, ChangeDisplaySettingsExW(device_name.c_str(), &registry_dm, nullptr, CDS_UPDATEREGISTRY | CDS_NORESET, nullptr);
CDS_UPDATEREGISTRY | CDS_NORESET, nullptr);
} }
} }
} }
// Standard path / fallback. // Standard path / fallback.
if (!changed) { if (!changed) {
LONG rc = ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, LONG rc = ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, CDS_FULLSCREEN, nullptr);
CDS_FULLSCREEN, nullptr);
if (rc == DISP_CHANGE_SUCCESSFUL) changed = true; if (rc == DISP_CHANGE_SUCCESSFUL) changed = true;
} }
@@ -210,9 +204,8 @@ bool DisplayModeManager::RestoreOriginalMode(HWND window) {
original_devmode_.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; original_devmode_.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS;
LONG rc = ChangeDisplaySettingsExW(original_device_name_.c_str(), LONG rc =
&original_devmode_, nullptr, ChangeDisplaySettingsExW(original_device_name_.c_str(), &original_devmode_, nullptr, CDS_FULLSCREEN, nullptr);
CDS_FULLSCREEN, nullptr);
if (rc == DISP_CHANGE_SUCCESSFUL) { if (rc == DISP_CHANGE_SUCCESSFUL) {
mode_changed_ = false; mode_changed_ = false;
@@ -239,8 +232,7 @@ bool DisplayModeManager::IsHDRSupported(HWND window) {
// Follows Kodi's GetDisplayHDRStatus pattern. // Follows Kodi's GetDisplayHDRStatus pattern.
if (IsWin11_24H2OrNewer()) { if (IsWin11_24H2OrNewer()) {
DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2 info = {}; DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2 info = {};
info.header.type = static_cast<DISPLAYCONFIG_DEVICE_INFO_TYPE>( info.header.type = static_cast<DISPLAYCONFIG_DEVICE_INFO_TYPE>(DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2);
DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2);
info.header.size = sizeof(info); info.header.size = sizeof(info);
info.header.adapterId = target_id->adapter_id; info.header.adapterId = target_id->adapter_id;
info.header.id = target_id->id; info.header.id = target_id->id;
@@ -275,8 +267,7 @@ bool DisplayModeManager::IsHDREnabled(HWND window) {
if (IsWin11_24H2OrNewer()) { if (IsWin11_24H2OrNewer()) {
DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2 info = {}; DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2 info = {};
info.header.type = static_cast<DISPLAYCONFIG_DEVICE_INFO_TYPE>( info.header.type = static_cast<DISPLAYCONFIG_DEVICE_INFO_TYPE>(DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2);
DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2);
info.header.size = sizeof(info); info.header.size = sizeof(info);
info.header.adapterId = target_id->adapter_id; info.header.adapterId = target_id->adapter_id;
info.header.id = target_id->id; info.header.id = target_id->id;
@@ -331,8 +322,7 @@ bool DisplayModeManager::SetHDREnabled(HWND window, bool enabled) {
// Source: Kodi WIN32Util.cpp:1276-1288. // Source: Kodi WIN32Util.cpp:1276-1288.
if (pre_toggle_dm.dmDisplayFrequency != 0) { if (pre_toggle_dm.dmDisplayFrequency != 0) {
pre_toggle_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; pre_toggle_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS;
ChangeDisplaySettingsExW(device_name.c_str(), &pre_toggle_dm, nullptr, ChangeDisplaySettingsExW(device_name.c_str(), &pre_toggle_dm, nullptr, CDS_FULLSCREEN, nullptr);
CDS_FULLSCREEN, nullptr);
} }
hdr_changed_ = true; hdr_changed_ = true;
@@ -365,8 +355,7 @@ bool DisplayModeManager::RestoreOriginalHDRState(HWND window) {
// Restore DEVMODEW after toggle. // Restore DEVMODEW after toggle.
if (pre_toggle_dm.dmDisplayFrequency != 0) { if (pre_toggle_dm.dmDisplayFrequency != 0) {
pre_toggle_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; pre_toggle_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS;
ChangeDisplaySettingsExW(original_hdr_device_name_.c_str(), &pre_toggle_dm, ChangeDisplaySettingsExW(original_hdr_device_name_.c_str(), &pre_toggle_dm, nullptr, CDS_FULLSCREEN, nullptr);
nullptr, CDS_FULLSCREEN, nullptr);
} }
hdr_changed_ = false; hdr_changed_ = false;
@@ -379,8 +368,7 @@ bool DisplayModeManager::RestoreOriginalHDRState(HWND window) {
LONG DisplayModeManager::SetHDRStateForTarget(const DisplayConfigId& target, bool enabled) { LONG DisplayModeManager::SetHDRStateForTarget(const DisplayConfigId& target, bool enabled) {
if (IsWin11_24H2OrNewer()) { if (IsWin11_24H2OrNewer()) {
DISPLAYCONFIG_SET_HDR_STATE state = {}; DISPLAYCONFIG_SET_HDR_STATE state = {};
state.header.type = static_cast<DISPLAYCONFIG_DEVICE_INFO_TYPE>( state.header.type = static_cast<DISPLAYCONFIG_DEVICE_INFO_TYPE>(DISPLAYCONFIG_DEVICE_INFO_SET_HDR_STATE);
DISPLAYCONFIG_DEVICE_INFO_SET_HDR_STATE);
state.header.size = sizeof(state); state.header.size = sizeof(state);
state.header.adapterId = target.adapter_id; state.header.adapterId = target.adapter_id;
state.header.id = target.id; state.header.id = target.id;
@@ -399,23 +387,21 @@ LONG DisplayModeManager::SetHDRStateForTarget(const DisplayConfigId& target, boo
bool DisplayModeManager::WriteRegistryDWORD(const wchar_t* value_name, DWORD value) { bool DisplayModeManager::WriteRegistryDWORD(const wchar_t* value_name, DWORD value) {
HKEY key; HKEY key;
if (RegCreateKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, nullptr, if (RegCreateKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr) !=
0, KEY_WRITE, nullptr, &key, nullptr) != ERROR_SUCCESS) ERROR_SUCCESS)
return false; return false;
LONG result = RegSetValueExW(key, value_name, 0, REG_DWORD, LONG result = RegSetValueExW(key, value_name, 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(value));
reinterpret_cast<const BYTE*>(&value), sizeof(value));
RegCloseKey(key); RegCloseKey(key);
return result == ERROR_SUCCESS; return result == ERROR_SUCCESS;
} }
bool DisplayModeManager::WriteRegistryString(const wchar_t* value_name, bool DisplayModeManager::WriteRegistryString(const wchar_t* value_name, const std::wstring& value) {
const std::wstring& value) {
HKEY key; HKEY key;
if (RegCreateKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, nullptr, if (RegCreateKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr) !=
0, KEY_WRITE, nullptr, &key, nullptr) != ERROR_SUCCESS) ERROR_SUCCESS)
return false; return false;
LONG result = RegSetValueExW(key, value_name, 0, REG_SZ, LONG result = RegSetValueExW(
reinterpret_cast<const BYTE*>(value.c_str()), key, value_name, 0, REG_SZ, reinterpret_cast<const BYTE*>(value.c_str()),
static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t))); static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t)));
RegCloseKey(key); RegCloseKey(key);
return result == ERROR_SUCCESS; return result == ERROR_SUCCESS;
@@ -423,20 +409,17 @@ bool DisplayModeManager::WriteRegistryString(const wchar_t* value_name,
bool DisplayModeManager::ReadRegistryDWORD(const wchar_t* value_name, DWORD& value) { bool DisplayModeManager::ReadRegistryDWORD(const wchar_t* value_name, DWORD& value) {
HKEY key; HKEY key;
if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_READ, &key) != ERROR_SUCCESS) if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_READ, &key) != ERROR_SUCCESS) return false;
return false;
DWORD size = sizeof(value); DWORD size = sizeof(value);
DWORD type = 0; DWORD type = 0;
LONG result = RegQueryValueExW(key, value_name, nullptr, &type, LONG result = RegQueryValueExW(key, value_name, nullptr, &type, reinterpret_cast<BYTE*>(&value), &size);
reinterpret_cast<BYTE*>(&value), &size);
RegCloseKey(key); RegCloseKey(key);
return result == ERROR_SUCCESS && type == REG_DWORD; return result == ERROR_SUCCESS && type == REG_DWORD;
} }
bool DisplayModeManager::ReadRegistryString(const wchar_t* value_name, std::wstring& value) { bool DisplayModeManager::ReadRegistryString(const wchar_t* value_name, std::wstring& value) {
HKEY key; HKEY key;
if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_READ, &key) != ERROR_SUCCESS) if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_READ, &key) != ERROR_SUCCESS) return false;
return false;
DWORD size = 0; DWORD size = 0;
DWORD type = 0; DWORD type = 0;
RegQueryValueExW(key, value_name, nullptr, &type, nullptr, &size); RegQueryValueExW(key, value_name, nullptr, &type, nullptr, &size);
@@ -445,8 +428,7 @@ bool DisplayModeManager::ReadRegistryString(const wchar_t* value_name, std::wstr
return false; return false;
} }
value.resize(size / sizeof(wchar_t)); value.resize(size / sizeof(wchar_t));
LONG result = RegQueryValueExW(key, value_name, nullptr, nullptr, LONG result = RegQueryValueExW(key, value_name, nullptr, nullptr, reinterpret_cast<BYTE*>(&value[0]), &size);
reinterpret_cast<BYTE*>(&value[0]), &size);
RegCloseKey(key); RegCloseKey(key);
if (result != ERROR_SUCCESS) return false; if (result != ERROR_SUCCESS) return false;
// Remove trailing null. // Remove trailing null.
@@ -456,8 +438,7 @@ bool DisplayModeManager::ReadRegistryString(const wchar_t* value_name, std::wstr
bool DisplayModeManager::DeleteRegistryValue(const wchar_t* value_name) { bool DisplayModeManager::DeleteRegistryValue(const wchar_t* value_name) {
HKEY key; HKEY key;
if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_WRITE, &key) != ERROR_SUCCESS) if (RegOpenKeyExW(HKEY_CURRENT_USER, kRegistryPath, 0, KEY_WRITE, &key) != ERROR_SUCCESS) return false;
return false;
RegDeleteValueW(key, value_name); RegDeleteValueW(key, value_name);
RegCloseKey(key); RegCloseKey(key);
return true; return true;
@@ -517,8 +498,7 @@ bool DisplayModeManager::RecoverIfNeeded(HWND window) {
dm.dmDisplayFrequency = refresh; dm.dmDisplayFrequency = refresh;
dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY;
LONG rc = ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, LONG rc = ChangeDisplaySettingsExW(device_name.c_str(), &dm, nullptr, CDS_FULLSCREEN, nullptr);
CDS_FULLSCREEN, nullptr);
if (rc == DISP_CHANGE_SUCCESSFUL) recovered = true; if (rc == DISP_CHANGE_SUCCESSFUL) recovered = true;
} }
} }
@@ -542,8 +522,7 @@ bool DisplayModeManager::RecoverIfNeeded(HWND window) {
// Restore display mode after HDR toggle. // Restore display mode after HDR toggle.
if (pre_dm.dmDisplayFrequency != 0) { if (pre_dm.dmDisplayFrequency != 0) {
pre_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS; pre_dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_DISPLAYFLAGS;
ChangeDisplaySettingsExW(device_name.c_str(), &pre_dm, nullptr, ChangeDisplaySettingsExW(device_name.c_str(), &pre_dm, nullptr, CDS_FULLSCREEN, nullptr);
CDS_FULLSCREEN, nullptr);
} }
} }
} }
@@ -556,10 +535,8 @@ bool DisplayModeManager::RecoverIfNeeded(HWND window) {
// --- Refresh rate matching --- // --- Refresh rate matching ---
DWORD DisplayModeManager::FindBestRefreshRate(double video_fps, DWORD DisplayModeManager::FindBestRefreshRate(
const std::vector<DisplayMode>& modes, double video_fps, const std::vector<DisplayMode>& modes, DWORD current_width, DWORD current_height) {
DWORD current_width,
DWORD current_height) {
if (video_fps <= 0) return 0; if (video_fps <= 0) return 0;
// Collect unique refresh rates available at the current resolution. // Collect unique refresh rates available at the current resolution.
@@ -592,8 +569,7 @@ DWORD DisplayModeManager::FindBestRefreshRate(double video_fps,
// Prefer lowest multiplier (exact match > 2x > 3x > ...). // Prefer lowest multiplier (exact match > 2x > 3x > ...).
// Among equal multipliers, prefer higher rate (shouldn't happen, but safe). // Among equal multipliers, prefer higher rate (shouldn't happen, but safe).
if (best_rate == 0 || multiplier < best_multiplier || if (best_rate == 0 || multiplier < best_multiplier || (multiplier == best_multiplier && rate > best_rate)) {
(multiplier == best_multiplier && rate > best_rate)) {
best_rate = rate; best_rate = rate;
best_multiplier = multiplier; best_multiplier = multiplier;
} }
+10 -7
View File
@@ -26,10 +26,14 @@ struct DisplayConfigId {
// Pure Win32 utility — no mpv or Flutter dependency. // Pure Win32 utility — no mpv or Flutter dependency.
// //
// References: // References:
// ChangeDisplaySettingsExW: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changedisplaysettingsexw // ChangeDisplaySettingsExW:
// EnumDisplaySettingsW: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaysettingsw // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changedisplaysettingsexw
// DisplayConfigGetDeviceInfo: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-displayconfiggetdeviceinfo // EnumDisplaySettingsW:
// DisplayConfigSetDeviceInfo: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-displayconfigsetdeviceinfo // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaysettingsw
// DisplayConfigGetDeviceInfo:
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-displayconfiggetdeviceinfo
// DisplayConfigSetDeviceInfo:
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-displayconfigsetdeviceinfo
// Kodi impl: xbmc/platform/win32/DisplayUtilsWin32.cpp, xbmc/platform/win32/WIN32Util.cpp // Kodi impl: xbmc/platform/win32/DisplayUtilsWin32.cpp, xbmc/platform/win32/WIN32Util.cpp
class DisplayModeManager { class DisplayModeManager {
public: public:
@@ -98,9 +102,8 @@ class DisplayModeManager {
// Find the best matching refresh rate for a given video fps from available modes. // Find the best matching refresh rate for a given video fps from available modes.
// Returns 0 if no suitable match found. // Returns 0 if no suitable match found.
static DWORD FindBestRefreshRate(double video_fps, static DWORD FindBestRefreshRate(
const std::vector<DisplayMode>& modes, double video_fps, const std::vector<DisplayMode>& modes, DWORD current_width, DWORD current_height);
DWORD current_width, DWORD current_height);
private: private:
// Get the GDI device name for the monitor containing the window. // Get the GDI device name for the monitor containing the window.
+10 -17
View File
@@ -22,16 +22,13 @@ HWND MpvContainer::Create() {
// Use WS_POPUP for a borderless window without title bar. // Use WS_POPUP for a borderless window without title bar.
// Use WS_EX_TOOLWINDOW | WS_EX_NOREDIRECTIONBITMAP to prevent shadow and DWM effects. // Use WS_EX_TOOLWINDOW | WS_EX_NOREDIRECTIONBITMAP to prevent shadow and DWM effects.
handle_ = ::CreateWindowExW( handle_ = ::CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_NOREDIRECTIONBITMAP, WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_NOREDIRECTIONBITMAP, kClassName, kWindowName, WS_POPUP, 0, 0, 100,
kClassName, kWindowName, WS_POPUP, 100, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
0, 0, 100, 100, nullptr, nullptr,
GetModuleHandle(nullptr), nullptr);
// Disable DWM animations on the container. // Disable DWM animations on the container.
auto disable_window_transitions = TRUE; auto disable_window_transitions = TRUE;
DwmSetWindowAttribute(handle_, DWMWA_TRANSITIONS_FORCEDISABLED, DwmSetWindowAttribute(
&disable_window_transitions, handle_, DWMWA_TRANSITIONS_FORCEDISABLED, &disable_window_transitions, sizeof(disable_window_transitions));
sizeof(disable_window_transitions));
return handle_; return handle_;
} }
@@ -43,11 +40,10 @@ HWND MpvContainer::Get(HWND flutter_window) {
RECT window_rect; RECT window_rect;
::GetWindowRect(flutter_window, &window_rect); ::GetWindowRect(flutter_window, &window_rect);
::SetWindowPos(handle_, flutter_window, window_rect.left, window_rect.top, ::SetWindowPos(
window_rect.right - window_rect.left, handle_, flutter_window, window_rect.left, window_rect.top, window_rect.right - window_rect.left,
window_rect.bottom - window_rect.top, SWP_NOACTIVATE); window_rect.bottom - window_rect.top, SWP_NOACTIVATE);
::SetWindowLongPtr(handle_, GWLP_USERDATA, ::SetWindowLongPtr(handle_, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(flutter_window));
reinterpret_cast<LONG_PTR>(flutter_window));
::ShowWindow(handle_, SW_SHOWNOACTIVATE); ::ShowWindow(handle_, SW_SHOWNOACTIVATE);
::SetFocus(flutter_window); ::SetFocus(flutter_window);
@@ -55,10 +51,8 @@ HWND MpvContainer::Get(HWND flutter_window) {
return handle_; return handle_;
} }
LRESULT CALLBACK MpvContainer::WindowProc(HWND const window, LRESULT CALLBACK
UINT const message, MpvContainer::WindowProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept {
WPARAM const wparam,
LPARAM const lparam) noexcept {
switch (message) { switch (message) {
case WM_DESTROY: { case WM_DESTROY: {
::PostQuitMessage(0); ::PostQuitMessage(0);
@@ -87,7 +81,6 @@ LRESULT CALLBACK MpvContainer::WindowProc(HWND const window,
return ::DefWindowProc(window, message, wparam, lparam); return ::DefWindowProc(window, message, wparam, lparam);
} }
std::unique_ptr<MpvContainer> MpvContainer::instance_ = std::unique_ptr<MpvContainer> MpvContainer::instance_ = std::make_unique<MpvContainer>();
std::make_unique<MpvContainer>();
} // namespace mpv } // namespace mpv
+1 -2
View File
@@ -26,8 +26,7 @@ class MpvContainer {
HWND handle() const { return handle_; } HWND handle() const { return handle_; }
private: private:
static LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM wparam, static LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept;
LPARAM lparam) noexcept;
HWND handle_ = nullptr; HWND handle_ = nullptr;
+23 -35
View File
@@ -9,12 +9,9 @@ namespace mpv {
MpvCore* MpvCore::GetInstance() { return instance_.get(); } MpvCore* MpvCore::GetInstance() { return instance_.get(); }
void MpvCore::SetInstance(std::unique_ptr<MpvCore> instance) { void MpvCore::SetInstance(std::unique_ptr<MpvCore> instance) { instance_ = std::move(instance); }
instance_ = std::move(instance);
}
MpvCore::MpvCore(HWND flutter_window) MpvCore::MpvCore(HWND flutter_window) : flutter_window_(flutter_window) {}
: flutter_window_(flutter_window) {}
MpvCore::~MpvCore() { MpvCore::~MpvCore() {
// Close all mpv views. // Close all mpv views.
@@ -29,8 +26,7 @@ void MpvCore::EnsureInitialized() {
container_ = MpvContainer::GetInstance()->Get(flutter_window_); container_ = MpvContainer::GetInstance()->Get(flutter_window_);
} }
void MpvCore::CreateMpvView(HWND mpv_hwnd, RECT rect, void MpvCore::CreateMpvView(HWND mpv_hwnd, RECT rect, double device_pixel_ratio) {
double device_pixel_ratio) {
::SetParent(mpv_hwnd, container_); ::SetParent(mpv_hwnd, container_);
::ShowWindow(mpv_hwnd, SW_SHOW); ::ShowWindow(mpv_hwnd, SW_SHOW);
@@ -43,20 +39,18 @@ void MpvCore::CreateMpvView(HWND mpv_hwnd, RECT rect,
mpv_views_[mpv_hwnd] = rect; mpv_views_[mpv_hwnd] = rect;
// Position the mpv view behind the Flutter window. // Position the mpv view behind the Flutter window.
auto global_rect = auto global_rect = GetGlobalRect(rect.left, rect.top, rect.right, rect.bottom);
GetGlobalRect(rect.left, rect.top, rect.right, rect.bottom); ::SetWindowPos(
::SetWindowPos(mpv_hwnd, flutter_window_, global_rect.left, global_rect.top, mpv_hwnd, flutter_window_, global_rect.left, global_rect.top, global_rect.right - global_rect.left,
global_rect.right - global_rect.left,
global_rect.bottom - global_rect.top, SWP_NOACTIVATE); global_rect.bottom - global_rect.top, SWP_NOACTIVATE);
} }
void MpvCore::ResizeMpvView(HWND mpv_hwnd, RECT rect) { void MpvCore::ResizeMpvView(HWND mpv_hwnd, RECT rect) {
mpv_views_[mpv_hwnd] = rect; mpv_views_[mpv_hwnd] = rect;
auto global_rect = auto global_rect = GetGlobalRect(rect.left, rect.top, rect.right, rect.bottom);
GetGlobalRect(rect.left, rect.top, rect.right, rect.bottom);
// Use MoveWindow to trigger redraw. // Use MoveWindow to trigger redraw.
::MoveWindow(mpv_hwnd, global_rect.left, global_rect.top, ::MoveWindow(
global_rect.right - global_rect.left, mpv_hwnd, global_rect.left, global_rect.top, global_rect.right - global_rect.left,
global_rect.bottom - global_rect.top, TRUE); global_rect.bottom - global_rect.top, TRUE);
} }
@@ -76,15 +70,14 @@ void MpvCore::SetVisible(bool visible) {
} }
} }
std::optional<HRESULT> MpvCore::WindowProc(HWND hwnd, UINT message, std::optional<HRESULT> MpvCore::WindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
WPARAM wparam, LPARAM lparam) {
switch (message) { switch (message) {
case WM_ACTIVATE: { case WM_ACTIVATE: {
RECT window_rect; RECT window_rect;
::GetWindowRect(flutter_window_, &window_rect); ::GetWindowRect(flutter_window_, &window_rect);
// Position container behind Flutter window. // Position container behind Flutter window.
::SetWindowPos(container_, flutter_window_, window_rect.left, ::SetWindowPos(
window_rect.top, window_rect.right - window_rect.left, container_, flutter_window_, window_rect.left, window_rect.top, window_rect.right - window_rect.left,
window_rect.bottom - window_rect.top, SWP_NOACTIVATE); window_rect.bottom - window_rect.top, SWP_NOACTIVATE);
break; break;
} }
@@ -92,8 +85,7 @@ std::optional<HRESULT> MpvCore::WindowProc(HWND hwnd, UINT message,
// Handle Windows's minimize & maximize animations properly. // Handle Windows's minimize & maximize animations properly.
// During these transitions, we hide the container and make Flutter opaque, // During these transitions, we hide the container and make Flutter opaque,
// then restore after the animation completes using a Windows timer. // then restore after the animation completes using a Windows timer.
if (wparam != SIZE_RESTORED || last_wm_size_wparam_ == SIZE_MINIMIZED || if (wparam != SIZE_RESTORED || last_wm_size_wparam_ == SIZE_MINIMIZED || last_wm_size_wparam_ == SIZE_MAXIMIZED ||
last_wm_size_wparam_ == SIZE_MAXIMIZED ||
was_window_hidden_due_to_minimize_) { was_window_hidden_due_to_minimize_) {
was_window_hidden_due_to_minimize_ = false; was_window_hidden_due_to_minimize_ = false;
DisableComposition(); DisableComposition();
@@ -112,8 +104,8 @@ std::optional<HRESULT> MpvCore::WindowProc(HWND hwnd, UINT message,
// Update container position to match current Flutter window bounds // Update container position to match current Flutter window bounds
RECT window_rect; RECT window_rect;
::GetWindowRect(flutter_window_, &window_rect); ::GetWindowRect(flutter_window_, &window_rect);
::SetWindowPos(container_, flutter_window_, window_rect.left, ::SetWindowPos(
window_rect.top, window_rect.right - window_rect.left, container_, flutter_window_, window_rect.left, window_rect.top, window_rect.right - window_rect.left,
window_rect.bottom - window_rect.top, SWP_NOACTIVATE); window_rect.bottom - window_rect.top, SWP_NOACTIVATE);
// Restore transparency if video is visible // Restore transparency if video is visible
@@ -121,8 +113,7 @@ std::optional<HRESULT> MpvCore::WindowProc(HWND hwnd, UINT message,
EnableComposition(); EnableComposition();
// Force a redraw to ensure Flutter's render surface is correctly sized // Force a redraw to ensure Flutter's render surface is correctly sized
::RedrawWindow(flutter_window_, nullptr, nullptr, ::RedrawWindow(flutter_window_, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN);
RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN);
} }
} }
break; break;
@@ -130,14 +121,12 @@ std::optional<HRESULT> MpvCore::WindowProc(HWND hwnd, UINT message,
case WM_WINDOWPOSCHANGED: { case WM_WINDOWPOSCHANGED: {
RECT window_rect; RECT window_rect;
::GetWindowRect(flutter_window_, &window_rect); ::GetWindowRect(flutter_window_, &window_rect);
if (window_rect.right - window_rect.left > 0 && if (window_rect.right - window_rect.left > 0 && window_rect.bottom - window_rect.top > 0) {
window_rect.bottom - window_rect.top > 0) { ::SetWindowPos(
::SetWindowPos(container_, flutter_window_, window_rect.left, container_, flutter_window_, window_rect.left, window_rect.top, window_rect.right - window_rect.left,
window_rect.top, window_rect.right - window_rect.left,
window_rect.bottom - window_rect.top, SWP_NOACTIVATE); window_rect.bottom - window_rect.top, SWP_NOACTIVATE);
// Window is minimized (negative coordinates). // Window is minimized (negative coordinates).
if (window_rect.left < 0 && window_rect.top < 0 && if (window_rect.left < 0 && window_rect.top < 0 && window_rect.right < 0 && window_rect.bottom < 0) {
window_rect.right < 0 && window_rect.bottom < 0) {
DisableComposition(); DisableComposition();
was_window_hidden_due_to_minimize_ = true; was_window_hidden_due_to_minimize_ = true;
} }
@@ -158,8 +147,7 @@ std::optional<HRESULT> MpvCore::WindowProc(HWND hwnd, UINT message,
return std::nullopt; return std::nullopt;
} }
RECT MpvCore::GetGlobalRect(int32_t left, int32_t top, int32_t right, RECT MpvCore::GetGlobalRect(int32_t left, int32_t top, int32_t right, int32_t bottom) {
int32_t bottom) {
// Expand client area to prevent transparent gaps. // Expand client area to prevent transparent gaps.
left -= static_cast<int32_t>(ceil(device_pixel_ratio_)); left -= static_cast<int32_t>(ceil(device_pixel_ratio_));
top -= static_cast<int32_t>(ceil(device_pixel_ratio_)); top -= static_cast<int32_t>(ceil(device_pixel_ratio_));
@@ -177,8 +165,8 @@ RECT MpvCore::GetGlobalRect(int32_t left, int32_t top, int32_t right,
} }
void MpvCore::EnableComposition() { void MpvCore::EnableComposition() {
::SetWindowPos(flutter_window_, nullptr, 0, 0, 0, 0, ::SetWindowPos(
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); flutter_window_, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
if (!composition_enabled_) { if (!composition_enabled_) {
SetWindowComposition(flutter_window_, 2, 0); SetWindowComposition(flutter_window_, 2, 0);
composition_enabled_ = true; composition_enabled_ = true;
+1 -2
View File
@@ -39,8 +39,7 @@ class MpvCore {
void SetVisible(bool visible); void SetVisible(bool visible);
// Window procedure handler for Flutter window messages. // Window procedure handler for Flutter window messages.
std::optional<HRESULT> WindowProc(HWND hwnd, UINT message, WPARAM wparam, std::optional<HRESULT> WindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
LPARAM lparam);
private: private:
RECT GetGlobalRect(int32_t left, int32_t top, int32_t right, int32_t bottom); RECT GetGlobalRect(int32_t left, int32_t top, int32_t right, int32_t bottom);
+18 -31
View File
@@ -23,9 +23,8 @@ bool MpvPlayer::Initialize(HWND container, HWND flutter_window) {
} }
// Create a child window for mpv to render into. // Create a child window for mpv to render into.
hwnd_ = ::CreateWindowW(L"STATIC", L"", WS_CHILD | WS_VISIBLE, 0, 0, 100, 100, hwnd_ = ::CreateWindowW(
container, nullptr, GetModuleHandle(nullptr), L"STATIC", L"", WS_CHILD | WS_VISIBLE, 0, 0, 100, 100, container, nullptr, GetModuleHandle(nullptr), nullptr);
nullptr);
if (!hwnd_) { if (!hwnd_) {
mpv_destroy(mpv_); mpv_destroy(mpv_);
mpv_ = nullptr; mpv_ = nullptr;
@@ -118,8 +117,7 @@ void MpvPlayer::Command(const std::vector<std::string>& args) {
mpv_command(mpv_, c_args.data()); mpv_command(mpv_, c_args.data());
} }
void MpvPlayer::CommandAsync(const std::vector<std::string>& args, void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallback callback) {
CommandCallback callback) {
if (!mpv_) { if (!mpv_) {
if (callback) callback(0); if (callback) callback(0);
return; return;
@@ -178,9 +176,7 @@ std::string MpvPlayer::GetProperty(const std::string& name) {
return result; return result;
} }
void MpvPlayer::ObserveProperty(const std::string& name, void MpvPlayer::ObserveProperty(const std::string& name, const std::string& format, int id) {
const std::string& format,
int id) {
if (!mpv_) return; if (!mpv_) return;
// Check if already observing. // Check if already observing.
@@ -213,9 +209,10 @@ void MpvPlayer::SetRect(RECT rect, double device_pixel_ratio) {
device_pixel_ratio_ = device_pixel_ratio; device_pixel_ratio_ = device_pixel_ratio;
if (hwnd_ && container_ && flutter_window_) { if (hwnd_ && container_ && flutter_window_) {
// The rect from Dart is in Flutter client area coordinates (0,0 is top-left of Flutter content). // The rect from Dart is in Flutter client area coordinates (0,0 is top-left of Flutter
// The container window is positioned to match the Flutter window's full bounds (including title bar). // content). The container window is positioned to match the Flutter window's full bounds
// We need to offset the mpv window within the container to align with Flutter's client area. // (including title bar). We need to offset the mpv window within the container to align with
// Flutter's client area.
// Get the Flutter window's window rect (screen coordinates, includes title bar) // Get the Flutter window's window rect (screen coordinates, includes title bar)
RECT window_rect; RECT window_rect;
@@ -310,17 +307,13 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
case MPV_EVENT_LOG_MESSAGE: { case MPV_EVENT_LOG_MESSAGE: {
auto* msg = static_cast<mpv_event_log_message*>(event->data); auto* msg = static_cast<mpv_event_log_message*>(event->data);
char log_msg[512]; char log_msg[512];
snprintf(log_msg, sizeof(log_msg), "MPV [%s] %s: %s", snprintf(log_msg, sizeof(log_msg), "MPV [%s] %s: %s", msg->level, msg->prefix, msg->text);
msg->level, msg->prefix, msg->text);
OutputDebugStringA(log_msg); OutputDebugStringA(log_msg);
flutter::EncodableMap data; flutter::EncodableMap data;
data[flutter::EncodableValue("prefix")] = data[flutter::EncodableValue("prefix")] = flutter::EncodableValue(SanitizeUtf8(msg->prefix));
flutter::EncodableValue(SanitizeUtf8(msg->prefix)); data[flutter::EncodableValue("level")] = flutter::EncodableValue(SanitizeUtf8(msg->level));
data[flutter::EncodableValue("level")] = data[flutter::EncodableValue("text")] = flutter::EncodableValue(SanitizeUtf8(msg->text));
flutter::EncodableValue(SanitizeUtf8(msg->level));
data[flutter::EncodableValue("text")] =
flutter::EncodableValue(SanitizeUtf8(msg->text));
SendEvent("log-message", data); SendEvent("log-message", data);
break; break;
} }
@@ -353,8 +346,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
} }
// Handle sig-peak for HDR detection // Handle sig-peak for HDR detection
if (strcmp(prop->name, "video-params/sig-peak") == 0 && if (strcmp(prop->name, "video-params/sig-peak") == 0 && prop->format == MPV_FORMAT_DOUBLE && prop->data) {
prop->format == MPV_FORMAT_DOUBLE && prop->data) {
double sigPeak = *static_cast<double*>(prop->data); double sigPeak = *static_cast<double*>(prop->data);
last_sig_peak_ = sigPeak; last_sig_peak_ = sigPeak;
UpdateHDRMode(sigPeak); UpdateHDRMode(sigPeak);
@@ -364,8 +356,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
// to null output (e.g. after sleep/wake or device unplug), re-set // to null output (e.g. after sleep/wake or device unplug), re-set
// audio-device to switch back to the real output. // audio-device to switch back to the real output.
// Mirrors mpv's TOOLS/lua/ao-null-reload.lua for embedded libmpv. // Mirrors mpv's TOOLS/lua/ao-null-reload.lua for embedded libmpv.
if (strcmp(prop->name, "audio-device-list") == 0 && if (strcmp(prop->name, "audio-device-list") == 0 && GetProperty("current-ao") == "null") {
GetProperty("current-ao") == "null") {
auto device = GetProperty("audio-device"); auto device = GetProperty("audio-device");
if (!device.empty()) { if (!device.empty()) {
mpv_set_property_string(mpv_, "audio-device", device.c_str()); mpv_set_property_string(mpv_, "audio-device", device.c_str());
@@ -378,13 +369,10 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
case MPV_EVENT_END_FILE: { case MPV_EVENT_END_FILE: {
auto* end = static_cast<mpv_event_end_file*>(event->data); auto* end = static_cast<mpv_event_end_file*>(event->data);
flutter::EncodableMap data; flutter::EncodableMap data;
data[flutter::EncodableValue("reason")] = data[flutter::EncodableValue("reason")] = flutter::EncodableValue(static_cast<int>(end->reason));
flutter::EncodableValue(static_cast<int>(end->reason));
if (end->reason == MPV_END_FILE_REASON_ERROR) { if (end->reason == MPV_END_FILE_REASON_ERROR) {
data[flutter::EncodableValue("error")] = data[flutter::EncodableValue("error")] = flutter::EncodableValue(static_cast<int>(end->error));
flutter::EncodableValue(static_cast<int>(end->error)); data[flutter::EncodableValue("message")] = flutter::EncodableValue(SanitizeUtf8(mpv_error_string(end->error)));
data[flutter::EncodableValue("message")] =
flutter::EncodableValue(SanitizeUtf8(mpv_error_string(end->error)));
} }
SendEvent("end-file", data); SendEvent("end-file", data);
break; break;
@@ -443,8 +431,7 @@ void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
} }
} }
void MpvPlayer::SendEvent(const std::string& name, void MpvPlayer::SendEvent(const std::string& name, const flutter::EncodableMap& data) {
const flutter::EncodableMap& data) {
flutter::EncodableMap event; flutter::EncodableMap event;
event[flutter::EncodableValue("type")] = flutter::EncodableValue("event"); event[flutter::EncodableValue("type")] = flutter::EncodableValue("event");
event[flutter::EncodableValue("name")] = flutter::EncodableValue(name); event[flutter::EncodableValue("name")] = flutter::EncodableValue(name);
+4 -8
View File
@@ -2,6 +2,7 @@
#define MPV_PLAYER_H_ #define MPV_PLAYER_H_
#include <Windows.h> #include <Windows.h>
#include <flutter/encodable_value.h>
#include <mpv/client.h> #include <mpv/client.h>
#include <atomic> #include <atomic>
@@ -13,16 +14,13 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
#include <flutter/encodable_value.h>
namespace mpv { namespace mpv {
// Wrapper for libmpv that handles initialization, commands, properties, // Wrapper for libmpv that handles initialization, commands, properties,
// and event dispatching. // and event dispatching.
class MpvPlayer { class MpvPlayer {
public: public:
using EventCallback = using EventCallback = std::function<void(const flutter::EncodableValue&)>;
std::function<void(const flutter::EncodableValue&)>;
MpvPlayer(); MpvPlayer();
~MpvPlayer(); ~MpvPlayer();
@@ -53,8 +51,7 @@ class MpvPlayer {
std::string GetProperty(const std::string& name); std::string GetProperty(const std::string& name);
// Observes an mpv property for changes. // Observes an mpv property for changes.
void ObserveProperty(const std::string& name, const std::string& format, void ObserveProperty(const std::string& name, const std::string& format, int id);
int id);
// Returns the mpv video window handle. // Returns the mpv video window handle.
HWND GetHwnd() const { return hwnd_; } HWND GetHwnd() const { return hwnd_; }
@@ -77,8 +74,7 @@ class MpvPlayer {
void EventLoop(); void EventLoop();
void HandleMpvEvent(mpv_event* event); void HandleMpvEvent(mpv_event* event);
void SendPropertyChange(const char* name, mpv_node* data); void SendPropertyChange(const char* name, mpv_node* data);
void SendEvent(const std::string& name, void SendEvent(const std::string& name, const flutter::EncodableMap& data = {});
const flutter::EncodableMap& data = {});
mpv_handle* mpv_ = nullptr; mpv_handle* mpv_ = nullptr;
HWND hwnd_ = nullptr; HWND hwnd_ = nullptr;
+41 -71
View File
@@ -11,52 +11,40 @@ static flutter::EncodableMap DisplayModeToMap(const mpv::DisplayMode& mode) {
return m; return m;
} }
void MpvPlayerPluginRegisterWithRegistrar( void MpvPlayerPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar) {
FlutterDesktopPluginRegistrarRef registrar) {
mpv::MpvPlayerPlugin::RegisterWithRegistrar( mpv::MpvPlayerPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarManager::GetInstance() flutter::PluginRegistrarManager::GetInstance()->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
} }
namespace mpv { namespace mpv {
void MpvPlayerPlugin::RegisterWithRegistrar( void MpvPlayerPlugin::RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar) {
flutter::PluginRegistrarWindows* registrar) {
auto plugin = std::make_unique<MpvPlayerPlugin>(registrar); auto plugin = std::make_unique<MpvPlayerPlugin>(registrar);
registrar->AddPlugin(std::move(plugin)); registrar->AddPlugin(std::move(plugin));
} }
MpvPlayerPlugin::MpvPlayerPlugin(flutter::PluginRegistrarWindows* registrar) MpvPlayerPlugin::MpvPlayerPlugin(flutter::PluginRegistrarWindows* registrar) : registrar_(registrar) {
: registrar_(registrar) {
// Create method channel. // Create method channel.
method_channel_ = method_channel_ = std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>( registrar->messenger(), "com.plezy/mpv_player", &flutter::StandardMethodCodec::GetInstance());
registrar->messenger(), "com.plezy/mpv_player",
&flutter::StandardMethodCodec::GetInstance());
method_channel_->SetMethodCallHandler( method_channel_->SetMethodCallHandler(
[this](const auto& call, auto result) { [this](const auto& call, auto result) { HandleMethodCall(call, std::move(result)); });
HandleMethodCall(call, std::move(result));
});
// Create event channel. // Create event channel.
event_channel_ = event_channel_ = std::make_unique<flutter::EventChannel<flutter::EncodableValue>>(
std::make_unique<flutter::EventChannel<flutter::EncodableValue>>( registrar->messenger(), "com.plezy/mpv_player/events", &flutter::StandardMethodCodec::GetInstance());
registrar->messenger(), "com.plezy/mpv_player/events",
&flutter::StandardMethodCodec::GetInstance());
auto handler = std::make_unique< auto handler = std::make_unique<flutter::StreamHandlerFunctions<flutter::EncodableValue>>(
flutter::StreamHandlerFunctions<flutter::EncodableValue>>( [this](
[this](const flutter::EncodableValue* arguments, const flutter::EncodableValue* arguments,
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& events)
events) -> std::unique_ptr<flutter::StreamHandlerError< -> std::unique_ptr<flutter::StreamHandlerError<flutter::EncodableValue>> {
flutter::EncodableValue>> {
event_sink_ = std::move(events); event_sink_ = std::move(events);
return nullptr; return nullptr;
}, },
[this](const flutter::EncodableValue* arguments) [this](const flutter::EncodableValue* arguments)
-> std::unique_ptr< -> std::unique_ptr<flutter::StreamHandlerError<flutter::EncodableValue>> {
flutter::StreamHandlerError<flutter::EncodableValue>> {
event_sink_ = nullptr; event_sink_ = nullptr;
return nullptr; return nullptr;
}); });
@@ -72,13 +60,9 @@ MpvPlayerPlugin::~MpvPlayerPlugin() {
} }
} }
HWND MpvPlayerPlugin::GetChildWindow() { HWND MpvPlayerPlugin::GetChildWindow() { return registrar_->GetView()->GetNativeWindow(); }
return registrar_->GetView()->GetNativeWindow();
}
HWND MpvPlayerPlugin::GetWindow() { HWND MpvPlayerPlugin::GetWindow() { return ::GetAncestor(GetChildWindow(), GA_ROOT); }
return ::GetAncestor(GetChildWindow(), GA_ROOT);
}
void MpvPlayerPlugin::HandleMethodCall( void MpvPlayerPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call, const flutter::MethodCall<flutter::EncodableValue>& method_call,
@@ -94,11 +78,10 @@ void MpvPlayerPlugin::HandleMethodCall(
HWND flutter_window = GetWindow(); HWND flutter_window = GetWindow();
MpvCore::SetInstance( MpvCore::SetInstance(std::make_unique<MpvCore>(flutter_window));
std::make_unique<MpvCore>(flutter_window));
proc_id_ = registrar_->RegisterTopLevelWindowProcDelegate( proc_id_ =
[](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { registrar_->RegisterTopLevelWindowProcDelegate([](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
auto* core = MpvCore::GetInstance(); auto* core = MpvCore::GetInstance();
if (core) { if (core) {
return core->WindowProc(hwnd, message, wparam, lparam); return core->WindowProc(hwnd, message, wparam, lparam);
@@ -121,9 +104,7 @@ void MpvPlayerPlugin::HandleMethodCall(
if (success) { if (success) {
// Set up event callback. // Set up event callback.
player_->SetEventCallback([this](const flutter::EncodableValue& event) { player_->SetEventCallback([this](const flutter::EncodableValue& event) { SendEvent(event); });
SendEvent(event);
});
// Register the mpv window with core for z-order management. // Register the mpv window with core for z-order management.
RECT rect = {0, 0, 100, 100}; RECT rect = {0, 0, 100, 100};
@@ -161,8 +142,7 @@ void MpvPlayerPlugin::HandleMethodCall(
const auto& map = std::get<flutter::EncodableMap>(*args); const auto& map = std::get<flutter::EncodableMap>(*args);
auto it = map.find(flutter::EncodableValue("args")); auto it = map.find(flutter::EncodableValue("args"));
if (it == map.end() || if (it == map.end() || !std::holds_alternative<flutter::EncodableList>(it->second)) {
!std::holds_alternative<flutter::EncodableList>(it->second)) {
result->Error("INVALID_ARGS", "Missing 'args' list"); result->Error("INVALID_ARGS", "Missing 'args' list");
return; return;
} }
@@ -177,12 +157,13 @@ void MpvPlayerPlugin::HandleMethodCall(
// Use async command to prevent UI blocking during network operations // Use async command to prevent UI blocking during network operations
// Move result into shared_ptr for safe capture in callback // Move result into shared_ptr for safe capture in callback
auto result_ptr = std::make_shared<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(std::move(result)); auto result_ptr =
std::make_shared<std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>>>(std::move(result));
std::string cmd_name = command_args.empty() ? "unknown" : command_args[0]; std::string cmd_name = command_args.empty() ? "unknown" : command_args[0];
player_->CommandAsync(command_args, [result_ptr, cmd_name](int error) { player_->CommandAsync(command_args, [result_ptr, cmd_name](int error) {
if (error < 0) { if (error < 0) {
(*result_ptr)->Error("COMMAND_FAILED", (*result_ptr)
"MPV command failed: " + cmd_name + " (error " + std::to_string(error) + ")"); ->Error("COMMAND_FAILED", "MPV command failed: " + cmd_name + " (error " + std::to_string(error) + ")");
} else { } else {
(*result_ptr)->Success(); (*result_ptr)->Success();
} }
@@ -204,19 +185,16 @@ void MpvPlayerPlugin::HandleMethodCall(
auto name_it = map.find(flutter::EncodableValue("name")); auto name_it = map.find(flutter::EncodableValue("name"));
auto value_it = map.find(flutter::EncodableValue("value")); auto value_it = map.find(flutter::EncodableValue("value"));
if (name_it == map.end() || if (name_it == map.end() || !std::holds_alternative<std::string>(name_it->second)) {
!std::holds_alternative<std::string>(name_it->second)) {
result->Error("INVALID_ARGS", "Missing 'name'"); result->Error("INVALID_ARGS", "Missing 'name'");
return; return;
} }
if (value_it == map.end() || if (value_it == map.end() || !std::holds_alternative<std::string>(value_it->second)) {
!std::holds_alternative<std::string>(value_it->second)) {
result->Error("INVALID_ARGS", "Missing 'value'"); result->Error("INVALID_ARGS", "Missing 'value'");
return; return;
} }
player_->SetProperty(std::get<std::string>(name_it->second), player_->SetProperty(std::get<std::string>(name_it->second), std::get<std::string>(value_it->second));
std::get<std::string>(value_it->second));
result->Success(); result->Success();
} else if (method == "setLogLevel") { } else if (method == "setLogLevel") {
if (!player_ || !player_->IsInitialized()) { if (!player_ || !player_->IsInitialized()) {
@@ -233,8 +211,7 @@ void MpvPlayerPlugin::HandleMethodCall(
const auto& map = std::get<flutter::EncodableMap>(*args); const auto& map = std::get<flutter::EncodableMap>(*args);
auto level_it = map.find(flutter::EncodableValue("level")); auto level_it = map.find(flutter::EncodableValue("level"));
if (level_it == map.end() || if (level_it == map.end() || !std::holds_alternative<std::string>(level_it->second)) {
!std::holds_alternative<std::string>(level_it->second)) {
result->Error("INVALID_ARGS", "Missing 'level'"); result->Error("INVALID_ARGS", "Missing 'level'");
return; return;
} }
@@ -256,14 +233,12 @@ void MpvPlayerPlugin::HandleMethodCall(
const auto& map = std::get<flutter::EncodableMap>(*args); const auto& map = std::get<flutter::EncodableMap>(*args);
auto name_it = map.find(flutter::EncodableValue("name")); auto name_it = map.find(flutter::EncodableValue("name"));
if (name_it == map.end() || if (name_it == map.end() || !std::holds_alternative<std::string>(name_it->second)) {
!std::holds_alternative<std::string>(name_it->second)) {
result->Error("INVALID_ARGS", "Missing 'name'"); result->Error("INVALID_ARGS", "Missing 'name'");
return; return;
} }
std::string value = std::string value = player_->GetProperty(std::get<std::string>(name_it->second));
player_->GetProperty(std::get<std::string>(name_it->second));
if (value.empty()) { if (value.empty()) {
result->Success(); result->Success();
} else { } else {
@@ -286,24 +261,21 @@ void MpvPlayerPlugin::HandleMethodCall(
auto format_it = map.find(flutter::EncodableValue("format")); auto format_it = map.find(flutter::EncodableValue("format"));
auto id_it = map.find(flutter::EncodableValue("id")); auto id_it = map.find(flutter::EncodableValue("id"));
if (name_it == map.end() || if (name_it == map.end() || !std::holds_alternative<std::string>(name_it->second)) {
!std::holds_alternative<std::string>(name_it->second)) {
result->Error("INVALID_ARGS", "Missing 'name'"); result->Error("INVALID_ARGS", "Missing 'name'");
return; return;
} }
if (format_it == map.end() || if (format_it == map.end() || !std::holds_alternative<std::string>(format_it->second)) {
!std::holds_alternative<std::string>(format_it->second)) {
result->Error("INVALID_ARGS", "Missing 'format'"); result->Error("INVALID_ARGS", "Missing 'format'");
return; return;
} }
if (id_it == map.end() || if (id_it == map.end() || !std::holds_alternative<int32_t>(id_it->second)) {
!std::holds_alternative<int32_t>(id_it->second)) {
result->Error("INVALID_ARGS", "Missing 'id'"); result->Error("INVALID_ARGS", "Missing 'id'");
return; return;
} }
player_->ObserveProperty(std::get<std::string>(name_it->second), player_->ObserveProperty(
std::get<std::string>(format_it->second), std::get<std::string>(name_it->second), std::get<std::string>(format_it->second),
std::get<int32_t>(id_it->second)); std::get<int32_t>(id_it->second));
result->Success(); result->Success();
} else if (method == "setVisible") { } else if (method == "setVisible") {
@@ -316,8 +288,7 @@ void MpvPlayerPlugin::HandleMethodCall(
const auto& map = std::get<flutter::EncodableMap>(*args); const auto& map = std::get<flutter::EncodableMap>(*args);
auto visible_it = map.find(flutter::EncodableValue("visible")); auto visible_it = map.find(flutter::EncodableValue("visible"));
if (visible_it == map.end() || if (visible_it == map.end() || !std::holds_alternative<bool>(visible_it->second)) {
!std::holds_alternative<bool>(visible_it->second)) {
result->Error("INVALID_ARGS", "Missing 'visible'"); result->Error("INVALID_ARGS", "Missing 'visible'");
return; return;
} }
@@ -400,13 +371,12 @@ void MpvPlayerPlugin::HandleMethodCall(
const auto& map = std::get<flutter::EncodableMap>(*args); const auto& map = std::get<flutter::EncodableMap>(*args);
auto get_int = [&map](const char* key) -> int { auto get_int = [&map](const char* key) -> int {
auto it = map.find(flutter::EncodableValue(key)); auto it = map.find(flutter::EncodableValue(key));
if (it != map.end() && std::holds_alternative<int32_t>(it->second)) if (it != map.end() && std::holds_alternative<int32_t>(it->second)) return std::get<int32_t>(it->second);
return std::get<int32_t>(it->second);
return 0; return 0;
}; };
HWND hwnd = GetWindow(); HWND hwnd = GetWindow();
bool success = display_mode_manager_.SetDisplayMode( bool success =
hwnd, get_int("width"), get_int("height"), get_int("refreshRate")); display_mode_manager_.SetDisplayMode(hwnd, get_int("width"), get_int("height"), get_int("refreshRate"));
result->Success(flutter::EncodableValue(success)); result->Success(flutter::EncodableValue(success));
} else if (method == "restoreDisplayMode") { } else if (method == "restoreDisplayMode") {
HWND hwnd = GetWindow(); HWND hwnd = GetWindow();
+3 -6
View File
@@ -16,8 +16,7 @@
#include "mpv_player.h" #include "mpv_player.h"
// C-style registration function for the plugin. // C-style registration function for the plugin.
void MpvPlayerPluginRegisterWithRegistrar( void MpvPlayerPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar);
FlutterDesktopPluginRegistrarRef registrar);
namespace mpv { namespace mpv {
@@ -39,10 +38,8 @@ class MpvPlayerPlugin : public flutter::Plugin {
HWND GetChildWindow(); HWND GetChildWindow();
flutter::PluginRegistrarWindows* registrar_; flutter::PluginRegistrarWindows* registrar_;
std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> method_channel_;
method_channel_; std::unique_ptr<flutter::EventChannel<flutter::EncodableValue>> event_channel_;
std::unique_ptr<flutter::EventChannel<flutter::EncodableValue>>
event_channel_;
std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> event_sink_; std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> event_sink_;
std::unique_ptr<MpvPlayer> player_; std::unique_ptr<MpvPlayer> player_;
+5 -10
View File
@@ -56,8 +56,7 @@ typedef struct _ACCENT_POLICY {
DWORD AnimationId; DWORD AnimationId;
} ACCENT_POLICY; } ACCENT_POLICY;
typedef BOOL(WINAPI* _SetWindowCompositionAttribute)( typedef BOOL(WINAPI* _SetWindowCompositionAttribute)(HWND, WINDOWCOMPOSITIONATTRIBDATA*);
HWND, WINDOWCOMPOSITIONATTRIBDATA*);
static _SetWindowCompositionAttribute g_set_window_composition_attribute = NULL; static _SetWindowCompositionAttribute g_set_window_composition_attribute = NULL;
static bool g_set_window_composition_attribute_initialized = false; static bool g_set_window_composition_attribute_initialized = false;
@@ -71,8 +70,7 @@ static RTL_OSVERSIONINFOW GetWindowsVersion() {
static RTL_OSVERSIONINFOW cached = []() { static RTL_OSVERSIONINFOW cached = []() {
HMODULE hmodule = ::GetModuleHandleW(L"ntdll.dll"); HMODULE hmodule = ::GetModuleHandleW(L"ntdll.dll");
if (hmodule) { if (hmodule) {
RtlGetVersionPtr rtl_get_version_ptr = RtlGetVersionPtr rtl_get_version_ptr = (RtlGetVersionPtr)::GetProcAddress(hmodule, "RtlGetVersion");
(RtlGetVersionPtr)::GetProcAddress(hmodule, "RtlGetVersion");
if (rtl_get_version_ptr != nullptr) { if (rtl_get_version_ptr != nullptr) {
RTL_OSVERSIONINFOW rovi = {0}; RTL_OSVERSIONINFOW rovi = {0};
rovi.dwOSVersionInfoSize = sizeof(rovi); rovi.dwOSVersionInfoSize = sizeof(rovi);
@@ -87,23 +85,20 @@ static RTL_OSVERSIONINFOW GetWindowsVersion() {
return cached; return cached;
} }
void SetWindowComposition(HWND window, int32_t accent_state, void SetWindowComposition(HWND window, int32_t accent_state, int32_t gradient_color) {
int32_t gradient_color) {
if (GetWindowsVersion().dwBuildNumber >= 18362) { if (GetWindowsVersion().dwBuildNumber >= 18362) {
if (!g_set_window_composition_attribute_initialized) { if (!g_set_window_composition_attribute_initialized) {
auto user32 = ::GetModuleHandleA("user32.dll"); auto user32 = ::GetModuleHandleA("user32.dll");
if (user32) { if (user32) {
g_set_window_composition_attribute = g_set_window_composition_attribute =
reinterpret_cast<_SetWindowCompositionAttribute>( reinterpret_cast<_SetWindowCompositionAttribute>(::GetProcAddress(user32, "SetWindowCompositionAttribute"));
::GetProcAddress(user32, "SetWindowCompositionAttribute"));
if (g_set_window_composition_attribute) { if (g_set_window_composition_attribute) {
g_set_window_composition_attribute_initialized = true; g_set_window_composition_attribute_initialized = true;
} }
} }
} }
if (g_set_window_composition_attribute) { if (g_set_window_composition_attribute) {
ACCENT_POLICY accent = {static_cast<ACCENT_STATE>(accent_state), 2, ACCENT_POLICY accent = {static_cast<ACCENT_STATE>(accent_state), 2, static_cast<DWORD>(gradient_color), 0};
static_cast<DWORD>(gradient_color), 0};
WINDOWCOMPOSITIONATTRIBDATA data; WINDOWCOMPOSITIONATTRIBDATA data;
data.Attrib = WCA_ACCENT_POLICY; data.Attrib = WCA_ACCENT_POLICY;
data.pvData = &accent; data.pvData = &accent;
+1 -2
View File
@@ -11,8 +11,7 @@ namespace mpv {
// Sets window composition attribute for transparency. // Sets window composition attribute for transparency.
// accent_state = 6 enables per-pixel transparency. // accent_state = 6 enables per-pixel transparency.
// accent_state = 0 makes window opaque. // accent_state = 0 makes window opaque.
void SetWindowComposition(HWND window, int32_t accent_state, void SetWindowComposition(HWND window, int32_t accent_state, int32_t gradient_color);
int32_t gradient_color);
} // namespace mpv } // namespace mpv
+7 -7
View File
@@ -3,17 +3,17 @@
#include <Windows.h> #include <Windows.h>
#include <dwmapi.h> #include <dwmapi.h>
#include <optional>
#include <memory>
#include <io.h> #include <io.h>
#include <stdio.h> #include <stdio.h>
#include <algorithm>
#include <cmath>
#include <functional>
#include <iostream> #include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
#include <functional>
#include <cmath>
#include <algorithm>
#include <map>
#endif #endif
+3 -6
View File
@@ -9,7 +9,7 @@
void CreateAndAttachConsole() { void CreateAndAttachConsole() {
if (::AllocConsole()) { if (::AllocConsole()) {
FILE *unused; FILE* unused;
freopen_s(&unused, "CONOUT$", "w", stdout); freopen_s(&unused, "CONOUT$", "w", stdout);
freopen_s(&unused, "CONOUT$", "w", stderr); freopen_s(&unused, "CONOUT$", "w", stderr);
std::ios::sync_with_stdio(); std::ios::sync_with_stdio();
@@ -41,9 +41,7 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) {
if (utf16_string == nullptr) { if (utf16_string == nullptr) {
return std::string(); return std::string();
} }
int raw_length = ::WideCharToMultiByte( int raw_length = ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr);
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, nullptr, 0, nullptr, nullptr);
if (raw_length <= 1) { if (raw_length <= 1) {
return std::string(); return std::string();
} }
@@ -52,8 +50,7 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) {
std::string utf8_string; std::string utf8_string;
utf8_string.resize(target_length); utf8_string.resize(target_length);
int converted_length = ::WideCharToMultiByte( int converted_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, input_length, utf8_string.data(), target_length, nullptr, nullptr);
input_length, utf8_string.data(), target_length, nullptr, nullptr);
if (converted_length == 0) { if (converted_length == 0) {
return std::string(); return std::string();
} }
+26 -54
View File
@@ -33,9 +33,7 @@ using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
// Scale helper to convert logical scaler values to physical using passed in // Scale helper to convert logical scaler values to physical using passed in
// scale factor // scale factor
int Scale(int source, double scale_factor) { int Scale(int source, double scale_factor) { return static_cast<int>(source * scale_factor); }
return static_cast<int>(source * scale_factor);
}
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
// This API is only needed for PerMonitor V1 awareness mode. // This API is only needed for PerMonitor V1 awareness mode.
@@ -45,8 +43,7 @@ void EnableFullDpiSupportIfAvailable(HWND hwnd) {
return; return;
} }
auto enable_non_client_dpi_scaling = auto enable_non_client_dpi_scaling =
reinterpret_cast<EnableNonClientDpiScaling*>( reinterpret_cast<EnableNonClientDpiScaling*>(GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
if (enable_non_client_dpi_scaling != nullptr) { if (enable_non_client_dpi_scaling != nullptr) {
enable_non_client_dpi_scaling(hwnd); enable_non_client_dpi_scaling(hwnd);
} }
@@ -95,8 +92,7 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() {
window_class.cbClsExtra = 0; window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0; window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr); window_class.hInstance = GetModuleHandle(nullptr);
window_class.hIcon = window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
window_class.hbrBackground = 0; window_class.hbrBackground = 0;
window_class.lpszMenuName = nullptr; window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = Win32Window::WndProc; window_class.lpfnWndProc = Win32Window::WndProc;
@@ -111,34 +107,27 @@ void WindowClassRegistrar::UnregisterWindowClass() {
class_registered_ = false; class_registered_ = false;
} }
Win32Window::Win32Window() { Win32Window::Win32Window() { ++g_active_window_count; }
++g_active_window_count;
}
Win32Window::~Win32Window() { Win32Window::~Win32Window() {
--g_active_window_count; --g_active_window_count;
Destroy(); Destroy();
} }
bool Win32Window::Create(const std::wstring& title, bool Win32Window::Create(const std::wstring& title, const Point& origin, const Size& size) {
const Point& origin,
const Size& size) {
Destroy(); Destroy();
const wchar_t* window_class = const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass();
WindowClassRegistrar::GetInstance()->GetWindowClass();
const POINT target_point = {static_cast<LONG>(origin.x), const POINT target_point = {static_cast<LONG>(origin.x), static_cast<LONG>(origin.y)};
static_cast<LONG>(origin.y)};
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0; double scale_factor = dpi / 96.0;
HWND window = CreateWindow( HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW, window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr),
Scale(size.width, scale_factor), Scale(size.height, scale_factor), this);
nullptr, nullptr, GetModuleHandle(nullptr), this);
if (!window) { if (!window) {
return false; return false;
@@ -149,19 +138,14 @@ bool Win32Window::Create(const std::wstring& title,
return OnCreate(); return OnCreate();
} }
bool Win32Window::Show() { bool Win32Window::Show() { return ShowWindow(window_handle_, SW_SHOWNORMAL); }
return ShowWindow(window_handle_, SW_SHOWNORMAL);
}
// static // static
LRESULT CALLBACK Win32Window::WndProc(HWND const window, LRESULT CALLBACK
UINT const message, Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept {
WPARAM const wparam,
LPARAM const lparam) noexcept {
if (message == WM_NCCREATE) { if (message == WM_NCCREATE) {
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam); auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA, SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams); auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window); EnableFullDpiSupportIfAvailable(window);
@@ -174,10 +158,7 @@ LRESULT CALLBACK Win32Window::WndProc(HWND const window,
} }
LRESULT LRESULT
Win32Window::MessageHandler(HWND hwnd, Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept {
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
switch (message) { switch (message) {
case WM_DESTROY: case WM_DESTROY:
window_handle_ = nullptr; window_handle_ = nullptr;
@@ -192,8 +173,8 @@ Win32Window::MessageHandler(HWND hwnd,
LONG newWidth = newRectSize->right - newRectSize->left; LONG newWidth = newRectSize->right - newRectSize->left;
LONG newHeight = newRectSize->bottom - newRectSize->top; LONG newHeight = newRectSize->bottom - newRectSize->top;
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, SetWindowPos(
newHeight, SWP_NOZORDER | SWP_NOACTIVATE); hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
return 0; return 0;
} }
@@ -201,8 +182,7 @@ Win32Window::MessageHandler(HWND hwnd,
RECT rect = GetClientArea(); RECT rect = GetClientArea();
if (child_content_ != nullptr) { if (child_content_ != nullptr) {
// Size and position the child window. // Size and position the child window.
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE);
rect.bottom - rect.top, TRUE);
} }
return 0; return 0;
} }
@@ -234,8 +214,7 @@ void Win32Window::Destroy() {
} }
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
return reinterpret_cast<Win32Window*>( return reinterpret_cast<Win32Window*>(GetWindowLongPtr(window, GWLP_USERDATA));
GetWindowLongPtr(window, GWLP_USERDATA));
} }
void Win32Window::SetChildContent(HWND content) { void Win32Window::SetChildContent(HWND content) {
@@ -243,8 +222,7 @@ void Win32Window::SetChildContent(HWND content) {
SetParent(content, window_handle_); SetParent(content, window_handle_);
RECT frame = GetClientArea(); RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left, MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true);
frame.bottom - frame.top, true);
SetFocus(child_content_); SetFocus(child_content_);
} }
@@ -255,13 +233,9 @@ RECT Win32Window::GetClientArea() {
return frame; return frame;
} }
HWND Win32Window::GetHandle() { HWND Win32Window::GetHandle() { return window_handle_; }
return window_handle_;
}
void Win32Window::SetQuitOnClose(bool quit_on_close) { void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; }
quit_on_close_ = quit_on_close;
}
bool Win32Window::OnCreate() { bool Win32Window::OnCreate() {
// No-op; provided for subclasses. // No-op; provided for subclasses.
@@ -275,14 +249,12 @@ void Win32Window::OnDestroy() {
void Win32Window::UpdateTheme(HWND const window) { void Win32Window::UpdateTheme(HWND const window) {
DWORD light_mode; DWORD light_mode;
DWORD light_mode_size = sizeof(light_mode); DWORD light_mode_size = sizeof(light_mode);
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, LSTATUS result = RegGetValue(
kGetPreferredBrightnessRegValue, HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr,
RRF_RT_REG_DWORD, nullptr, &light_mode, &light_mode, &light_mode_size);
&light_mode_size);
if (result == ERROR_SUCCESS) { if (result == ERROR_SUCCESS) {
BOOL enable_dark_mode = light_mode == 0; BOOL enable_dark_mode = light_mode == 0;
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, &enable_dark_mode, sizeof(enable_dark_mode));
&enable_dark_mode, sizeof(enable_dark_mode));
} }
} }
+4 -10
View File
@@ -21,8 +21,7 @@ class Win32Window {
struct Size { struct Size {
unsigned int width; unsigned int width;
unsigned int height; unsigned int height;
Size(unsigned int width, unsigned int height) Size(unsigned int width, unsigned int height) : width(width), height(height) {}
: width(width), height(height) {}
}; };
Win32Window(); Win32Window();
@@ -59,10 +58,7 @@ class Win32Window {
// Processes and route salient window messages for mouse handling, // Processes and route salient window messages for mouse handling,
// size change and DPI. Delegates handling of these to member overloads that // size change and DPI. Delegates handling of these to member overloads that
// inheriting classes can handle. // inheriting classes can handle.
virtual LRESULT MessageHandler(HWND window, virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept;
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Called when CreateAndShow is called, allowing subclass window-related // Called when CreateAndShow is called, allowing subclass window-related
// setup. Subclasses should return false if setup fails. // setup. Subclasses should return false if setup fails.
@@ -79,10 +75,8 @@ class Win32Window {
// non-client DPI scaling so that the non-client area automatically // non-client DPI scaling so that the non-client area automatically
// responds to changes in DPI. All other messages are handled by // responds to changes in DPI. All other messages are handled by
// MessageHandler. // MessageHandler.
static LRESULT CALLBACK WndProc(HWND const window, static LRESULT CALLBACK
UINT const message, WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept;
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Retrieves a class instance pointer for |window| // Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept; static Win32Window* GetThisFromHandle(HWND const window) noexcept;