Skip to content

Commit cd33120

Browse files
authored
fix: AgoraVideoView crash when dispose after RtcEngine.release (#1585)
The `IrisRtcEngineRendering` handle becomes invalid after `RtcEngine.release`, which needs explicit clean-up of the texture renderers on the native side. Fix #1567
1 parent 4cb3c68 commit cd33120

File tree

9 files changed

+111
-33
lines changed

9 files changed

+111
-33
lines changed

android/src/main/cpp/iris_rtc_rendering_android.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ class NativeTextureRenderer final
711711
}
712712
}
713713

714-
~NativeTextureRenderer() final { Dispose(); }
714+
~NativeTextureRenderer() final {}
715715

716716
void OnVideoFrameReceived(const void *videoFrame,
717717
const IrisRtcVideoFrameConfig &config,

android/src/main/java/io/agora/agora_rtc_ng/VideoViewController.java

+15-4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ void releaseRef() {
5656
class PlatformRenderPool {
5757

5858
private final Map<Integer, SimpleRef> renders = new HashMap<>();
59+
5960
SimpleRef createView(int platformViewId,
6061
Context context,
6162
AgoraPlatformViewFactory.PlatformViewProvider viewProvider) {
@@ -182,10 +183,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
182183
case "createTextureRender": {
183184
final Map<?, ?> args = (Map<?, ?>) call.arguments;
184185

185-
@SuppressWarnings("ConstantConditions")
186-
final long irisRtcRenderingHandle = getLong(args.get("irisRtcRenderingHandle"));
187-
@SuppressWarnings("ConstantConditions")
188-
final long uid = getLong(args.get("uid"));
186+
@SuppressWarnings("ConstantConditions") final long irisRtcRenderingHandle = getLong(args.get("irisRtcRenderingHandle"));
187+
@SuppressWarnings("ConstantConditions") final long uid = getLong(args.get("uid"));
189188
final String channelId = (String) args.get("channelId");
190189
final int videoSourceType = (int) args.get("videoSourceType");
191190
final int videoViewSetupMode = (int) args.get("videoViewSetupMode");
@@ -205,13 +204,25 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
205204
result.success(success);
206205
break;
207206
}
207+
case "dispose": {
208+
disposeAllRenderers();
209+
result.success(true);
210+
break;
211+
}
208212
case "updateTextureRenderData":
209213
default:
210214
result.notImplemented();
211215
break;
212216
}
213217
}
214218

219+
private void disposeAllRenderers() {
220+
for (final TextureRenderer textureRenderer : textureRendererMap.values()) {
221+
textureRenderer.dispose();
222+
}
223+
textureRendererMap.clear();
224+
}
225+
215226
/**
216227
* Flutter may convert a long to int type in java, we force parse a long value via this function
217228
*/

lib/src/impl/platform/io/global_video_view_controller_platform_io.dart

+11-25
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ class GlobalVideoViewControllerIO extends GlobalVideoViewControllerPlatfrom {
2525
@override
2626
int get irisRtcRenderingHandle => _irisRtcRenderingHandle;
2727

28-
final Map<int, Completer<void>> _destroyTextureRenderCompleters = {};
29-
bool _isDetachVFBMing = false;
30-
3128
void _hotRestartListener(Object? message) {
3229
assert(() {
3330
// Free `IrisRtcRendering` when hot restart
@@ -64,34 +61,28 @@ class GlobalVideoViewControllerIO extends GlobalVideoViewControllerPlatfrom {
6461
return;
6562
}
6663

64+
final irisRtcRenderingHandle = _irisRtcRenderingHandle;
65+
_irisRtcRenderingHandle = 0;
66+
6767
irisMethodChannel.removeHotRestartListener(_hotRestartListener);
6868

69-
_isDetachVFBMing = true;
70-
71-
// Need wait for all `destroyTextureRender` functions are called completed before
72-
// `FreeIrisVideoFrameBufferManager`, if not, the `destroyTextureRender`(call
73-
// `IrisVideoFrameBufferManager.DisableVideoFrameBuffer` in native side) and
74-
// `FreeIrisVideoFrameBufferManager` will be called parallelly, which will cause crash.
75-
for (final completer in _destroyTextureRenderCompleters.values) {
76-
if (!completer.isCompleted) {
77-
await completer.future;
78-
}
79-
}
80-
_destroyTextureRenderCompleters.clear();
69+
await methodChannel.invokeMethod('dispose');
8170

8271
await irisMethodChannel.invokeMethod(IrisMethodCall(
8372
'FreeIrisRtcRendering',
8473
jsonEncode({
8574
'irisRtcEngineNativeHandle': irisRtcEngineIntPtr,
86-
'irisRtcRenderingHandle': _irisRtcRenderingHandle,
75+
'irisRtcRenderingHandle': irisRtcRenderingHandle,
8776
}),
8877
));
89-
_irisRtcRenderingHandle = 0;
9078
}
9179

9280
@override
9381
Future<int> createTextureRender(int uid, String channelId,
9482
int videoSourceType, int videoViewSetupMode) async {
83+
if (_irisRtcRenderingHandle == 0) {
84+
return kTextureNotInit;
85+
}
9586
final textureId =
9687
await methodChannel.invokeMethod<int>('createTextureRender', {
9788
'irisRtcRenderingHandle': _irisRtcRenderingHandle,
@@ -106,16 +97,11 @@ class GlobalVideoViewControllerIO extends GlobalVideoViewControllerPlatfrom {
10697
/// Call `IrisVideoFrameBufferManager.DisableVideoFrameBuffer` in the native side
10798
@override
10899
Future<void> destroyTextureRender(int textureId) async {
109-
_destroyTextureRenderCompleters.putIfAbsent(
110-
textureId, () => Completer<void>());
100+
if (_irisRtcRenderingHandle == 0) {
101+
return;
102+
}
111103

112104
await methodChannel.invokeMethod('destroyTextureRender', textureId);
113-
114-
_destroyTextureRenderCompleters[textureId]?.complete(null);
115-
116-
if (!_isDetachVFBMing) {
117-
_destroyTextureRenderCompleters.remove(textureId);
118-
}
119105
}
120106

121107
/// Decrease the ref count of the native view(`UIView` in iOS) of the `platformViewId`.

shared/darwin/VideoViewController.mm

+12
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ @interface VideoViewController ()
132132
@property(nonatomic) PlatformRenderPool* platformRenderPool;
133133

134134
@property(nonatomic, strong) FlutterMethodChannel *methodChannel;
135+
136+
- (void)dispose;
135137
@end
136138

137139
@implementation VideoViewController
@@ -185,6 +187,9 @@ - (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
185187
int64_t platformViewId = [platformViewIdValue longLongValue];
186188
[self dePlatformRenderRef:platformViewId];
187189

190+
result(@(YES));
191+
} else if ([@"dispose" isEqualToString:call.method]) {
192+
[self dispose];
188193
result(@(YES));
189194
}
190195
}
@@ -231,4 +236,11 @@ - (BOOL)destroyTextureRender:(int64_t)textureId {
231236
return NO;
232237
}
233238

239+
- (void)dispose {
240+
for (TextureRender * textureRender in self.textureRenders.allValues) {
241+
[textureRender dispose];
242+
}
243+
[self.textureRenders removeAllObjects];
244+
}
245+
234246
@end

test_shard/integration_test_app/integration_test/fake/fake_iris_method_channel.dart

+7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class FakeIrisMethodChannelConfig {
1010
this.isFakeRemoveHotRestartListener = true,
1111
this.isFakeDispose = true,
1212
this.delayInvokeMethod = const {},
13+
this.fakeInvokeMethods = const {},
1314
});
1415

1516
final bool isFakeInitilize;
@@ -19,6 +20,7 @@ class FakeIrisMethodChannelConfig {
1920
final bool isFakeRemoveHotRestartListener;
2021
final bool isFakeDispose;
2122
final Map<String, int> delayInvokeMethod;
23+
final Map<String, CallApiResult> fakeInvokeMethods;
2224

2325
FakeIrisMethodChannelConfig copyWith({
2426
bool? isFakeInitilize,
@@ -28,6 +30,7 @@ class FakeIrisMethodChannelConfig {
2830
bool? isFakeRemoveHotRestartListener,
2931
bool? isFakeDispose,
3032
Map<String, int>? delayInvokeMethod,
33+
Map<String, CallApiResult>? fakeInvokeMethods
3134
}) {
3235
return FakeIrisMethodChannelConfig(
3336
isFakeInitilize: isFakeInitilize ?? this.isFakeInitilize,
@@ -40,6 +43,7 @@ class FakeIrisMethodChannelConfig {
4043
isFakeRemoveHotRestartListener ?? this.isFakeRemoveHotRestartListener,
4144
isFakeDispose: isFakeDispose ?? this.isFakeDispose,
4245
delayInvokeMethod: delayInvokeMethod ?? this.delayInvokeMethod,
46+
fakeInvokeMethods: fakeInvokeMethods ?? this.fakeInvokeMethods,
4347
);
4448
}
4549
}
@@ -77,6 +81,9 @@ class FakeIrisMethodChannel extends IrisMethodChannel {
7781

7882
if (_config.isFakeInvokeMethod) {
7983
await __maybeDelay();
84+
if (_config.fakeInvokeMethods.containsKey(methodCall.funcName)) {
85+
return _config.fakeInvokeMethods[methodCall.funcName]!;
86+
}
8087
return CallApiResult(data: {'result': 0}, irisReturnCode: 0);
8188
}
8289

test_shard/integration_test_app/integration_test/testcases/agora_video_view_testcases.dart

+43-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import 'dart:async';
2-
import 'dart:convert';
32
import 'dart:io';
43
import 'dart:math';
54

65
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
76
import 'package:flutter/foundation.dart';
87
import 'package:flutter/material.dart';
98
import 'package:flutter_test/flutter_test.dart';
10-
import 'package:agora_rtc_engine/src/impl/agora_rtc_engine_impl.dart';
11-
import '../fake/fake_iris_method_channel.dart';
129

1310
class _RenderViewWidget extends StatefulWidget {
1411
const _RenderViewWidget({
@@ -325,4 +322,47 @@ void testCases() {
325322

326323
expect(find.byType(AgoraVideoView), findsNothing);
327324
});
325+
326+
testWidgets('Dispose AgoraVideoView after RtcEngine.release',
327+
(WidgetTester tester) async {
328+
final videoViewCreatedCompleter = Completer<bool>();
329+
final key = GlobalKey<_RenderViewWidgetState>();
330+
331+
await tester.pumpWidget(_RenderViewWidget(
332+
key: key,
333+
builder: (context, engine) {
334+
return SizedBox(
335+
height: 100,
336+
width: 100,
337+
child: AgoraVideoView(
338+
controller: VideoViewController(
339+
rtcEngine: engine,
340+
canvas: const VideoCanvas(uid: 0),
341+
useFlutterTexture: true,
342+
),
343+
onAgoraVideoViewCreated: (viewId) {
344+
if (!videoViewCreatedCompleter.isCompleted) {
345+
videoViewCreatedCompleter.complete(true);
346+
}
347+
},
348+
),
349+
);
350+
},
351+
));
352+
353+
await tester.pumpAndSettle(const Duration(milliseconds: 5000));
354+
// pumpAndSettle again to ensure the `AgoraVideoView` shown
355+
await tester.pumpAndSettle(const Duration(milliseconds: 5000));
356+
357+
await videoViewCreatedCompleter.future;
358+
359+
// Call `RtcEngine.release` before `AgoraVideoView` dispose
360+
await key.currentState?._dispose();
361+
362+
await tester.pumpWidget(Container());
363+
await tester.pumpAndSettle(const Duration(milliseconds: 5000));
364+
await Future.delayed(const Duration(seconds: 5));
365+
366+
expect(find.byType(AgoraVideoView), findsNothing);
367+
});
328368
}

test_shard/integration_test_app/integration_test/testcases/fake_agora_video_view_testcases.dart

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:agora_rtc_engine/src/impl/agora_rtc_engine_impl.dart';
1111
import '../fake/fake_iris_method_channel.dart';
1212
import 'package:agora_rtc_engine/src/impl/platform/io/global_video_view_controller_platform_io.dart';
1313
import 'package:agora_rtc_engine/src/impl/platform/io/native_iris_api_engine_binding_delegate.dart';
14+
import 'package:iris_method_channel/iris_method_channel.dart';
1415

1516
class _RenderViewWidget extends StatefulWidget {
1617
const _RenderViewWidget({
@@ -191,6 +192,11 @@ void testCases() {
191192

192193
setUp(() {
193194
irisMethodChannel.reset();
195+
irisMethodChannel.config =
196+
FakeIrisMethodChannelConfig(fakeInvokeMethods: {
197+
'CreateIrisRtcRendering': CallApiResult(
198+
data: {'irisRtcRenderingHandle': 100}, irisReturnCode: 0)
199+
});
194200
});
195201

196202
group(

windows/include/agora_rtc_engine/video_view_controller.h

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class VideoViewController
3434

3535
bool DestroyTextureRender(int64_t textureId);
3636

37+
void Dispose();
38+
3739
public:
3840
VideoViewController(flutter::TextureRegistrar *texture_registrar, flutter::BinaryMessenger *messenger_);
3941
virtual ~VideoViewController();

windows/video_view_controller.cc

+14
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ void VideoViewController::HandleMethodCall(
126126

127127
result->Success(flutter::EncodableValue(true));
128128
}
129+
else if (method.compare("dispose") == 0)
130+
{
131+
Dispose();
132+
result->Success(flutter::EncodableValue(true));
133+
}
129134
else if (method.compare("updateTextureRenderData") == 0)
130135
{
131136
}
@@ -171,4 +176,13 @@ bool VideoViewController::DestroyTextureRender(int64_t textureId)
171176
return true;
172177
}
173178
return false;
179+
}
180+
181+
void VideoViewController::Dispose()
182+
{
183+
for (const auto &entry : renderers_)
184+
{
185+
entry.second->Dispose();
186+
}
187+
renderers_.clear();
174188
}

0 commit comments

Comments
 (0)