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