Skip to content

Commit

Permalink
fix: Prevent multiple initializations of internal resources when `Rtc…
Browse files Browse the repository at this point in the history
…Engine.initialize` is called simultaneously (#1712)

A case like:
```dart
for (int i = 0; i < 5; ++i) {
  rtcEngine.initialize(RtcEngineContext(
    appId: engineAppId,
    areaCode: AreaCode.areaCodeGlob.value(),
    logConfig: LogConfig(filePath: logPath),
  ));
}
```
This causes `irisMethodChannel.initilize(args)`, and
`_initializeInternal(context)` called multiple times, which causes
unexpected behaviors.

`iris_method_channel` fix:
AgoraIO-Extensions/iris_method_channel_flutter#98
  • Loading branch information
littleGnAl authored Apr 21, 2024
1 parent 02aec26 commit 462cfc3
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 20 deletions.
69 changes: 52 additions & 17 deletions lib/src/impl/agora_rtc_engine_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@ import 'package:agora_rtc_engine/src/impl/media_player_impl.dart'
as media_player_impl;

import 'package:agora_rtc_engine/src/impl/platform/platform_bindings_provider.dart';
import 'package:async/async.dart' show AsyncMemoizer;
import 'package:flutter/foundation.dart'
show ChangeNotifier, debugPrint, defaultTargetPlatform, kIsWeb;
show
ChangeNotifier,
debugPrint,
defaultTargetPlatform,
kIsWeb,
visibleForTesting;
import 'package:flutter/services.dart' show MethodChannel;
import 'package:flutter/widgets.dart' show VoidCallback, TargetPlatform;
import 'package:iris_method_channel/iris_method_channel.dart';
Expand Down Expand Up @@ -361,6 +367,8 @@ class RtcEngineImpl extends rtc_engine_ex_binding.RtcEngineExImpl
@internal
late MethodChannel engineMethodChannel;

AsyncMemoizer? _initializeCallOnce;

static RtcEngineEx create({
Object? sharedNativeHandle,
IrisMethodChannel? irisMethodChannel,
Expand All @@ -379,6 +387,17 @@ class RtcEngineImpl extends rtc_engine_ex_binding.RtcEngineExImpl
return _instance!;
}

@visibleForTesting
static RtcEngineEx createForTesting({
Object? sharedNativeHandle,
required IrisMethodChannel irisMethodChannel,
}) {
return RtcEngineImpl._(
irisMethodChannel: irisMethodChannel,
sharedNativeHandle: sharedNativeHandle,
);
}

void _updateSharedNativeHandle(Object? sharedNativeHandle) {
if (_sharedNativeHandle != sharedNativeHandle) {
_sharedNativeHandle = sharedNativeHandle;
Expand All @@ -404,26 +423,35 @@ class RtcEngineImpl extends rtc_engine_ex_binding.RtcEngineExImpl
await _releasingCompleter?.future;
}

_initializingCompleter = Completer<void>();
engineMethodChannel = const MethodChannel('agora_rtc_ng');

if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
await engineMethodChannel.invokeMethod('androidInit');
// If previous initialization still in progess, skip it.
if (_initializingCompleter != null &&
!_initializingCompleter!.isCompleted) {
return;
}

List<InitilizationArgProvider> args = [
if (_sharedNativeHandle != null)
SharedNativeHandleInitilizationArgProvider(_sharedNativeHandle!)
];
assert(() {
if (_mockRtcEngineProvider != null) {
args.add(_mockRtcEngineProvider!);
_initializingCompleter = Completer<void>();
_initializeCallOnce ??= AsyncMemoizer();
await _initializeCallOnce!.runOnce(() async {
engineMethodChannel = const MethodChannel('agora_rtc_ng');

if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
await engineMethodChannel.invokeMethod('androidInit');
}
return true;
}());

await irisMethodChannel.initilize(args);
await _initializeInternal(context);
List<InitilizationArgProvider> args = [
if (_sharedNativeHandle != null)
SharedNativeHandleInitilizationArgProvider(_sharedNativeHandle!)
];
assert(() {
if (_mockRtcEngineProvider != null) {
args.add(_mockRtcEngineProvider!);
}
return true;
}());

await irisMethodChannel.initilize(args);
await _initializeInternal(context);
});

await super.initialize(context);

Expand Down Expand Up @@ -457,6 +485,11 @@ class RtcEngineImpl extends rtc_engine_ex_binding.RtcEngineExImpl
await _initializingCompleter?.future;
}

// If previous release still in progess, skip it.
if (_releasingCompleter != null && !_releasingCompleter!.isCompleted) {
return;
}

if (!_rtcEngineState.isInitialzed || _isReleased) {
return;
}
Expand All @@ -480,6 +513,8 @@ class RtcEngineImpl extends rtc_engine_ex_binding.RtcEngineExImpl
_isReleased = true;
_releasingCompleter?.complete(null);
_releasingCompleter = null;
assert(_initializeCallOnce!.hasRun);
_initializeCallOnce = null;
_instance = null;
}

Expand Down
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ dependencies:
sdk: flutter
json_annotation: ^4.4.0
ffi: '>=1.1.2'
async: ^2.8.2
async: '>=2.8.2'
meta: ^1.7.0
iris_method_channel: 2.1.0
iris_method_channel: 2.1.1
js: '>=0.6.3'
dev_dependencies:
flutter_test:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class FakeIrisMethodChannel extends IrisMethodChannel {
@override
Future<InitilizationResult?> initilize(
List<InitilizationArgProvider> args) async {
methodCallQueue.add(const IrisMethodCall('initilize', '{}'));
if (_config.isFakeInitilize) {
return null;
}
Expand Down Expand Up @@ -93,6 +94,7 @@ class FakeIrisMethodChannel extends IrisMethodChannel {

@override
int getApiEngineHandle() {
methodCallQueue.add(const IrisMethodCall('getApiEngineHandle', '{}'));
if (_config.isFakeGetNativeHandle) {
return 100;
}
Expand All @@ -101,6 +103,7 @@ class FakeIrisMethodChannel extends IrisMethodChannel {

@override
VoidCallback addHotRestartListener(HotRestartListener listener) {
methodCallQueue.add(const IrisMethodCall('addHotRestartListener', '{}'));
if (_config.isFakeAddHotRestartListener) {
return () {};
}
Expand All @@ -110,6 +113,7 @@ class FakeIrisMethodChannel extends IrisMethodChannel {

@override
void removeHotRestartListener(HotRestartListener listener) {
methodCallQueue.add(const IrisMethodCall('removeHotRestartListener', '{}'));
if (_config.isFakeRemoveHotRestartListener) {
return;
}
Expand All @@ -119,6 +123,7 @@ class FakeIrisMethodChannel extends IrisMethodChannel {

@override
Future<void> dispose() async {
methodCallQueue.add(const IrisMethodCall('dispose', '{}'));
if (_config.isFakeDispose) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'package:integration_test/integration_test.dart';

import 'testcases/mediarecorder_fake_test_testcases.dart' as fake_mediarecorder;
import 'testcases/rtcengine_fake_test_testcases.dart' as fake_rtcengine;

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

fake_mediarecorder.testCases();
fake_rtcengine.testCases();
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ void testCases() {
MediaRecorderFakeIrisMethodChannel(
IrisApiEngineNativeBindingDelegateProvider());
final RtcEngine rtcEngine =
RtcEngineImpl.create(irisMethodChannel: irisMethodChannel);
RtcEngineImpl.createForTesting(irisMethodChannel: irisMethodChannel);

setUp(() {
irisMethodChannel.reset();
Expand Down Expand Up @@ -121,6 +121,7 @@ void testCases() {
const MediaRecorderConfiguration(storagePath: 'path'));
await recorder?.stopRecording();
await rtcEngine.destroyMediaRecorder(recorder!);
await rtcEngine.release();

expect(
_isCallOnce(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:agora_rtc_engine/src/impl/platform/io/native_iris_api_engine_binding_delegate.dart';
import '../fake/fake_iris_method_channel.dart';
import 'package:agora_rtc_engine/src/impl/agora_rtc_engine_impl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'dart:io';

void testCases() {
group('RtcEngine.initialize', () {
final FakeIrisMethodChannel irisMethodChannel =
FakeIrisMethodChannel(IrisApiEngineNativeBindingDelegateProvider());
final RtcEngine rtcEngine =
RtcEngineImpl.createForTesting(irisMethodChannel: irisMethodChannel);

setUp(() {
irisMethodChannel.reset();
});

testWidgets(
'only initialize once',
(WidgetTester tester) async {
String engineAppId = const String.fromEnvironment('TEST_APP_ID',
defaultValue: '<YOUR_APP_ID>');

Directory appDocDir = await getApplicationDocumentsDirectory();
String logPath = path.join(appDocDir.path, 'test_log.txt');

await rtcEngine.initialize(RtcEngineContext(
appId: engineAppId,
areaCode: AreaCode.areaCodeGlob.value(),
logConfig: LogConfig(filePath: logPath),
));
await rtcEngine.initialize(RtcEngineContext(
appId: engineAppId,
areaCode: AreaCode.areaCodeGlob.value(),
logConfig: LogConfig(filePath: logPath),
));
await rtcEngine.initialize(RtcEngineContext(
appId: engineAppId,
areaCode: AreaCode.areaCodeGlob.value(),
logConfig: LogConfig(filePath: logPath),
));
final calls = irisMethodChannel.methodCallQueue
.where((e) => e.funcName == 'initilize')
.toList();
expect(calls.length == 1, true);

await rtcEngine.release();
},
);

testWidgets(
'only initialize once when called simultaneously',
(WidgetTester tester) async {
String engineAppId = const String.fromEnvironment('TEST_APP_ID',
defaultValue: '<YOUR_APP_ID>');

Directory appDocDir = await getApplicationDocumentsDirectory();
String logPath = path.join(appDocDir.path, 'test_log.txt');

for (int i = 0; i < 5; ++i) {
rtcEngine.initialize(RtcEngineContext(
appId: engineAppId,
areaCode: AreaCode.areaCodeGlob.value(),
logConfig: LogConfig(filePath: logPath),
));
}
// Wait for the 5 times calls of `irisMethodChannel.initilize` are completed.
await Future.delayed(const Duration(milliseconds: 1000));

final calls = irisMethodChannel.methodCallQueue
.where((e) => e.funcName == 'initilize')
.toList();
expect(calls.length == 1, true);

await rtcEngine.release();
},
);

testWidgets(
're-initialize once after release',
(WidgetTester tester) async {
String engineAppId = const String.fromEnvironment('TEST_APP_ID',
defaultValue: '<YOUR_APP_ID>');

Directory appDocDir = await getApplicationDocumentsDirectory();
String logPath = path.join(appDocDir.path, 'test_log.txt');
{
await rtcEngine.initialize(RtcEngineContext(
appId: engineAppId,
areaCode: AreaCode.areaCodeGlob.value(),
logConfig: LogConfig(filePath: logPath),
));
await rtcEngine.initialize(RtcEngineContext(
appId: engineAppId,
areaCode: AreaCode.areaCodeGlob.value(),
logConfig: LogConfig(filePath: logPath),
));
await rtcEngine.initialize(RtcEngineContext(
appId: engineAppId,
areaCode: AreaCode.areaCodeGlob.value(),
logConfig: LogConfig(filePath: logPath),
));

final calls = irisMethodChannel.methodCallQueue
.where((e) => e.funcName == 'initilize')
.toList();
expect(calls.length == 1, true);

await rtcEngine.release();
}

irisMethodChannel.reset();

{
await rtcEngine.initialize(RtcEngineContext(
appId: engineAppId,
areaCode: AreaCode.areaCodeGlob.value(),
logConfig: LogConfig(filePath: logPath),
));
await rtcEngine.initialize(RtcEngineContext(
appId: engineAppId,
areaCode: AreaCode.areaCodeGlob.value(),
logConfig: LogConfig(filePath: logPath),
));
await rtcEngine.initialize(RtcEngineContext(
appId: engineAppId,
areaCode: AreaCode.areaCodeGlob.value(),
logConfig: LogConfig(filePath: logPath),
));

final calls = irisMethodChannel.methodCallQueue
.where((e) => e.funcName == 'initilize')
.toList();
expect(calls.length == 1, true);
}

await rtcEngine.release();
},
);
});
}

0 comments on commit 462cfc3

Please sign in to comment.