Skip to content

Commit a192ea9

Browse files
authored
Local recording API for Darwin and Android (#1880)
startLocalRecording & stopLocalRecording for Darwin / Android
1 parent 4165493 commit a192ea9

File tree

5 files changed

+210
-7
lines changed

5 files changed

+210
-7
lines changed

android/src/main/java/com/cloudwebrtc/webrtc/MethodCallHandlerImpl.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import android.media.AudioAttributes;
1313
import android.media.AudioDeviceInfo;
1414
import android.os.Build;
15+
import android.os.Handler;
16+
import android.os.Looper;
1517
import android.util.Log;
1618
import android.util.LongSparseArray;
1719
import android.view.Surface;
@@ -90,6 +92,8 @@
9092
import java.util.Map;
9193
import java.util.Map.Entry;
9294
import java.util.UUID;
95+
import java.util.concurrent.ExecutorService;
96+
import java.util.concurrent.Executors;
9397

9498
import io.flutter.plugin.common.BinaryMessenger;
9599
import io.flutter.plugin.common.EventChannel;
@@ -123,7 +127,7 @@ public class MethodCallHandlerImpl implements MethodCallHandler, StateProvider {
123127

124128
private CameraUtils cameraUtils;
125129

126-
private AudioDeviceModule audioDeviceModule;
130+
private JavaAudioDeviceModule audioDeviceModule;
127131

128132
private FlutterRTCFrameCryptor frameCryptor;
129133

@@ -145,6 +149,9 @@ public void onLogMessage(String message, Severity sev, String tag) {
145149
}
146150
}
147151

152+
ExecutorService executor = Executors.newSingleThreadExecutor();
153+
Handler mainHandler = new Handler(Looper.getMainLooper());
154+
148155
public static LogSink logSink = new LogSink();
149156

150157
MethodCallHandlerImpl(Context context, BinaryMessenger messenger, TextureRegistry textureRegistry) {
@@ -1037,6 +1044,24 @@ public void onMethodCall(MethodCall call, @NonNull Result notSafeResult) {
10371044
}
10381045
break;
10391046
}
1047+
case "startLocalRecording": {
1048+
executor.execute(() -> {
1049+
audioDeviceModule.prewarmRecording();
1050+
mainHandler.post(() -> {
1051+
result.success(null);
1052+
});
1053+
});
1054+
break;
1055+
}
1056+
case "stopLocalRecording": {
1057+
executor.execute(() -> {
1058+
audioDeviceModule.requestStopRecording();
1059+
mainHandler.post(() -> {
1060+
result.success(null);
1061+
});
1062+
});
1063+
break;
1064+
}
10401065
case "setLogSeverity": {
10411066
//now it's possible to setup logSeverity only via PeerConnectionFactory.initialize method
10421067
//Log.d(TAG, "no implementation for 'setLogSeverity'");

common/darwin/Classes/FlutterWebRTCPlugin.m

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
620620
NSString* dataChannelId = argsMap[@"dataChannelId"];
621621

622622
[self dataChannelGetBufferedAmount:peerConnectionId dataChannelId:dataChannelId result:result];
623-
}
623+
}
624624
else if ([@"dataChannelClose" isEqualToString:call.method]) {
625625
NSDictionary* argsMap = call.arguments;
626626
NSString* peerConnectionId = argsMap[@"peerConnectionId"];
@@ -1588,9 +1588,60 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
15881588
details:nil]);
15891589
}
15901590
#endif
1591+
} else if ([@"startLocalRecording" isEqualToString:call.method]) {
1592+
RTCAudioDeviceModule* adm = _peerConnectionFactory.audioDeviceModule;
1593+
// Run on background queue
1594+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
1595+
NSInteger admResult = [adm initAndStartRecording];
1596+
1597+
// Return to main queue
1598+
dispatch_async(dispatch_get_main_queue(), ^{
1599+
if (admResult == 0) {
1600+
result(nil);
1601+
} else {
1602+
result([FlutterError
1603+
errorWithCode:[NSString stringWithFormat:@"%@ failed", call.method]
1604+
message:[NSString stringWithFormat:@"Error: adm api failed with code: %ld",
1605+
(long)admResult]
1606+
details:nil]);
1607+
}
1608+
});
1609+
});
1610+
} else if ([@"stopLocalRecording" isEqualToString:call.method]) {
1611+
RTCAudioDeviceModule* adm = _peerConnectionFactory.audioDeviceModule;
1612+
// Run on background queue
1613+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
1614+
NSInteger admResult = [adm stopRecording];
1615+
1616+
// Return to main queue
1617+
dispatch_async(dispatch_get_main_queue(), ^{
1618+
if (admResult == 0) {
1619+
result(nil);
1620+
} else {
1621+
result([FlutterError
1622+
errorWithCode:[NSString stringWithFormat:@"%@ failed", call.method]
1623+
message:[NSString stringWithFormat:@"Error: adm api failed with code: %ld",
1624+
(long)admResult]
1625+
details:nil]);
1626+
}
1627+
});
1628+
});
1629+
} else if ([@"isVoiceProcessingEnabled" isEqualToString:call.method]) {
1630+
RTCAudioDeviceModule* adm = _peerConnectionFactory.audioDeviceModule;
1631+
NSNumber* admResult = [NSNumber numberWithBool:adm.isVoiceProcessingEnabled];
1632+
result(admResult);
1633+
} else if ([@"isVoiceProcessingBypassed" isEqualToString:call.method]) {
1634+
RTCAudioDeviceModule* adm = _peerConnectionFactory.audioDeviceModule;
1635+
NSNumber* admResult = [NSNumber numberWithBool:adm.isVoiceProcessingBypassed];
1636+
result(admResult);
1637+
} else if ([@"setIsVoiceProcessingBypassed" isEqualToString:call.method]) {
1638+
RTCAudioDeviceModule* adm = _peerConnectionFactory.audioDeviceModule;
1639+
NSNumber* value = call.arguments[@"value"];
1640+
adm.voiceProcessingBypassed = value.boolValue;
1641+
result(nil);
15911642
} else {
1592-
[self handleFrameCryptorMethodCall:call result:result];
1593-
}
1643+
[self handleFrameCryptorMethodCall:call result:result];
1644+
}
15941645
}
15951646

