Skip to content

Commit

Permalink
fix: Fix MediaRecorder.startRecording return -4 after the previous de…
Browse files Browse the repository at this point in the history
…stroy
  • Loading branch information
littleGnAl committed Jul 6, 2023
1 parent 50199cb commit 76d4dc4
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@ class _State extends State<MediaRecorderExample> {
}

Future<void> _dispose() async {
if (_mediaRecorder != null) {
await _engine.destroyMediaRecorder(_mediaRecorder!);
}
await _engine.release();
}

Expand Down Expand Up @@ -146,7 +143,11 @@ class _State extends State<MediaRecorderExample> {
}

Future<void> _stopMediaRecording() async {
await _mediaRecorder?.stopRecording();
if (_mediaRecorder != null) {
await _mediaRecorder!.stopRecording();
await _engine.destroyMediaRecorder(_mediaRecorder!);
_mediaRecorder = null;
}
setState(() {
_recordingFileStoragePath = '';
_isStartedMediaRecording = false;
Expand Down
46 changes: 40 additions & 6 deletions lib/src/impl/agora_media_recorder_impl_override.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic> createParams(Map<String, dynamic> param) {
return {
Expand All @@ -75,20 +97,32 @@ class MediaRecorderImpl extends media_recorder_impl_binding.MediaRecorderImpl

final param = createParams({});

final eventHandlerWrapper =
_mediaRecorderObserver =
MediaRecorderObserverWrapperOverride(strNativeHandle, callback);

await irisMethodChannel.registerEventHandler(
ScopedEvent(
scopedKey: _mediaRecorderScopedKey,
registerName: 'MediaRecorder_setMediaRecorderObserver',
unregisterName: 'MediaRecorder_unsetMediaRecorderObserver',
handler: eventHandlerWrapper),
handler: _mediaRecorderObserver!),
jsonEncode(param));
}

@override
Future<void> 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;
}
}
2 changes: 2 additions & 0 deletions lib/src/impl/agora_rtc_engine_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,8 @@ class RtcEngineImpl extends rtc_engine_ex_binding.RtcEngineExImpl
Future<void> 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});
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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<CallApiResult> invokeMethod(IrisMethodCall methodCall) async {
final result = super.invokeMethod(methodCall);
if (methodCall.funcName == 'RtcEngine_createMediaRecorder') {
return CallApiResult(data: {'result': '1000'}, irisReturnCode: 0);
}

return result;
}

Future<CallApiResult> registerEventHandler(
ScopedEvent scopedEvent, String params) async {
IrisMethodCall methodCall =
IrisMethodCall(scopedEvent.registerName, params);

return invokeMethod(methodCall);
}

Future<CallApiResult> 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: '<YOUR_APP_ID>');

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);
},
);
});
}

0 comments on commit 76d4dc4

Please sign in to comment.