Vendor media_kit_video 2.0.1 with the X11/EGL fix (hardware rendering on X11 with Flutter >= 3.38)
ci / core (push) Successful in 28s
ci / server (push) Successful in 51s
ci / updater-windows (push) Failing after 5m59s

Flutter >= 3.38 keeps its EGL contexts on the raster thread, so
media_kit's video output found none on the platform thread and fell
back to software rendering (no shaders, ~5 fps) on X11. The patched
output opens Flutter's EGL display itself, steps GDK's GLX context aside
on X11 and releases its own context. Patch from a friend's agent, kept
in third_party/media_kit_video/starsync/. Wayland/NVIDIA still renders
on the GPU (checked); the X11 path was verified on the friend's machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-16 01:13:14 +03:00
1 parent 25b19f2d6a
commit a87ee98faa
131 files changed
+15532 -6

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
group 'com.alexmercerind.media_kit_video'
version '1.0'
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
android {
namespace 'com.alexmercerind.media_kit_video'
compileSdkVersion 36
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 16
consumerProguardFiles 'proguard-rules.pro'
}
}
@@ -0,0 +1,3 @@
-keepclassmembers class io.flutter.embedding.engine.FlutterEngine {
private io.flutter.embedding.engine.FlutterJNI flutterJNI;
}
+1
View File
@@ -0,0 +1 @@
rootProject.name = 'media_kit_video'
@@ -0,0 +1 @@
<manifest package="com.alexmercerind.media_kit_video"></manifest>
@@ -0,0 +1,84 @@
/**
* This file is a part of media_kit (https://github.com/media-kit/media-kit).
* <p>
* Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
* All rights reserved.
* Use of this source code is governed by MIT license that can be found in the LICENSE file.
*/
package com.alexmercerind.media_kit_video;
import androidx.annotation.NonNull;
import java.util.HashMap;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
/**
* MediaKitVideoPlugin
*/
public class MediaKitVideoPlugin implements FlutterPlugin, MethodCallHandler {
private MethodChannel channel;
private VideoOutputManager videoOutputManager;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "com.alexmercerind/media_kit_video");
channel.setMethodCallHandler(this);
videoOutputManager = new VideoOutputManager(flutterPluginBinding.getTextureRegistry());
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "VideoOutputManager.Create": {
final long handle = Long.parseLong(call.argument("handle"));
videoOutputManager.create(handle, (id, wid, width, height) -> channel.invokeMethod("VideoOutput.Resize", new HashMap<String, Object>() {{
put("handle", handle);
put("id", id);
put("wid", wid);
put("rect", new HashMap<String, Object>() {{
put("left", 0);
put("top", 0);
put("width", width);
put("height", height);
}});
}}));
result.success(null);
break;
}
case "VideoOutputManager.SetSurfaceSize": {
final long handle = Long.parseLong(call.argument("handle"));
final int width = Integer.parseInt(call.argument("width"));
final int height = Integer.parseInt(call.argument("height"));
videoOutputManager.setSurfaceSize(handle, width, height);
result.success(null);
break;
}
case "VideoOutputManager.Dispose": {
final long handle = Long.parseLong(call.argument("handle"));
videoOutputManager.dispose(handle);
result.success(null);
break;
}
case "Utils.IsEmulator": {
result.success(Utils.isEmulator());
break;
}
default: {
result.notImplemented();
break;
}
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
}
@@ -0,0 +1,5 @@
package com.alexmercerind.media_kit_video;
public interface TextureUpdateCallback {
void onTextureUpdate(long id, long wid, int width, int height);
}
@@ -0,0 +1,42 @@
/**
* This file is a part of media_kit (https://github.com/media-kit/media-kit).
* <p>
* Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
* All rights reserved.
* Use of this source code is governed by MIT license that can be found in the LICENSE file.
*/
package com.alexmercerind.media_kit_video;
import android.os.Build;
import io.flutter.Log;
public abstract class Utils {
private static final String TAG = "Utils";
public static boolean isEmulator() {
try {
// https://github.com/fluttercommunity/plus_plugins/blob/ff54dc49230ee5f8b772a3326d4ff3758618df80/packages/device_info_plus/device_info_plus/android/src/main/kotlin/dev/fluttercommunity/plus/device_info/MethodCallHandlerImpl.kt#L110-L125
return Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
|| Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.HARDWARE.contains("goldfish")
|| Build.HARDWARE.contains("ranchu")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| Build.PRODUCT.contains("sdk_google")
|| Build.PRODUCT.contains("google_sdk")
|| Build.PRODUCT.contains("sdk")
|| Build.PRODUCT.contains("sdk_x86")
|| Build.PRODUCT.contains("vbox86p")
|| Build.PRODUCT.contains("emulator")
|| Build.PRODUCT.contains("simulator");
} catch (Throwable e) {
Log.e(TAG, "isEmulator", e);
}
return false;
}
}
@@ -0,0 +1,141 @@
/**
* This file is a part of media_kit (https://github.com/media-kit/media-kit).
* <p>
* Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
* All rights reserved.
* Use of this source code is governed by MIT license that can be found in the LICENSE file.
*/
package com.alexmercerind.media_kit_video;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects;
import io.flutter.view.TextureRegistry;
public class VideoOutput implements TextureRegistry.SurfaceProducer.Callback {
private static final String TAG = "VideoOutput";
private static final Method newGlobalObjectRef;
private static final Method deleteGlobalObjectRef;
private static final HashSet<Long> deletedGlobalObjectRefs = new HashSet<>();
private static final Handler handler = new Handler(Looper.getMainLooper());
static {
try {
// com.alexmercerind.mediakitandroidhelper.MediaKitAndroidHelper is part of package:media_kit_libs_android_video & package:media_kit_libs_android_audio packages.
// Use reflection to invoke methods of com.alexmercerind.mediakitandroidhelper.MediaKitAndroidHelper.
Class<?> mediaKitAndroidHelperClass = Class.forName("com.alexmercerind.mediakitandroidhelper.MediaKitAndroidHelper");
newGlobalObjectRef = mediaKitAndroidHelperClass.getDeclaredMethod("newGlobalObjectRef", Object.class);
deleteGlobalObjectRef = mediaKitAndroidHelperClass.getDeclaredMethod("deleteGlobalObjectRef", long.class);
newGlobalObjectRef.setAccessible(true);
deleteGlobalObjectRef.setAccessible(true);
} catch (Throwable e) {
Log.i("media_kit", "package:media_kit_libs_android_video missing. Make sure you have added it to pubspec.yaml.");
throw new RuntimeException("Failed to initialize com.alexmercerind.media_kit_video.VideoOutput.");
}
}
private long id = 0;
private long wid = 0;
private final TextureUpdateCallback textureUpdateCallback;
private final TextureRegistry.SurfaceProducer surfaceProducer;
private final Object lock = new Object();
VideoOutput(TextureRegistry textureRegistryReference, TextureUpdateCallback textureUpdateCallback) {
this.textureUpdateCallback = textureUpdateCallback;
surfaceProducer = textureRegistryReference.createSurfaceProducer();
surfaceProducer.setCallback(this);
}
public void dispose() {
synchronized (lock) {
try {
surfaceProducer.getSurface().release();
} catch (Throwable e) {
Log.e(TAG, "dispose", e);
}
try {
surfaceProducer.release();
} catch (Throwable e) {
Log.e(TAG, "dispose", e);
}
onSurfaceCleanup();
}
}
public void setSurfaceSize(int width, int height) {
setSurfaceSize(width, height, false);
}
private void setSurfaceSize(int width, int height, boolean force) {
synchronized (lock) {
try {
if (!force && surfaceProducer.getWidth() == width && surfaceProducer.getHeight() == height) {
return;
}
surfaceProducer.setSize(width, height);
onSurfaceAvailable();
} catch (Throwable e) {
Log.e(TAG, "setSurfaceSize", e);
}
}
}
@Override
public void onSurfaceAvailable() {
synchronized (lock) {
Log.i(TAG, "onSurfaceAvailable");
id = surfaceProducer.id();
wid = newGlobalObjectRef(surfaceProducer.getSurface());
textureUpdateCallback.onTextureUpdate(id, wid, surfaceProducer.getWidth(), surfaceProducer.getHeight());
}
}
@Override
public void onSurfaceCleanup() {
synchronized (lock) {
Log.i(TAG, "onSurfaceCleanup");
textureUpdateCallback.onTextureUpdate(id, 0, surfaceProducer.getWidth(), surfaceProducer.getHeight());
if (wid != 0) {
final long widReference = wid;
handler.postDelayed(() -> deleteGlobalObjectRef(widReference), 5000);
}
}
}
private static long newGlobalObjectRef(Object object) {
Log.i(TAG, String.format(Locale.ENGLISH, "newGlobalRef: object = %s", object));
try {
return (long) Objects.requireNonNull(newGlobalObjectRef.invoke(null, object));
} catch (Throwable e) {
Log.e(TAG, "newGlobalRef", e);
return 0;
}
}
private static void deleteGlobalObjectRef(long ref) {
if (deletedGlobalObjectRefs.contains(ref)) {
Log.i(TAG, String.format(Locale.ENGLISH, "deleteGlobalObjectRef: ref = %d ALREADY DELETED", ref));
return;
}
if (deletedGlobalObjectRefs.size() > 100) {
deletedGlobalObjectRefs.clear();
}
deletedGlobalObjectRefs.add(ref);
Log.i(TAG, String.format(Locale.ENGLISH, "deleteGlobalObjectRef: ref = %d", ref));
try {
deleteGlobalObjectRef.invoke(null, ref);
} catch (Throwable e) {
Log.e(TAG, "deleteGlobalObjectRef", e);
}
}
}
@@ -0,0 +1,57 @@
/**
* This file is a part of media_kit (https://github.com/media-kit/media-kit).
* <p>
* Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
* All rights reserved.
* Use of this source code is governed by MIT license that can be found in the LICENSE file.
*/
package com.alexmercerind.media_kit_video;
import android.util.Log;
import java.util.HashMap;
import java.util.Locale;
import java.util.Objects;
import io.flutter.view.TextureRegistry;
public class VideoOutputManager {
private static final String TAG = "VideoOutputManager";
private final HashMap<Long, VideoOutput> videoOutputs = new HashMap<>();
private final TextureRegistry textureRegistryReference;
private final Object lock = new Object();
VideoOutputManager(TextureRegistry textureRegistryReference) {
this.textureRegistryReference = textureRegistryReference;
}
public void create(long handle, TextureUpdateCallback textureUpdateCallback) {
synchronized (lock) {
Log.i(TAG, String.format(Locale.ENGLISH, "com.alexmercerind.media_kit_video.VideoOutputManager.create: %d", handle));
if (!videoOutputs.containsKey(handle)) {
final VideoOutput videoOutput = new VideoOutput(textureRegistryReference, textureUpdateCallback);
videoOutputs.put(handle, videoOutput);
}
}
}
public void dispose(long handle) {
synchronized (lock) {
Log.i(TAG, String.format(Locale.ENGLISH, "com.alexmercerind.media_kit_video.VideoOutputManager.dispose: %d", handle));
if (videoOutputs.containsKey(handle)) {
Objects.requireNonNull(videoOutputs.get(handle)).dispose();
videoOutputs.remove(handle);
}
}
}
public void setSurfaceSize(long handle, int width, int height) {
synchronized (lock) {
Log.i(TAG, String.format(Locale.ENGLISH, "com.alexmercerind.media_kit_video.VideoOutputManager.setSurfaceSize: %d %d %d", handle, width, height));
if (videoOutputs.containsKey(handle)) {
Objects.requireNonNull(videoOutputs.get(handle)).setSurfaceSize(width, height);
}
}
}
}