15961647
- (void)dealloc {
@@ -1889,7 +1940,7 @@ - (nonnull RTCConfiguration*)RTCConfiguration:(id)json {
18891940
NSNumber* maxIPv6Networks = json[@"maxIPv6Networks"];
18901941
config.maxIPv6Networks = [maxIPv6Networks intValue];
18911942
}
1892-
1943+
18931944
// === below is private api in webrtc ===
18941945
if (json[@"tcpCandidatePolicy"] != nil &&
18951946
[json[@"tcpCandidatePolicy"] isKindOfClass:[NSString class]]) {
@@ -2107,7 +2158,7 @@ - (NSDictionary*)rtpParametersToMap:(RTCRtpParameters*)parameters {
21072158
@"kind" : codec.kind
21082159
}];
21092160
}
2110-
2161+
21112162
NSString *degradationPreference = @"balanced";
21122163
if(parameters.degradationPreference != nil) {
21132164
if ([parameters.degradationPreference intValue] == RTCDegradationPreferenceMaintainFramerate ) {
@@ -2323,7 +2374,7 @@ - (RTCRtpParameters*)updateRtpParameters:(RTCRtpParameters*)parameters
23232374
NSArray<RTCRtpEncodingParameters*>* currentEncodings = parameters.encodings;
23242375
// new encodings
23252376
NSArray* newEncodings = [newParameters objectForKey:@"encodings"];
2326-
2377+
23272378
NSString *degradationPreference = [newParameters objectForKey:@"degradationPreference"];
23282379

23292380
if( degradationPreference != nil) {

example/lib/main.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
44
import 'package:flutter_background/flutter_background.dart';
55
import 'package:flutter_webrtc_example/src/capture_frame_sample.dart';
66

7+
import 'src/adm_sample.dart';
78
import 'src/device_enumeration_sample.dart';
89
import 'src/get_display_media_sample.dart';
910
import 'src/get_user_media_sample.dart'
@@ -127,6 +128,15 @@ class _MyAppState extends State<MyApp> {
127128
MaterialPageRoute(
128129
builder: (BuildContext context) => CaptureFrameSample()));
129130
}),
131+
RouteItem(
132+
title: 'ADM Sample',
133+
push: (BuildContext context) {
134+
Navigator.push(
135+
context,
136+
MaterialPageRoute(
137+
builder: (BuildContext context) => AdmSample()));
138+
}),
139+
130140
];
131141
}
132142
}

