diff --git a/example/lib/examples/advanced/media_recorder/media_recorder.dart b/example/lib/examples/advanced/media_recorder/media_recorder.dart index 22d46c883..451c8d2de 100644 --- a/example/lib/examples/advanced/media_recorder/media_recorder.dart +++ b/example/lib/examples/advanced/media_recorder/media_recorder.dart @@ -44,9 +44,6 @@ class _State extends State { } Future _dispose() async { - if (_mediaRecorder != null) { - await _engine.destroyMediaRecorder(_mediaRecorder!); - } await _engine.release(); } @@ -146,7 +143,11 @@ class _State extends State { } Future _stopMediaRecording() async { - await _mediaRecorder?.stopRecording(); + if (_mediaRecorder != null) { + await _mediaRecorder!.stopRecording(); + await _engine.destroyMediaRecorder(_mediaRecorder!); + _mediaRecorder = null; + } setState(() { _recordingFileStoragePath = ''; _isStartedMediaRecording = false; diff --git a/lib/src/impl/agora_media_recorder_impl_override.dart b/lib/src/impl/agora_media_recorder_impl_override.dart index 45fbec5ad..c64fa46b8 100644 --- a/lib/src/impl/agora_media_recorder_impl_override.dart +++ b/lib/src/impl/agora_media_recorder_impl_override.dart @@ -46,21 +46,43 @@ class MediaRecorderObserverWrapperOverride } } +class _MediaRecorderScopedKey extends TypedScopedKey { + const _MediaRecorderScopedKey(Type type, this.strNativeHandle) : super(type); + final String strNativeHandle; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is _MediaRecorderScopedKey && + other.type == type && + other.strNativeHandle == strNativeHandle; + } + + @override + int get hashCode => Object.hash(type, strNativeHandle); +} + class MediaRecorderImpl extends media_recorder_impl_binding.MediaRecorderImpl with ScopedDisposableObjectMixin { MediaRecorderImpl._(IrisMethodChannel irisMethodChannel, this.strNativeHandle) - : super(irisMethodChannel); + : super(irisMethodChannel) { + _mediaRecorderScopedKey = + _MediaRecorderScopedKey(MediaRecorderImpl, strNativeHandle); + } factory MediaRecorderImpl.fromNativeHandle( IrisMethodChannel irisMethodChannel, String strNativeHandle) { return MediaRecorderImpl._(irisMethodChannel, strNativeHandle); } - final TypedScopedKey _mediaRecorderScopedKey = - const TypedScopedKey(MediaRecorderImpl); + late final TypedScopedKey _mediaRecorderScopedKey; final String strNativeHandle; + MediaRecorderObserverWrapperOverride? _mediaRecorderObserver; + @override Map createParams(Map param) { return { @@ -75,7 +97,7 @@ class MediaRecorderImpl extends media_recorder_impl_binding.MediaRecorderImpl final param = createParams({}); - final eventHandlerWrapper = + _mediaRecorderObserver = MediaRecorderObserverWrapperOverride(strNativeHandle, callback); await irisMethodChannel.registerEventHandler( @@ -83,12 +105,24 @@ class MediaRecorderImpl extends media_recorder_impl_binding.MediaRecorderImpl scopedKey: _mediaRecorderScopedKey, registerName: 'MediaRecorder_setMediaRecorderObserver', unregisterName: 'MediaRecorder_unsetMediaRecorderObserver', - handler: eventHandlerWrapper), + handler: _mediaRecorderObserver!), jsonEncode(param)); } @override Future dispose() async { - await irisMethodChannel.unregisterEventHandlers(_mediaRecorderScopedKey); + if (_mediaRecorderObserver == null) { + return; + } + + final param = createParams({}); + await irisMethodChannel.unregisterEventHandler( + ScopedEvent( + scopedKey: _mediaRecorderScopedKey, + registerName: 'MediaRecorder_setMediaRecorderObserver', + unregisterName: 'MediaRecorder_unsetMediaRecorderObserver', + handler: _mediaRecorderObserver!), + jsonEncode(param)); + _mediaRecorderObserver = null; } } diff --git a/lib/src/impl/agora_rtc_engine_impl.dart b/lib/src/impl/agora_rtc_engine_impl.dart index 9b3e1c4e4..9d29a5eb6 100644 --- a/lib/src/impl/agora_rtc_engine_impl.dart +++ b/lib/src/impl/agora_rtc_engine_impl.dart @@ -1038,6 +1038,8 @@ class RtcEngineImpl extends rtc_engine_ex_binding.RtcEngineExImpl Future destroyMediaRecorder(MediaRecorder mediaRecorder) async { final impl = mediaRecorder as media_recorder_impl.MediaRecorderImpl; + await impl.dispose(); + final apiType = '${isOverrideClassName ? className : 'RtcEngine'}_destroyMediaRecorder'; final param = createParams({'nativeHandle': impl.strNativeHandle}); diff --git a/test_shard/integration_test_app/integration_test/fake_apis_call_integration_test.dart b/test_shard/integration_test_app/integration_test/fake_apis_call_integration_test.dart new file mode 100644 index 000000000..8da87c7e1 --- /dev/null +++ b/test_shard/integration_test_app/integration_test/fake_apis_call_integration_test.dart @@ -0,0 +1,9 @@ +import 'package:integration_test/integration_test.dart'; + +import 'testcases/mediarecorder_fake_test_testcases.dart' as fake_mediarecorder; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + fake_mediarecorder.testCases(); +} diff --git a/test_shard/integration_test_app/integration_test/testcases/mediarecorder_fake_test_testcases.dart b/test_shard/integration_test_app/integration_test/testcases/mediarecorder_fake_test_testcases.dart new file mode 100644 index 000000000..11795923b --- /dev/null +++ b/test_shard/integration_test_app/integration_test/testcases/mediarecorder_fake_test_testcases.dart @@ -0,0 +1,132 @@ +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:agora_rtc_engine/src/impl/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:iris_method_channel/iris_method_channel.dart'; + +class MediaRecorderFakeIrisMethodChannel extends FakeIrisMethodChannel { + MediaRecorderFakeIrisMethodChannel(NativeBindingsProvider provider) + : super(provider); + + @override + Future invokeMethod(IrisMethodCall methodCall) async { + final result = super.invokeMethod(methodCall); + if (methodCall.funcName == 'RtcEngine_createMediaRecorder') { + return CallApiResult(data: {'result': '1000'}, irisReturnCode: 0); + } + + return result; + } + + Future registerEventHandler( + ScopedEvent scopedEvent, String params) async { + IrisMethodCall methodCall = + IrisMethodCall(scopedEvent.registerName, params); + + return invokeMethod(methodCall); + } + + Future unregisterEventHandler( + ScopedEvent scopedEvent, String params) async { + IrisMethodCall methodCall = + IrisMethodCall(scopedEvent.unregisterName, params); + + return invokeMethod(methodCall); + } +} + +void testCases() { + bool _isCallOnce( + MediaRecorderFakeIrisMethodChannel irisMethodChannel, String apiName) { + final calls = irisMethodChannel.methodCallQueue + .where((e) => e.funcName == apiName) + .toList(); + + return calls.length == 1; + } + + group('FakeIrisMethodChannel integration test', () { + final MediaRecorderFakeIrisMethodChannel irisMethodChannel = + MediaRecorderFakeIrisMethodChannel( + IrisApiEngineNativeBindingDelegateProvider()); + final RtcEngine rtcEngine = + RtcEngineImpl.create(irisMethodChannel: irisMethodChannel); + + setUp(() { + irisMethodChannel.reset(); + }); + + testWidgets( + 'can call startRecording after previous MediaRecorder destroy', + (WidgetTester tester) async { + String engineAppId = const String.fromEnvironment('TEST_APP_ID', + defaultValue: ''); + + await rtcEngine.initialize(RtcEngineContext( + appId: engineAppId, + areaCode: AreaCode.areaCodeGlob.value(), + )); + + MediaRecorder? recorder = await rtcEngine.createMediaRecorder( + const RecorderStreamInfo(channelId: 'test', uid: 0)); + recorder?.setMediaRecorderObserver(MediaRecorderObserver( + onRecorderStateChanged: (channelId, uid, state, error) {}, + )); + await recorder?.startRecording( + const MediaRecorderConfiguration(storagePath: 'path')); + await recorder?.stopRecording(); + await rtcEngine.destroyMediaRecorder(recorder!); + + expect(_isCallOnce(irisMethodChannel, 'RtcEngine_createMediaRecorder'), + isTrue); + expect( + _isCallOnce( + irisMethodChannel, 'MediaRecorder_setMediaRecorderObserver'), + isTrue); + expect(_isCallOnce(irisMethodChannel, 'MediaRecorder_startRecording'), + isTrue); + expect(_isCallOnce(irisMethodChannel, 'MediaRecorder_stopRecording'), + isTrue); + // When `RtcEngine.destroyMediaRecorder` is called, will call `MediaRecorderImpl.dispose` + expect( + _isCallOnce( + irisMethodChannel, 'MediaRecorder_unsetMediaRecorderObserver'), + isTrue); + expect(_isCallOnce(irisMethodChannel, 'RtcEngine_destroyMediaRecorder'), + isTrue); + + irisMethodChannel.reset(); + + recorder = await rtcEngine.createMediaRecorder( + const RecorderStreamInfo(channelId: 'test', uid: 0)); + recorder?.setMediaRecorderObserver(MediaRecorderObserver( + onRecorderStateChanged: (channelId, uid, state, error) {}, + )); + await recorder?.startRecording( + const MediaRecorderConfiguration(storagePath: 'path')); + await recorder?.stopRecording(); + await rtcEngine.destroyMediaRecorder(recorder!); + + expect(_isCallOnce(irisMethodChannel, 'RtcEngine_createMediaRecorder'), + isTrue); + expect( + _isCallOnce( + irisMethodChannel, 'MediaRecorder_setMediaRecorderObserver'), + isTrue); + expect(_isCallOnce(irisMethodChannel, 'MediaRecorder_startRecording'), + isTrue); + expect(_isCallOnce(irisMethodChannel, 'MediaRecorder_stopRecording'), + isTrue); + // When `RtcEngine.destroyMediaRecorder` is called, will call `MediaRecorderImpl.dispose` + expect( + _isCallOnce( + irisMethodChannel, 'MediaRecorder_unsetMediaRecorderObserver'), + isTrue); + expect(_isCallOnce(irisMethodChannel, 'RtcEngine_destroyMediaRecorder'), + isTrue); + }, + ); + }); +}