From 9587518f763a423270cccd1318975d0d56e23c47 Mon Sep 17 00:00:00 2001 From: Chenx Dust Date: Sat, 22 Nov 2025 22:17:55 +0800 Subject: [PATCH 1/5] fix: better implementation --- media_kit_test/lib/main.dart | 14 +- .../GlobalObjectRefManager.java | 82 +++++++++++ .../media_kit_video/MediaKitVideoPlugin.java | 9 ++ .../media_kit_video/VideoOutput.java | 56 +------ .../platformview/PlatformVideoView.java | 138 ++++++++++++++++++ .../PlatformVideoViewFactory.java | 69 +++++++++ .../lib/src/video/platform_view_video.dart | 66 +++++++++ .../lib/src/video/video_texture.dart | 23 ++- .../android_video_controller/real.dart | 53 +++++-- .../platform_video_controller.dart | 11 ++ 10 files changed, 448 insertions(+), 73 deletions(-) create mode 100644 media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/GlobalObjectRefManager.java create mode 100644 media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/platformview/PlatformVideoView.java create mode 100644 media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/platformview/PlatformVideoViewFactory.java create mode 100644 media_kit_video/lib/src/video/platform_view_video.dart diff --git a/media_kit_test/lib/main.dart b/media_kit_test/lib/main.dart index 3f24a173c..e2c252af0 100644 --- a/media_kit_test/lib/main.dart +++ b/media_kit_test/lib/main.dart @@ -68,13 +68,25 @@ class PrimaryScreen extends StatelessWidget { valueListenable: configuration, builder: (context, value, _) => TextButton( onPressed: () { - configuration.value = VideoControllerConfiguration( + configuration.value = value.copyWith( enableHardwareAcceleration: !value.enableHardwareAcceleration, ); }, child: Text(value.enableHardwareAcceleration ? 'H/W' : 'S/W'), ), ), + if (UniversalPlatform.isAndroid) + ValueListenableBuilder( + valueListenable: configuration, + builder: (context, value, _) => TextButton( + onPressed: () { + configuration.value = value.copyWith( + usePlatformView: !value.usePlatformView, + ); + }, + child: Text(value.usePlatformView ? 'PlatformView' : 'TextureView'), + ), + ), const SizedBox(width: 16.0), ], ), diff --git a/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/GlobalObjectRefManager.java b/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/GlobalObjectRefManager.java new file mode 100644 index 000000000..3eeec7422 --- /dev/null +++ b/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/GlobalObjectRefManager.java @@ -0,0 +1,82 @@ +/** + * This file is a part of media_kit (https://github.com/media-kit/media-kit). + *

+ * Copyright © 2021 & onwards, Hitesh Kumar Saini . + * 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.lang.reflect.Method; +import java.util.HashSet; +import java.util.Locale; +import java.util.Objects; + +/** + * Manages global object references through JNI. + * This class provides functionality to create and delete global references to Java objects + * for use in native code. + */ +public class GlobalObjectRefManager { + private static final String TAG = "GlobalObjectRefManager"; + private static final Method newGlobalObjectRefMethod; + private static final Method deleteGlobalObjectRefMethod; + private static final HashSet deletedGlobalObjectRefs = new HashSet<>(); + + 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"); + newGlobalObjectRefMethod = mediaKitAndroidHelperClass.getDeclaredMethod("newGlobalObjectRef", Object.class); + deleteGlobalObjectRefMethod = mediaKitAndroidHelperClass.getDeclaredMethod("deleteGlobalObjectRef", long.class); + newGlobalObjectRefMethod.setAccessible(true); + deleteGlobalObjectRefMethod.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.GlobalObjectRefManager.", e); + } + } + + /** + * Creates a new global reference to the given object. + * + * @param object The object to create a global reference for. + * @return The global reference ID, or 0 if creation failed. + */ + public static long newGlobalObjectRef(Object object) { + Log.i(TAG, String.format(Locale.ENGLISH, "newGlobalRef: object = %s", object)); + try { + return (long) Objects.requireNonNull(newGlobalObjectRefMethod.invoke(null, object)); + } catch (Throwable e) { + Log.e(TAG, "newGlobalRef", e); + return 0; + } + } + + /** + * Deletes a global reference by its ID. + * This method tracks deleted references to prevent double deletion. + * + * @param ref The global reference ID to delete. + */ + public 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 { + deleteGlobalObjectRefMethod.invoke(null, ref); + } catch (Throwable e) { + Log.e(TAG, "deleteGlobalObjectRef", e); + } + } +} + diff --git a/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/MediaKitVideoPlugin.java b/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/MediaKitVideoPlugin.java index b9277fe3c..96f01bec0 100644 --- a/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/MediaKitVideoPlugin.java +++ b/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/MediaKitVideoPlugin.java @@ -16,6 +16,9 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.platform.PlatformViewRegistry; + +import com.alexmercerind.media_kit_video.platformview.PlatformVideoViewFactory; /** * MediaKitVideoPlugin @@ -31,6 +34,12 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBindin videoOutputManager = new VideoOutputManager(flutterPluginBinding.getTextureRegistry()); + // Register PlatformViewFactory for PlatformView support + PlatformViewRegistry registry = flutterPluginBinding.getPlatformViewRegistry(); + registry.registerViewFactory( + "com.alexmercerind/media_kit_video_platform_view", + new PlatformVideoViewFactory(channel) + ); } @Override diff --git a/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/VideoOutput.java b/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/VideoOutput.java index cc645f086..9996a2ba3 100644 --- a/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/VideoOutput.java +++ b/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/VideoOutput.java @@ -11,35 +11,14 @@ 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 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; @@ -93,9 +72,9 @@ private void setSurfaceSize(int width, int height, boolean force) { @Override public void onSurfaceAvailable() { synchronized (lock) { - Log.i(TAG, "onSurfaceAvailable"); + Log.i(TAG, "onSurfaceAvailable: id=" + id + ", wid=" + wid + ", width=" + surfaceProducer.getWidth() + ", height=" + surfaceProducer.getHeight()); id = surfaceProducer.id(); - wid = newGlobalObjectRef(surfaceProducer.getSurface()); + wid = GlobalObjectRefManager.newGlobalObjectRef(surfaceProducer.getSurface()); textureUpdateCallback.onTextureUpdate(id, wid, surfaceProducer.getWidth(), surfaceProducer.getHeight()); } } @@ -103,39 +82,12 @@ public void onSurfaceAvailable() { @Override public void onSurfaceCleanup() { synchronized (lock) { - Log.i(TAG, "onSurfaceCleanup"); + Log.i(TAG, "onSurfaceCleanup: id=" + id + ", wid=" + wid + ", width=" + surfaceProducer.getWidth() + ", height=" + surfaceProducer.getHeight()); textureUpdateCallback.onTextureUpdate(id, 0, surfaceProducer.getWidth(), surfaceProducer.getHeight()); if (wid != 0) { final long widReference = wid; - handler.postDelayed(() -> deleteGlobalObjectRef(widReference), 5000); + handler.postDelayed(() -> GlobalObjectRefManager.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); - } - } } diff --git a/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/platformview/PlatformVideoView.java b/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/platformview/PlatformVideoView.java new file mode 100644 index 000000000..eff6d754f --- /dev/null +++ b/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/platformview/PlatformVideoView.java @@ -0,0 +1,138 @@ +/** + * This file is a part of media_kit (https://github.com/media-kit/media-kit). + *

+ * Copyright © 2021 & onwards, Hitesh Kumar Saini . + * 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.platformview; + +import java.util.function.Consumer; + +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; + +import androidx.annotation.NonNull; + +import io.flutter.plugin.platform.PlatformView; + +import com.alexmercerind.media_kit_video.GlobalObjectRefManager; +/** + * A class used to create a native video view that can be embedded in a Flutter app. + * It wraps a SurfaceView and connects it to libmpv. + */ +public final class PlatformVideoView implements PlatformView { + private static final String TAG = "PlatformVideoView"; + private static final Handler handler = new Handler(Looper.getMainLooper()); + @NonNull + private final SurfaceView surfaceView; + private final long handle; + private final int width; + private final int height; + private long wid = 0; + private final Consumer onSurfaceAvailable; + + /** + * Constructs a new PlatformVideoView. + * + * @param context The context in which the view is running. + * @param handle The handle (player ID) of the video player. + * @param width The width of the video. + * @param height The height of the video. + * @param onSurfaceAvailable The callback to be called when the Surface is available. + */ + public PlatformVideoView( + @NonNull Context context, + long handle, + int width, + int height, + @NonNull Consumer onSurfaceAvailable) { + this.handle = handle; + this.width = width; + this.height = height; + this.onSurfaceAvailable = onSurfaceAvailable; + this.surfaceView = new SurfaceView(context); + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) { + // Avoid blank space instead of a video on Android versions below 8 by adjusting video's + // z-layer within the Android view hierarchy: + surfaceView.setZOrderMediaOverlay(true); + } + + setupSurface(); + } + + private void setupSurface() { + surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(@NonNull SurfaceHolder holder) { + Log.i(TAG, "surfaceCreated: handle=" + handle + ", width=" + width + ", height=" + height); + if (holder.getSurface() != null) { + // Clean up old wid if it exists + if (wid != 0) { + Log.i(TAG, "surfaceCreated: cleaning up old wid=" + wid); + GlobalObjectRefManager.deleteGlobalObjectRef(wid); + wid = 0; + } + // Get global reference to the Surface only once when it's first created + wid = GlobalObjectRefManager.newGlobalObjectRef(holder.getSurface()); + Log.i(TAG, "surfaceCreated: created new wid=" + wid); + holder.setFixedSize(width, height); + // Notify Dart side about the PlatformView Surface availability + // Use 0x0 for size initially, actual video size will be set later via SetSurfaceSize + // videoOutputManager.notifyPlatformViewSurfaceAvailable(handle, wid, width, height); + onSurfaceAvailable.accept(wid); + } + } + + @Override + public void surfaceChanged( + @NonNull SurfaceHolder holder, int format, int width, int height) { + Log.i(TAG, String.format("surfaceChanged: handle=%d, width=%d, height=%d, wid=%d", handle, width, height, wid)); + // videoOutputManager.notifyPlatformViewSurfaceAvailable(handle, wid, width, height); + } + + @Override + public void surfaceDestroyed(@NonNull SurfaceHolder holder) { + Log.i(TAG, "surfaceDestroyed: handle=" + handle + ", wid=" + wid); + // videoOutputManager.notifyPlatformViewSurfaceAvailable(handle, 0, width, height); + if (wid != 0) { + final long widReference = wid; + handler.postDelayed(() -> GlobalObjectRefManager.deleteGlobalObjectRef(widReference), 5000); + wid = 0; + } + } + }); + } + + /** + * Returns the view associated with this PlatformView. + * + * @return The SurfaceView used to display the video. + */ + @NonNull + @Override + public View getView() { + return surfaceView; + } + + /** Disposes of the resources used by this PlatformView. */ + @Override + public void dispose() { + Log.i(TAG, "dispose: handle=" + handle); + if (wid != 0) { + GlobalObjectRefManager.deleteGlobalObjectRef(wid); + wid = 0; + } + if (surfaceView.getHolder().getSurface() != null) { + surfaceView.getHolder().getSurface().release(); + } + } +} + diff --git a/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/platformview/PlatformVideoViewFactory.java b/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/platformview/PlatformVideoViewFactory.java new file mode 100644 index 000000000..8bc4359e9 --- /dev/null +++ b/media_kit_video/android/src/main/java/com/alexmercerind/media_kit_video/platformview/PlatformVideoViewFactory.java @@ -0,0 +1,69 @@ +/** + * This file is a part of media_kit (https://github.com/media-kit/media-kit). + *

+ * Copyright © 2021 & onwards, Hitesh Kumar Saini . + * 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.platformview; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; +import io.flutter.plugin.common.StandardMessageCodec; +import io.flutter.plugin.common.MethodChannel; + +import android.util.Log; +import java.util.Objects; +import java.util.HashMap; +import java.util.function.Consumer; + +/** + * A factory class responsible for creating platform video views that can be embedded in a Flutter + * app. + */ +public class PlatformVideoViewFactory extends PlatformViewFactory { + private static final String TAG = "PlatformVideoViewFactory"; + private final MethodChannel channel; + + /** + * Constructs a new PlatformVideoViewFactory. + * + * @param channel The MethodChannel used to communicate with Flutter side. + */ + public PlatformVideoViewFactory(@NonNull MethodChannel channel) { + super(StandardMessageCodec.INSTANCE); + this.channel = channel; + } + + /** + * Creates a new instance of platform view. + * + * @param context The context in which the view is running. + * @param id The unique identifier for the view. + * @param args The arguments for creating the view. + * @return A new instance of PlatformVideoView. + */ + @NonNull + @Override + public PlatformView create(@NonNull Context context, int id, @Nullable Object args) { + @SuppressWarnings("unchecked") + final java.util.Map params = (java.util.Map) Objects.requireNonNull(args); + final long handle = ((Number) Objects.requireNonNull(params.get("handle"))).longValue(); + final int width = ((Number) Objects.requireNonNull(params.get("width"))).intValue(); + final int height = ((Number) Objects.requireNonNull(params.get("height"))).intValue(); + + Log.i(TAG, "Creating PlatformVideoView for handle: " + handle); + final long finalHandle = handle; + return new PlatformVideoView(context, handle, width, height, + (wid) -> channel.invokeMethod("PlatformVideoView.SurfaceAvailable", new HashMap() {{ + put("handle", finalHandle); + put("wid", wid); + }}) + ); + } +} + diff --git a/media_kit_video/lib/src/video/platform_view_video.dart b/media_kit_video/lib/src/video/platform_view_video.dart new file mode 100644 index 000000000..f1d6ebb0a --- /dev/null +++ b/media_kit_video/lib/src/video/platform_view_video.dart @@ -0,0 +1,66 @@ +/// This file is a part of media_kit (https://github.com/media-kit/media-kit). +/// +/// Copyright © 2021 & onwards, Hitesh Kumar Saini . +/// All rights reserved. +/// Use of this source code is governed by MIT license that can be found in the LICENSE file. +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// A widget that displays a video player using a platform view on Android. +class PlatformViewVideo extends StatelessWidget { + /// Creates a new instance of [PlatformViewVideo]. + const PlatformViewVideo({ + super.key, + required this.handle, + required this.width, + required this.height, + }); + + /// The handle (player ID) of the video player. + final int handle; + final int width; + final int height; + + @override + Widget build(BuildContext context) { + const String viewType = 'com.alexmercerind/media_kit_video_platform_view'; + final Map creationParams = { + 'handle': handle, + 'width': width, + 'height': height, + }; + + // IgnorePointer so that GestureDetector can be used above the platform view. + return IgnorePointer( + child: PlatformViewLink( + viewType: viewType, + surfaceFactory: + (BuildContext context, PlatformViewController controller) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: viewType, + layoutDirection: + Directionality.maybeOf(context) ?? TextDirection.ltr, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onFocus: () => params.onFocusChanged(true), + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); + }, + ), + ); + } +} + diff --git a/media_kit_video/lib/src/video/video_texture.dart b/media_kit_video/lib/src/video/video_texture.dart index d5ed54dde..715d0081b 100644 --- a/media_kit_video/lib/src/video/video_texture.dart +++ b/media_kit_video/lib/src/video/video_texture.dart @@ -18,6 +18,7 @@ import 'package:media_kit_video/src/utils/wakelock.dart'; import 'package:media_kit_video/src/video_view_parameters.dart'; import 'package:media_kit_video/src/video_controller/video_controller.dart'; import 'package:media_kit_video/src/video_controller/platform_video_controller.dart'; +import 'package:media_kit_video/src/video/platform_view_video.dart'; /// {@template video} /// @@ -407,12 +408,22 @@ class VideoState extends State