example/lib/src/adm_sample.dart

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_webrtc/flutter_webrtc.dart';
3+
4+
class AdmSample extends StatelessWidget {
5+
@override
6+
Widget build(BuildContext context) => Scaffold(
7+
appBar: AppBar(
8+
title: Text('ADM Sample'),
9+
),
10+
body: ListView(
11+
children: [
12+
ListTile(
13+
title: Text('startLocalRecording'),
14+
onTap: () async {
15+
await NativeAudioManagement.startLocalRecording();
16+
},
17+
),
18+
ListTile(
19+
title: Text('stopLocalRecording'),
20+
onTap: () async {
21+
await NativeAudioManagement.stopLocalRecording();
22+
},
23+
),
24+
ListTile(
25+
title: Text('isVoiceProcessingEnabled'),
26+
onTap: () async {
27+
final result = await NativeAudioManagement.isVoiceProcessingEnabled();
28+
print('isVoiceProcessingEnabled: $result');
29+
},
30+
),
31+
ListTile(
32+
title: Text('Get isVoiceProcessingBypassed'),
33+
onTap: () async {
34+
final result = await NativeAudioManagement.isVoiceProcessingBypassed();
35+
print('isVoiceProcessingBypassed: $result');
36+
},
37+
),
38+
ListTile(
39+
title: Text('Toggle isVoiceProcessingBypassed'),
40+
onTap: () async {
41+
final result = await NativeAudioManagement.isVoiceProcessingBypassed();
42+
await NativeAudioManagement.setIsVoiceProcessingBypassed(!result);
43+
print('isVoiceProcessingBypassed: $result');
44+
},
45+
),
46+
],
47+
),
48+
);
49+
}

lib/src/native/audio_management.dart

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,72 @@ class NativeAudioManagement {
6666
}
6767
track.enabled = !mute;
6868
}
69+
70+
// ADM APIs
71+
static Future<void> startLocalRecording() async {
72+
if (!kIsWeb) {
73+
try {
74+
await WebRTC.invokeMethod(
75+
'startLocalRecording',
76+
<String, dynamic>{},
77+
);
78+
} on PlatformException catch (e) {
79+
throw 'Unable to start local recording: ${e.message}';
80+
}
81+
}
82+
}
83+
84+
static Future<void> stopLocalRecording() async {
85+
if (!kIsWeb) {
86+
try {
87+
await WebRTC.invokeMethod(
88+
'stopLocalRecording',
89+
<String, dynamic>{},
90+
);
91+
} on PlatformException catch (e) {
92+
throw 'Unable to stop local recording: ${e.message}';
93+
}
94+
}
95+
}
96+
97+
static Future<bool> isVoiceProcessingEnabled() async {
98+
if (kIsWeb) return false;
99+
100+
try {
101+
final result = await WebRTC.invokeMethod(
102+
'isVoiceProcessingEnabled',
103+
<String, dynamic>{},
104+
);
105+
return result as bool;
106+
} on PlatformException catch (e) {
107+
throw 'Unable to get isVoiceProcessingEnabled: ${e.message}';
108+
}
109+
}
110+
111+
static Future<bool> isVoiceProcessingBypassed() async {
112+
if (kIsWeb) return false;
113+
114+
try {
115+
final result = await WebRTC.invokeMethod(
116+
'isVoiceProcessingBypassed',
117+
<String, dynamic>{},
118+
);
119+
return result as bool;
120+
} on PlatformException catch (e) {
121+
throw 'Unable to get isVoiceProcessingBypassed: ${e.message}';
122+
}
123+
}
124+
125+
static Future<void> setIsVoiceProcessingBypassed(bool value) async {
126+
if (kIsWeb) return;
127+
128+
try {
129+
await WebRTC.invokeMethod(
130+
'setIsVoiceProcessingBypassed',
131+
<String, dynamic>{"value": value},
132+
);
133+
} on PlatformException catch (e) {
134+
throw 'Unable to set isVoiceProcessingBypassed: ${e.message}';
135+
}
136+
}
69137
}

0 commit comments

Comments
 (0)