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