Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Prevent multiple initializations of internal resources when RtcEngine.initialize is called simultaneously #1712

Merged
merged 4 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
},
);
});
}
Loading