From f50c4e4a89d64090fc6dac25e9999392abbf4327 Mon Sep 17 00:00:00 2001 From: Littlegnal <8847263+littleGnAl@users.noreply.github.com> Date: Tue, 25 Jul 2023 17:16:03 +0800 Subject: [PATCH] fix: [android/ios] Fix crash due to AgoraVideoView.dispose call before RtcEngine.setupxxVideo is completed (#1224) fix: [android/ios] Fix crash due to AgoraVideoView.dispose call before RtcEngine.setupxxVideo is completed (#1224) --- .../AgoraPlatformViewFactory.java | 46 +++-- .../agora/agora_rtc_ng/AgoraRtcNgPlugin.java | 9 +- .../agora_rtc_ng/VideoViewController.java | 116 ++++++++++++- ios/Classes/AgoraRtcNgPlugin.m | 9 +- ios/Classes/AgoraSurfaceViewFactory.h | 4 +- ios/Classes/AgoraSurfaceViewFactory.mm | 34 +++- lib/src/impl/agora_rtc_engine_impl.dart | 4 +- lib/src/impl/agora_video_view_impl.dart | 13 +- .../impl/global_video_view_controller.dart | 8 + lib/src/impl/video_view_controller_impl.dart | 13 +- shared/darwin/VideoViewController.h | 8 +- shared/darwin/VideoViewController.mm | 162 ++++++++++++++++-- .../fake/fake_iris_method_channel.dart | 101 ++++++++++- .../fake_agora_video_view_testcases.dart | 62 ++++++- 14 files changed, 526 insertions(+), 63 deletions(-) diff --git a/android/src/main/java/io/agora/agora_rtc_ng/AgoraPlatformViewFactory.java b/android/src/main/java/io/agora/agora_rtc_ng/AgoraPlatformViewFactory.java index a2454a3b3..805b60e0a 100644 --- a/android/src/main/java/io/agora/agora_rtc_ng/AgoraPlatformViewFactory.java +++ b/android/src/main/java/io/agora/agora_rtc_ng/AgoraPlatformViewFactory.java @@ -1,7 +1,6 @@ package io.agora.agora_rtc_ng; import android.content.Context; -import android.view.SurfaceView; import android.view.TextureView; import android.view.View; import android.widget.FrameLayout; @@ -9,7 +8,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import io.agora.iris.IrisApiEngine; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -23,14 +21,18 @@ public class AgoraPlatformViewFactory extends PlatformViewFactory { private final BinaryMessenger messenger; private final PlatformViewProvider viewProvider; + private final VideoViewController controller; + AgoraPlatformViewFactory( String viewType, BinaryMessenger messenger, - PlatformViewProvider viewProvider) { + PlatformViewProvider viewProvider, + VideoViewController controller) { super(StandardMessageCodec.INSTANCE); this.viewType = viewType; this.messenger = messenger; this.viewProvider = viewProvider; + this.controller = controller; } interface PlatformViewProvider { @@ -59,34 +61,41 @@ static class AgoraPlatformView implements PlatformView, MethodChannel.MethodCall private final MethodChannel methodChannel; + private final VideoViewController controller; + + private SimpleRef viewRef; - private long platformViewPtr; + private final int platformViewId; AgoraPlatformView(Context context, String viewType, int viewId, PlatformViewProvider viewProvider, - BinaryMessenger messenger) { + BinaryMessenger messenger, + VideoViewController controller) { methodChannel = new MethodChannel(messenger, "agora_rtc_ng/" + viewType + "_" + viewId); methodChannel.setMethodCallHandler(this); - innerView = viewProvider.provide(context); + this.controller = controller; + this.platformViewId = viewId; + this.viewRef = controller.createPlatformRender(viewId, context, viewProvider); + + innerView = (View) viewRef.getValue(); parentView = new FrameLayout(context); parentView.addView(innerView); - - platformViewPtr = IrisApiEngine.GetJObjectAddress(innerView); } @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { if (call.method.equals("getNativeViewPtr")) { + long platformViewPtr = 0L; + if (viewRef != null) { + this.controller.addPlatformRenderRef(this.platformViewId); + platformViewPtr = viewRef.getNativeHandle(); + } + result.success(platformViewPtr); } else if (call.method.equals("deleteNativeViewPtr")) { - if (platformViewPtr != 0L) { - IrisApiEngine.FreeJObjectByAddress(platformViewPtr); - platformViewPtr = 0; - } - parentView.removeAllViews(); - innerView = null; + // Do nothing. result.success(0); } } @@ -99,7 +108,11 @@ public View getView() { @Override public void dispose() { - + this.controller.dePlatformRenderRef(this.platformViewId); + viewRef = null; + parentView.removeAllViews(); + parentView = null; + innerView = null; } } @@ -111,7 +124,8 @@ public PlatformView create(@Nullable Context context, int viewId, @Nullable Obje viewType, viewId, viewProvider, - messenger + messenger, + this.controller ); } } diff --git a/android/src/main/java/io/agora/agora_rtc_ng/AgoraRtcNgPlugin.java b/android/src/main/java/io/agora/agora_rtc_ng/AgoraRtcNgPlugin.java index f9b3fac3e..d7d83b490 100644 --- a/android/src/main/java/io/agora/agora_rtc_ng/AgoraRtcNgPlugin.java +++ b/android/src/main/java/io/agora/agora_rtc_ng/AgoraRtcNgPlugin.java @@ -27,22 +27,25 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBindin channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "agora_rtc_ng"); channel.setMethodCallHandler(this); flutterPluginBindingRef = new WeakReference<>(flutterPluginBinding); + videoViewController = new VideoViewController(flutterPluginBinding.getBinaryMessenger()); flutterPluginBinding.getPlatformViewRegistry().registerViewFactory( "AgoraTextureView", new AgoraPlatformViewFactory( "AgoraTextureView", flutterPluginBinding.getBinaryMessenger(), - new AgoraPlatformViewFactory.PlatformViewProviderTextureView())); + new AgoraPlatformViewFactory.PlatformViewProviderTextureView(), + this.videoViewController)); flutterPluginBinding.getPlatformViewRegistry().registerViewFactory( "AgoraSurfaceView", new AgoraPlatformViewFactory( "AgoraSurfaceView", flutterPluginBinding.getBinaryMessenger(), - new AgoraPlatformViewFactory.PlatformViewProviderSurfaceView())); + new AgoraPlatformViewFactory.PlatformViewProviderSurfaceView(), + this.videoViewController)); + - videoViewController = new VideoViewController(flutterPluginBinding.getBinaryMessenger()); } @Override diff --git a/android/src/main/java/io/agora/agora_rtc_ng/VideoViewController.java b/android/src/main/java/io/agora/agora_rtc_ng/VideoViewController.java index 5e8f290f2..bc3e8a523 100644 --- a/android/src/main/java/io/agora/agora_rtc_ng/VideoViewController.java +++ b/android/src/main/java/io/agora/agora_rtc_ng/VideoViewController.java @@ -1,26 +1,129 @@ package io.agora.agora_rtc_ng; +import android.content.Context; +import android.view.View; + import androidx.annotation.NonNull; +import java.util.HashMap; +import java.util.Map; + +import io.agora.iris.IrisApiEngine; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; +class SimpleRef { + private Object value; + private int refCount; + private long nativeHandle; + + SimpleRef(Object value) { + this.value = value; + this.refCount = 1; + this.nativeHandle = IrisApiEngine.GetJObjectAddress(this.value); + } + + int getRefCount() { + return this.refCount; + } + + Object getValue() { + return value; + } + + long getNativeHandle() { + return this.nativeHandle; + } + + void addRef() { + ++this.refCount; + } + + void deRef() { + --this.refCount; + } + + void releaseRef() { + IrisApiEngine.FreeJObjectByAddress(this.nativeHandle); + this.nativeHandle = 0L; + this.value = null; + this.refCount = 0; + } +} + +class PlatformRenderPool { + + private final Map renders = new HashMap<>(); + SimpleRef createView(int platformViewId, + Context context, + AgoraPlatformViewFactory.PlatformViewProvider viewProvider) { + final View view = viewProvider.provide(context); + + final SimpleRef simpleRef = new SimpleRef(view); + renders.put(platformViewId, simpleRef); + + return simpleRef; + } + + boolean addViewRef(int platformViewId) { + if (renders.containsKey(platformViewId)) { + final SimpleRef simpleRef = renders.get(platformViewId); + + //noinspection ConstantConditions + simpleRef.addRef(); + return true; + } + return false; + } + + boolean deViewRef(int platformViewId) { + if (renders.containsKey(platformViewId)) { + final SimpleRef simpleRef = renders.get(platformViewId); + + //noinspection ConstantConditions + simpleRef.deRef(); + + if (simpleRef.getRefCount() <= 0) { + simpleRef.releaseRef(); + renders.remove(platformViewId); + } + + return true; + } + + return false; + } +} + public class VideoViewController implements MethodChannel.MethodCallHandler { private final MethodChannel methodChannel; + private final PlatformRenderPool pool; VideoViewController(BinaryMessenger binaryMessenger) { methodChannel = new MethodChannel(binaryMessenger, "agora_rtc_ng/video_view_controller"); methodChannel.setMethodCallHandler(this); + pool = new PlatformRenderPool(); } - private long createPlatformRender(){ - return 0L; + public SimpleRef createPlatformRender( + int platformViewId, + Context context, + AgoraPlatformViewFactory.PlatformViewProvider viewProvider) { + return this.pool.createView(platformViewId, context, viewProvider); } - private boolean destroyPlatformRender(long platformRenderId) { - return true; + public boolean destroyPlatformRender(int platformRenderId) { + return this.pool.deViewRef(platformRenderId); + } + + public boolean addPlatformRenderRef(int platformViewId) { + return this.pool.addViewRef(platformViewId); + } + + public boolean dePlatformRenderRef(int platformViewId) { + return this.pool.deViewRef(platformViewId); } private long createTextureRender() { @@ -40,6 +143,11 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result case "detachVideoFrameBufferManager": result.success(true); break; + case "dePlatfromViewRef": + int platformViewId = (int) call.arguments; + this.dePlatformRenderRef(platformViewId); + result.success(true); + break; case "createTextureRender": case "destroyTextureRender": diff --git a/ios/Classes/AgoraRtcNgPlugin.m b/ios/Classes/AgoraRtcNgPlugin.m index 953a159dd..cb4158996 100644 --- a/ios/Classes/AgoraRtcNgPlugin.m +++ b/ios/Classes/AgoraRtcNgPlugin.m @@ -19,11 +19,12 @@ + (void)registerWithRegistrar:(NSObject*)registrar { instance.registrar = registrar; [registrar addMethodCallDelegate:instance channel:channel]; - [registrar registerViewFactory:[[AgoraSurfaceViewFactory alloc] - initWith:[registrar messenger]] - withId:@"AgoraSurfaceView"]; + instance.videoViewController = [[VideoViewController alloc] initWith:registrar.textures messenger:registrar.messenger]; - instance.videoViewController = [[VideoViewController alloc] initWith:registrar.textures messenger:registrar.messenger]; + [registrar registerViewFactory:[[AgoraSurfaceViewFactory alloc] + initWith:[registrar messenger] + controller:instance.videoViewController] + withId:@"AgoraSurfaceView"]; } - (void)getAssetAbsolutePath:(FlutterMethodCall *)call diff --git a/ios/Classes/AgoraSurfaceViewFactory.h b/ios/Classes/AgoraSurfaceViewFactory.h index 5510a5dcf..85e0518e8 100644 --- a/ios/Classes/AgoraSurfaceViewFactory.h +++ b/ios/Classes/AgoraSurfaceViewFactory.h @@ -1,7 +1,9 @@ #import +#import "VideoViewController.h" @interface AgoraSurfaceViewFactory : NSObject -- (instancetype)initWith:(NSObject *)messenger; +- (instancetype)initWith:(NSObject *)messenger + controller:(VideoViewController *)controller; @end diff --git a/ios/Classes/AgoraSurfaceViewFactory.mm b/ios/Classes/AgoraSurfaceViewFactory.mm index 888efdff1..24f362099 100644 --- a/ios/Classes/AgoraSurfaceViewFactory.mm +++ b/ios/Classes/AgoraSurfaceViewFactory.mm @@ -2,13 +2,18 @@ @interface AgoraSurfaceView : NSObject +@property(nonatomic, strong) VideoViewController *controller; + @property(nonatomic, strong) UIView *surfaceView; @property(nonatomic, strong) FlutterMethodChannel *methodChannel; @property(nonatomic) NSString *viewType; +@property(nonatomic) int64_t platformViewId; + - (instancetype)initWith:(NSObject *)messenger + controller:(VideoViewController *)controller frame:(CGRect)frame viewId:(int64_t)viewId args:(NSDictionary *)args; @@ -18,12 +23,15 @@ - (instancetype)initWith:(NSObject *)messenger @implementation AgoraSurfaceView - (instancetype)initWith:(NSObject *)messenger + controller:(VideoViewController *)controller frame:(CGRect)frame viewId:(int64_t)viewId args:(NSDictionary *)args { if (self = [super init]) { - self.viewType = [args objectForKey:@"viewType"]; - self.surfaceView = [[UIView alloc] initWithFrame:frame]; + self.controller = controller; + self.viewType = [args objectForKey:@"viewType"]; + self.surfaceView = (UIView *)[self.controller createPlatformRender:viewId frame:frame]; + self.platformViewId = viewId; self.methodChannel = [FlutterMethodChannel methodChannelWithName: [NSString @@ -52,7 +60,8 @@ - (instancetype)initWith:(NSObject *)messenger } - (void)dealloc { -// [self.methodChannel setMethodCallHandler:nil]; + [self.controller dePlatformRenderRef:self.platformViewId]; + self.surfaceView = NULL; } - (nonnull UIView *)view { @@ -61,9 +70,20 @@ - (nonnull UIView *)view { - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if ([@"getNativeViewPtr" isEqualToString:call.method]) { - result(@((uint64_t)self.surfaceView)); + if (self.surfaceView) { + // Add ref to ensure the `self.surfaceView` not be released by ARC, which will be + // de-ref by the `VideoViewController.dePlatformRenderRef`. + [self.controller addPlatformRenderRef:self.platformViewId]; + uint64_t viewId = (uint64_t)self.surfaceView; + result(@(viewId)); + } else { + result(@(0)); + } + + } else if ([@"deleteNativeViewPtr" isEqualToString:call.method]) { // Do nothing + result(@(0)); } } @@ -72,14 +92,17 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { @interface AgoraSurfaceViewFactory () @property(nonatomic, strong) NSObject *messenger; +@property(nonatomic, strong) VideoViewController *controller; @end @implementation AgoraSurfaceViewFactory -- (instancetype)initWith:(NSObject *)messenger { +- (instancetype)initWith:(NSObject *)messenger + controller:(VideoViewController *)controller { if (self = [super init]) { self.messenger = messenger; + self.controller = controller; } return self; } @@ -88,6 +111,7 @@ - (instancetype)initWith:(NSObject *)messenger { viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args { return [[AgoraSurfaceView alloc] initWith:self.messenger + controller:self.controller frame:frame viewId:viewId args:args]; diff --git a/lib/src/impl/agora_rtc_engine_impl.dart b/lib/src/impl/agora_rtc_engine_impl.dart index 89dace963..ad2905ee5 100644 --- a/lib/src/impl/agora_rtc_engine_impl.dart +++ b/lib/src/impl/agora_rtc_engine_impl.dart @@ -67,8 +67,8 @@ int _string2IntPtr(String stringPtr) { } extension RtcEngineExt on RtcEngine { - GlobalVideoViewController get globalVideoViewController => - (this as RtcEngineImpl)._globalVideoViewController!; + GlobalVideoViewController? get globalVideoViewController => + (this as RtcEngineImpl)._globalVideoViewController; ScopedObjects get objectPool => (this as RtcEngineImpl)._objectPool; diff --git a/lib/src/impl/agora_video_view_impl.dart b/lib/src/impl/agora_video_view_impl.dart index 535314b10..025f7ff0c 100644 --- a/lib/src/impl/agora_video_view_impl.dart +++ b/lib/src/impl/agora_video_view_impl.dart @@ -73,6 +73,7 @@ class _AgoraRtcRenderPlatformViewState extends State static const String _viewTypeAgoraTextureView = 'AgoraTextureView'; static const String _viewTypeAgoraSurfaceView = 'AgoraSurfaceView'; + int _platformViewId = 0; int _nativeViewIntPtr = 0; late String _viewType; @@ -137,6 +138,7 @@ class _AgoraRtcRenderPlatformViewState extends State return buildPlatformView( viewType: _viewType, onPlatformViewCreated: (int id) { + _platformViewId = id; _setupVideo(); }, ); @@ -147,7 +149,15 @@ class _AgoraRtcRenderPlatformViewState extends State return; } - await widget.controller.setupView(_nativeViewIntPtr); + try { + await widget.controller.setupView(_nativeViewIntPtr); + } catch (e) { + debugPrint( + '[AgoraVideoView] error when widget.controller.setupView: ${e.toString()}'); + } finally { + await _controller(widget.controller).dePlatformRenderRef(_platformViewId); + } + widget.onAgoraVideoViewCreated?.call(_nativeViewIntPtr); } @@ -175,7 +185,6 @@ class _AgoraRtcRenderPlatformViewState extends State _nativeViewIntPtr = 0; await widget.controller.disposeRender(); - await getMethodChannel()?.invokeMethod('deleteNativeViewPtr'); } } diff --git a/lib/src/impl/global_video_view_controller.dart b/lib/src/impl/global_video_view_controller.dart index f3de82f4f..a3f3f5380 100644 --- a/lib/src/impl/global_video_view_controller.dart +++ b/lib/src/impl/global_video_view_controller.dart @@ -88,4 +88,12 @@ class GlobalVideoViewController { _destroyTextureRenderCompleters.remove(textureId); } } + + /// Decrease the ref count of the native view(`UIView` in iOS) of the `platformViewId`. + /// Put this function here since the the `MethodChannel` in the `AgoraVideoView` is released + /// after `AgoraVideoView.dispose`, so the `MethodChannel.invokeMethod` will never return + /// after `AgoraVideoView.dispose`. + Future dePlatformRenderRef(int platformViewId) async { + await methodChannel.invokeMethod('dePlatfromViewRef', platformViewId); + } } diff --git a/lib/src/impl/video_view_controller_impl.dart b/lib/src/impl/video_view_controller_impl.dart index dcd5588f7..830315f35 100644 --- a/lib/src/impl/video_view_controller_impl.dart +++ b/lib/src/impl/video_view_controller_impl.dart @@ -86,7 +86,7 @@ mixin VideoViewControllerBaseMixin implements VideoViewControllerBase { Future disposeRenderInternal() async { if (shouldUseFlutterTexture) { await rtcEngine.globalVideoViewController - .destroyTextureRender(getTextureId()); + ?.destroyTextureRender(getTextureId()); _textureId = kTextureNotInit; return; } @@ -137,11 +137,15 @@ mixin VideoViewControllerBaseMixin implements VideoViewControllerBase { int videoSourceType, int videoViewSetupMode, ) async { + if (rtcEngine.globalVideoViewController == null) { + return kTextureNotInit; + } + if (_isCreatedRender) { return _textureId; } final textureId = - await rtcEngine.globalVideoViewController.createTextureRender( + await rtcEngine.globalVideoViewController!.createTextureRender( uid, channelId, videoSourceType, @@ -211,6 +215,11 @@ mixin VideoViewControllerBaseMixin implements VideoViewControllerBase { _isDisposeRender = false; } + Future dePlatformRenderRef(int platformViewId) async { + await rtcEngine.globalVideoViewController + ?.dePlatformRenderRef(platformViewId); + } + @internal bool get shouldHandlerRenderMode => true; } diff --git a/shared/darwin/VideoViewController.h b/shared/darwin/VideoViewController.h index 973dc9e72..a2924f5ab 100644 --- a/shared/darwin/VideoViewController.h +++ b/shared/darwin/VideoViewController.h @@ -12,9 +12,13 @@ - (instancetype)initWith:(NSObject *)textureRegistry messenger: (NSObject *)messenger; -- (int64_t)createPlatformRender; +- (id)createPlatformRender:(int64_t)platformViewId frame:(CGRect)frame; -- (BOOL)destroyPlatformRender:(int64_t)platformRenderId; +- (BOOL)destroyPlatformRender:(int64_t)platformViewId; + +- (BOOL)addPlatformRenderRef:(int64_t)platformViewId; + +- (BOOL)dePlatformRenderRef:(int64_t)platformViewId; - (int64_t)createTextureRender:(intptr_t)irisRtcRenderingHandle uid:(NSNumber *)uid diff --git a/shared/darwin/VideoViewController.mm b/shared/darwin/VideoViewController.mm index e28d2f62e..ae023c18d 100644 --- a/shared/darwin/VideoViewController.mm +++ b/shared/darwin/VideoViewController.mm @@ -4,10 +4,132 @@ #import #import +/// A simple implemetation of ref count for an object, which just hold the value reference and record the ref count. +@interface SimpleRef : NSObject + +@property(nonatomic, strong) id value; +@property(nonatomic) int refCount; + +- (instancetype)initWith:(id) value; + +/// Increase the ref count. +- (void) addRef; + +/// Decrease the ref count. +- (void) deRef; + +/// Force clean the value reference, and set the ref count to 0. +- (void) releaseRef; + +@end + +@implementation SimpleRef + +- (instancetype)initWith:(id) value { + self = [super init]; + if (self) { + self.value = value; + self.refCount = 1; + } + return self; +} + +- (void) addRef { + ++self.refCount; +} + +- (void) deRef { + --self.refCount; +} + +- (void) releaseRef { + self.value = NULL; + self.refCount = 0; +} + +@end + +/// A pool to manage the native views that to be used in the Flutter PlatformView. You can change the native views's lifecycle through +/// the `addViewRef`/`deViewRef`. +@interface PlatformRenderPool : NSObject + +@property(nonatomic) NSMutableDictionary *renders; +- (instancetype)init; +- (id)createView:(int64_t)platformViewId frame:(CGRect)frame; +- (BOOL)addViewRef:(int64_t)platformViewId; +- (BOOL)deViewRef:(int64_t)platformViewId; + +@end + +@implementation PlatformRenderPool + +- (instancetype)init { + self = [super init]; + if (self) { + self.renders = [NSMutableDictionary new]; + } + return self; +} + +- (id)createView:(int64_t)platformViewId frame:(CGRect)frame { +#if TARGET_OS_IPHONE + UIView *v = [[UIView alloc] initWithFrame:frame]; + SimpleRef * ref = [[SimpleRef alloc] initWith:v]; + + self.renders[@(platformViewId)] = ref; + + return v; +#endif + + // Not supported on macOS + NSAssert(false, @"NOT SUPPORTED"); + return NULL; +} + +- (BOOL)destroyView:(int64_t)viewId { + if ([self.renders objectForKey:@(viewId)]) { + [self.renders removeObjectForKey:@(viewId)]; + + return true; + } + + return false; +} + +- (BOOL)addViewRef:(int64_t)platformViewId { + if ([self.renders objectForKey:@(platformViewId)]) { + SimpleRef * ref = self.renders[@(platformViewId)]; + [ref addRef]; + + return true; + } + + return false; +} + +- (BOOL)deViewRef:(int64_t)platformViewId { + if ([self.renders objectForKey:@(platformViewId)]) { + SimpleRef * ref = self.renders[@(platformViewId)]; + [ref deRef]; + + if ([ref refCount] <= 0) { + [ref releaseRef]; + [self.renders removeObjectForKey:@(platformViewId)]; + } + + return true; + } + + return false; +} + +@end + @interface VideoViewController () @property(nonatomic, weak) NSObject *textureRegistry; @property(nonatomic, weak) NSObject *messenger; @property(nonatomic) NSMutableDictionary *textureRenders; +@property(nonatomic) PlatformRenderPool* platformRenderPool; @property(nonatomic, strong) FlutterMethodChannel *methodChannel; @end @@ -21,22 +143,20 @@ - (instancetype)initWith:(NSObject *)textureRegistry self.textureRegistry = textureRegistry; self.messenger = messenger; self.textureRenders = [NSMutableDictionary new]; + self.platformRenderPool = [PlatformRenderPool new]; - self.methodChannel = [FlutterMethodChannel + self.methodChannel = [FlutterMethodChannel methodChannelWithName: @"agora_rtc_ng/video_view_controller" binaryMessenger:messenger]; - __weak typeof(self) weakSelf = self; - [self.methodChannel setMethodCallHandler:^(FlutterMethodCall *_Nonnull call, + __weak typeof(self) weakSelf = self; + [self.methodChannel setMethodCallHandler:^(FlutterMethodCall *_Nonnull call, FlutterResult _Nonnull result) { - if (weakSelf != nil) { - [weakSelf onMethodCall:call result:result]; - } - }]; - - - + if (weakSelf != nil) { + [weakSelf onMethodCall:call result:result]; + } + }]; } return self; } @@ -60,15 +180,29 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { NSNumber *textureIdValue = call.arguments; BOOL success = [self destroyTextureRender: [textureIdValue longLongValue]]; result(@(success)); + } else if ([@"dePlatfromViewRef" isEqualToString:call.method]) { + NSNumber *platformViewIdValue = call.arguments; + int64_t platformViewId = [platformViewIdValue longLongValue]; + [self dePlatformRenderRef:platformViewId]; + + result(@(YES)); } } -- (int64_t)createPlatformRender { - return 0; +- (id)createPlatformRender:(int64_t)platformViewId frame:(CGRect)frame { + return [self.platformRenderPool createView:platformViewId frame:frame]; +} + +- (BOOL)destroyPlatformRender:(int64_t)platformViewId { + return [self.platformRenderPool deViewRef:platformViewId]; +} + +- (BOOL)addPlatformRenderRef:(int64_t)platformViewId { + return [self.platformRenderPool addViewRef:platformViewId]; } -- (BOOL)destroyPlatformRender:(int64_t)platformRenderId { - return true; +- (BOOL)dePlatformRenderRef:(int64_t)platformViewId { + return [self.platformRenderPool deViewRef:platformViewId]; } - (int64_t)createTextureRender:(intptr_t)irisRtcRenderingHandle diff --git a/test_shard/integration_test_app/integration_test/fake/fake_iris_method_channel.dart b/test_shard/integration_test_app/integration_test/fake/fake_iris_method_channel.dart index 08cd73e21..762776418 100644 --- a/test_shard/integration_test_app/integration_test/fake/fake_iris_method_channel.dart +++ b/test_shard/integration_test_app/integration_test/fake/fake_iris_method_channel.dart @@ -1,38 +1,127 @@ import 'package:flutter/foundation.dart'; import 'package:iris_method_channel/iris_method_channel.dart'; +class FakeIrisMethodChannelConfig { + const FakeIrisMethodChannelConfig({ + this.isFakeInitilize = true, + this.isFakeInvokeMethod = true, + this.isFakeGetNativeHandle = true, + this.isFakeAddHotRestartListener = true, + this.isFakeRemoveHotRestartListener = true, + this.isFakeDispose = true, + this.delayInvokeMethod = const {}, + }); + + final bool isFakeInitilize; + final bool isFakeInvokeMethod; + final bool isFakeGetNativeHandle; + final bool isFakeAddHotRestartListener; + final bool isFakeRemoveHotRestartListener; + final bool isFakeDispose; + final Map delayInvokeMethod; + + FakeIrisMethodChannelConfig copyWith({ + bool? isFakeInitilize, + bool? isFakeInvokeMethod, + bool? isFakeGetNativeHandle, + bool? isFakeAddHotRestartListener, + bool? isFakeRemoveHotRestartListener, + bool? isFakeDispose, + Map? delayInvokeMethod, + }) { + return FakeIrisMethodChannelConfig( + isFakeInitilize: isFakeInitilize ?? this.isFakeInitilize, + isFakeInvokeMethod: isFakeInvokeMethod ?? this.isFakeInvokeMethod, + isFakeGetNativeHandle: + isFakeGetNativeHandle ?? this.isFakeGetNativeHandle, + isFakeAddHotRestartListener: + isFakeAddHotRestartListener ?? this.isFakeAddHotRestartListener, + isFakeRemoveHotRestartListener: + isFakeRemoveHotRestartListener ?? this.isFakeRemoveHotRestartListener, + isFakeDispose: isFakeDispose ?? this.isFakeDispose, + delayInvokeMethod: delayInvokeMethod ?? this.delayInvokeMethod, + ); + } +} + class FakeIrisMethodChannel extends IrisMethodChannel { FakeIrisMethodChannel(NativeBindingsProvider provider) : super(provider); final List methodCallQueue = []; + FakeIrisMethodChannelConfig _config = const FakeIrisMethodChannelConfig(); + FakeIrisMethodChannelConfig get config => _config; + set config(FakeIrisMethodChannelConfig value) { + _config = value; + } + @override Future initilize(List args) async { - return null; + if (_config.isFakeInitilize) { + return null; + } + + return super.initilize(args); } @override Future invokeMethod(IrisMethodCall methodCall) async { methodCallQueue.add(methodCall); - return CallApiResult(data: {'result': 0}, irisReturnCode: 0); + + Future __maybeDelay() async { + if (_config.delayInvokeMethod.containsKey(methodCall.funcName)) { + await Future.delayed(Duration( + milliseconds: _config.delayInvokeMethod[methodCall.funcName]!)); + } + } + + if (_config.isFakeInvokeMethod) { + await __maybeDelay(); + return CallApiResult(data: {'result': 0}, irisReturnCode: 0); + } + + + await __maybeDelay(); + final res = super.invokeMethod(methodCall); + return res; } @override int getNativeHandle() { - return 100; + if (_config.isFakeGetNativeHandle) { + return 100; + } + return super.getNativeHandle(); } @override VoidCallback addHotRestartListener(HotRestartListener listener) { - return () {}; + if (_config.isFakeAddHotRestartListener) { + return () {}; + } + + return super.addHotRestartListener(listener); } @override - void removeHotRestartListener(HotRestartListener listener) {} + void removeHotRestartListener(HotRestartListener listener) { + if (_config.isFakeRemoveHotRestartListener) { + return; + } + + super.removeHotRestartListener(listener); + } @override - Future dispose() async {} + Future dispose() async { + if (_config.isFakeDispose) { + return; + } + + return super.dispose(); + } void reset() { + _config = const FakeIrisMethodChannelConfig(); methodCallQueue.clear(); } } diff --git a/test_shard/integration_test_app/integration_test/testcases/fake_agora_video_view_testcases.dart b/test_shard/integration_test_app/integration_test/testcases/fake_agora_video_view_testcases.dart index 00921fd41..b5fe8d417 100644 --- a/test_shard/integration_test_app/integration_test/testcases/fake_agora_video_view_testcases.dart +++ b/test_shard/integration_test_app/integration_test/testcases/fake_agora_video_view_testcases.dart @@ -87,7 +87,7 @@ class FakeGlobalVideoViewController { FakeGlobalVideoViewController( this.rtcEngine, this.testDefaultBinaryMessenger) { testDefaultBinaryMessenger.setMockMethodCallHandler( - rtcEngine.globalVideoViewController.methodChannel, ((message) async { + rtcEngine.globalVideoViewController!.methodChannel, ((message) async { methodCallQueue.add(message); if (message.method == 'createTextureRender') { @@ -110,7 +110,7 @@ class FakeGlobalVideoViewController { void dispose() { testDefaultBinaryMessenger.setMockMethodCallHandler( - rtcEngine.globalVideoViewController.methodChannel, null); + rtcEngine.globalVideoViewController!.methodChannel, null); reset(); } } @@ -692,5 +692,63 @@ void testCases() { // }, // skip: Platform.isAndroid, // ); + + testWidgets( + 'dispose AgoraVideoView not crash before setupLocalVideo/setupRemoteVideo[Ex] call is completed', + (WidgetTester tester) async { + // This case simulate the `AgoraVideoView` is disposed before setupLocalVideo/setupRemoteVideo[Ex] + // is completed. + + irisMethodChannel.config = irisMethodChannel.config.copyWith( + isFakeInitilize: false, + isFakeInvokeMethod: false, + isFakeGetNativeHandle: false, + isFakeAddHotRestartListener: false, + isFakeRemoveHotRestartListener: false, + isFakeDispose: false, + delayInvokeMethod: { + 'RtcEngine_setupLocalVideo': 5000 + }, // delay the `RtcEngine_setupLocalVideo` to 5s, make it complete after `Widget.dispose` more easier + ); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: SizedBox( + height: 100, + width: 100, + child: AgoraVideoView( + controller: VideoViewController( + rtcEngine: rtcEngine, + canvas: const VideoCanvas(uid: 0), + ), + ), + )), + )); + + await tester.pumpAndSettle(); + + String engineAppId = const String.fromEnvironment('TEST_APP_ID', + defaultValue: ''); + + await rtcEngine.initialize(RtcEngineContext( + appId: engineAppId, + areaCode: AreaCode.areaCodeGlob.value(), + )); + + // The `AgoraVideoView` will call the `RtcEngine.setupLocalVideo` after `RtcEngine.initialize`, + // pump a `Container` to trigger the `dispose` of `AgoraVideoView`. We have blocked the `RtcEngine.setupLocalVideo` + // with 5s, the `AgoraVideoView.dipose` should be called before the `RtcEngine.setupLocalVideo`. + await tester.pumpWidget(Container()); + await tester.pumpAndSettle(); + // pumpAndSettle again to ensure the flutter PlatformView's `dispose` called that inside `AgoraVideoView` + await tester.pumpAndSettle(); + await Future.delayed(const Duration(seconds: 5)); + + expect(find.byType(AgoraVideoView), findsNothing); + + await rtcEngine.release(); + }, + skip: !(Platform.isAndroid || Platform.isIOS), + ); }); }