Skip to content

Commit 69cc6fe

Browse files
authored
feat: adapt loopback interfaces (#2325)
1 parent 38cd44d commit 69cc6fe

File tree

11 files changed

+567
-4
lines changed

11 files changed

+567
-4
lines changed

example/lib/examples/advanced/index.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'package:agora_rtc_engine_example/examples/advanced/start_direct_cdn_stre
1515
import 'package:agora_rtc_engine_example/examples/advanced/start_local_video_transcoder/start_local_video_transcoder.dart';
1616
import 'package:agora_rtc_engine_example/examples/advanced/stream_message/stream_message.dart';
1717
import 'package:agora_rtc_engine_example/examples/advanced/take_snapshot/take_snapshot.dart';
18+
import 'package:agora_rtc_engine_example/examples/advanced/loopback_audio/loopback_audio.dart';
1819
import 'package:flutter/foundation.dart';
1920

2021
import 'audio_mixing/audio_mixing.dart';
@@ -99,4 +100,6 @@ final advanced = [
99100
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS))
100101
{'name': 'PreCallTest', 'widget': const PreCallTest()},
101102
{'name': 'MusicPlayer', 'widget': const MusicPlayerExample()},
103+
if (Platform.isWindows || Platform.isMacOS)
104+
{'name': 'LoopbackAudio', 'widget': const LoopbackAudio()},
102105
];
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
2+
import 'package:agora_rtc_engine_example/config/agora.config.dart' as config;
3+
import 'package:agora_rtc_engine_example/components/example_actions_widget.dart';
4+
import 'package:agora_rtc_engine_example/components/log_sink.dart';
5+
import 'package:flutter/material.dart';
6+
7+
/// MultiChannel Example
8+
class LoopbackAudio extends StatefulWidget {
9+
/// Construct the [LoopbackAudio]
10+
const LoopbackAudio({Key? key}) : super(key: key);
11+
12+
@override
13+
State<StatefulWidget> createState() => _State();
14+
}
15+
16+
class _State extends State<LoopbackAudio> {
17+
late final RtcEngine _engine;
18+
19+
bool isJoined = false, switchCamera = true, switchRender = true;
20+
Set<int> remoteUid = {};
21+
late TextEditingController _controller;
22+
late TextEditingController _appNameController;
23+
24+
late final RtcEngineEventHandler _rtcEngineEventHandler;
25+
26+
static const int kInvalidLoopbackAudioTrackId = -1;
27+
28+
int _loopbackAudioTrackId = kInvalidLoopbackAudioTrackId;
29+
double _volume = 100.0;
30+
LoopbackAudioTrackType _loopbackAudioTrackType =
31+
LoopbackAudioTrackType.lookbackSystem;
32+
33+
@override
34+
void initState() {
35+
super.initState();
36+
_controller = TextEditingController(text: config.channelId);
37+
_appNameController = TextEditingController();
38+
39+
_initEngine();
40+
}
41+
42+
@override
43+
void dispose() {
44+
super.dispose();
45+
_dispose();
46+
_appNameController.dispose();
47+
}
48+
49+
Future<void> _dispose() async {
50+
_engine.unregisterEventHandler(_rtcEngineEventHandler);
51+
await _engine.leaveChannel();
52+
await _engine.release();
53+
}
54+
55+
Future<void> _initEngine() async {
56+
_engine = createAgoraRtcEngine();
57+
await _engine.initialize(RtcEngineContext(
58+
appId: config.appId,
59+
));
60+
_rtcEngineEventHandler = RtcEngineEventHandler(
61+
onError: (ErrorCodeType err, String msg) {
62+
logSink.log('[onError] err: $err, msg: $msg');
63+
},
64+
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
65+
logSink.log(
66+
'[onJoinChannelSuccess] connection: ${connection.toJson()} elapsed: $elapsed');
67+
setState(() {
68+
isJoined = true;
69+
});
70+
},
71+
onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
72+
logSink.log(
73+
'[onUserJoined] connection: ${connection.toJson()} remoteUid: $rUid elapsed: $elapsed');
74+
setState(() {
75+
remoteUid.add(rUid);
76+
});
77+
},
78+
onUserOffline:
79+
(RtcConnection connection, int rUid, UserOfflineReasonType reason) {
80+
logSink.log(
81+
'[onUserOffline] connection: ${connection.toJson()} rUid: $rUid reason: $reason');
82+
setState(() {
83+
remoteUid.removeWhere((element) => element == rUid);
84+
});
85+
},
86+
onLeaveChannel: (RtcConnection connection, RtcStats stats) {
87+
logSink.log(
88+
'[onLeaveChannel] connection: ${connection.toJson()} stats: ${stats.toJson()}');
89+
setState(() {
90+
isJoined = false;
91+
remoteUid.clear();
92+
});
93+
},
94+
);
95+
96+
_engine.registerEventHandler(_rtcEngineEventHandler);
97+
98+
await _engine.enableVideo();
99+
await _engine.startPreview();
100+
}
101+
102+
Future<void> _joinChannel() async {
103+
await _engine.joinChannel(
104+
token: config.token,
105+
channelId: _controller.text,
106+
uid: config.uid,
107+
options: const ChannelMediaOptions(
108+
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
109+
clientRoleType: ClientRoleType.clientRoleBroadcaster
110+
),
111+
);
112+
}
113+
114+
Future<void> _updateChannelMediaOptions() async {
115+
await _engine.updateChannelMediaOptions(
116+
ChannelMediaOptions(
117+
publishLoopbackAudioTrack:
118+
_loopbackAudioTrackId != kInvalidLoopbackAudioTrackId,
119+
publishLoopbackAudioTrackId: _loopbackAudioTrackId,
120+
),
121+
);
122+
}
123+
124+
Future<void> _leaveChannel() async {
125+
await _engine.leaveChannel();
126+
}
127+
128+
Future<void> _createCustomAudioTrack() async {
129+
final trackId = await _engine.getMediaEngine().createLoopbackAudioTrack(
130+
LoopbackAudioTrackConfig(
131+
loopbackType: _loopbackAudioTrackType,
132+
appName: _appNameController.text.isNotEmpty
133+
? _appNameController.text
134+
: null,
135+
volume: _volume.toInt(),
136+
),
137+
);
138+
logSink.log('createLoopbackAudioTrack: $trackId');
139+
setState(() {
140+
_loopbackAudioTrackId = trackId;
141+
});
142+
}
143+
144+
Future<void> _destroyLoopbackAudioTrack() async {
145+
final result = await _engine
146+
.getMediaEngine()
147+
.destroyLoopbackAudioTrack(_loopbackAudioTrackId);
148+
logSink.log('destroyLoopbackAudioTrack: $result');
149+
setState(() {
150+
_loopbackAudioTrackId = kInvalidLoopbackAudioTrackId;
151+
});
152+
}
153+
154+
@override
155+
Widget build(BuildContext context) {
156+
return ExampleActionsWidget(
157+
displayContentBuilder: (context, isLayoutHorizontal) {
158+
return Stack(
159+
children: [
160+
AgoraVideoView(
161+
controller: VideoViewController(
162+
rtcEngine: _engine,
163+
canvas: const VideoCanvas(uid: 0),
164+
),
165+
onAgoraVideoViewCreated: (viewId) {
166+
_engine.startPreview();
167+
},
168+
),
169+
Align(
170+
alignment: Alignment.topLeft,
171+
child: SingleChildScrollView(
172+
scrollDirection: Axis.horizontal,
173+
child: Row(
174+
children: List.of(remoteUid.map(
175+
(e) => SizedBox(
176+
width: 120,
177+
height: 120,
178+
child: AgoraVideoView(
179+
controller: VideoViewController.remote(
180+
rtcEngine: _engine,
181+
canvas: VideoCanvas(uid: e),
182+
connection:
183+
RtcConnection(channelId: _controller.text),
184+
),
185+
),
186+
),
187+
)),
188+
),
189+
),
190+
)
191+
],
192+
);
193+
},
194+
actionsBuilder: (context, isLayoutHorizontal) {
195+
final loopbackAudioTrackType = [
196+
LoopbackAudioTrackType.lookbackSystem,
197+
LoopbackAudioTrackType.lookbackSystemExcludeSelf,
198+
LoopbackAudioTrackType.lookbackApplication,
199+
];
200+
final loopbackAudioTrackTypeItems = loopbackAudioTrackType
201+
.map((e) => DropdownMenuItem(
202+
child: Text(
203+
e.toString().split('.')[1],
204+
),
205+
value: e,
206+
))
207+
.toList();
208+
209+
return Column(
210+
mainAxisAlignment: MainAxisAlignment.start,
211+
crossAxisAlignment: CrossAxisAlignment.start,
212+
mainAxisSize: MainAxisSize.min,
213+
children: [
214+
TextField(
215+
controller: _controller,
216+
decoration: const InputDecoration(hintText: 'Channel ID'),
217+
),
218+
const SizedBox(
219+
height: 20,
220+
),
221+
const Text('Loopback Audio Track Type: '),
222+
DropdownButton<LoopbackAudioTrackType>(
223+
items: loopbackAudioTrackTypeItems,
224+
value: _loopbackAudioTrackType,
225+
onChanged: _loopbackAudioTrackId == kInvalidLoopbackAudioTrackId
226+
? (v) {
227+
setState(() {
228+
_loopbackAudioTrackType = v!;
229+
});
230+
}
231+
: null,
232+
),
233+
const SizedBox(
234+
height: 20,
235+
),
236+
if (_loopbackAudioTrackType ==
237+
LoopbackAudioTrackType.lookbackApplication)
238+
Column(
239+
crossAxisAlignment: CrossAxisAlignment.start,
240+
children: [
241+
const Text('Loopback Audio Track App Name:'),
242+
TextField(
243+
controller: _appNameController,
244+
decoration: const InputDecoration(
245+
hintText: 'Enter target app name',
246+
),
247+
enabled:
248+
_loopbackAudioTrackId == kInvalidLoopbackAudioTrackId,
249+
),
250+
const SizedBox(height: 20),
251+
],
252+
),
253+
Column(
254+
crossAxisAlignment: CrossAxisAlignment.start,
255+
children: [
256+
const Text('Loopback Audio Track Volume:'),
257+
Slider(
258+
min: 0.0,
259+
max: 100.0,
260+
divisions: 100,
261+
label: _volume.round().toString(),
262+
value: _volume,
263+
onChanged:
264+
_loopbackAudioTrackId == kInvalidLoopbackAudioTrackId
265+
? (double value) {
266+
setState(() {
267+
_volume = value;
268+
});
269+
}
270+
: null,
271+
),
272+
],
273+
),
274+
const SizedBox(
275+
height: 20,
276+
),
277+
Row(children: [
278+
Expanded(
279+
flex: 1,
280+
child: ElevatedButton(
281+
onPressed:
282+
_loopbackAudioTrackId == kInvalidLoopbackAudioTrackId
283+
? _createCustomAudioTrack
284+
: _destroyLoopbackAudioTrack,
285+
child: Text(
286+
'${_loopbackAudioTrackId == kInvalidLoopbackAudioTrackId ? 'Create' : 'Destroy'} Loopback Audio Track')))
287+
]),
288+
if (isJoined) ...[
289+
const SizedBox(
290+
height: 20,
291+
),
292+
Row(children: [
293+
Expanded(
294+
flex: 1,
295+
child: ElevatedButton(
296+
onPressed: _updateChannelMediaOptions,
297+
child: const Text('Update Channel Media Options'))),
298+
]),
299+
],
300+
const SizedBox(
301+
height: 20,
302+
),
303+
Row(
304+
children: [
305+
Expanded(
306+
flex: 1,
307+
child: ElevatedButton(
308+
onPressed: isJoined ? _leaveChannel : _joinChannel,
309+
child: Text('${isJoined ? 'Leave' : 'Join'} channel'),
310+
),
311+
)
312+
],
313+
),
314+
],
315+
);
316+
},
317+
);
318+
}
319+
}

lib/src/agora_base.dart

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5047,6 +5047,62 @@ class AudioTrackConfig {
50475047
Map<String, dynamic> toJson() => _$AudioTrackConfigToJson(this);
50485048
}
50495049

5050+
/// The type of loopback audio source mode
5051+
@JsonEnum(alwaysCreate: true)
5052+
enum LoopbackAudioTrackType {
5053+
/// 0: loopback the whole system
5054+
@JsonValue(0)
5055+
lookbackSystem,
5056+
5057+
/// 1: loopback the whole system exclude self
5058+
@JsonValue(1)
5059+
lookbackSystemExcludeSelf,
5060+
5061+
/// 2: loopback the specific application
5062+
@JsonValue(2)
5063+
lookbackApplication,
5064+
}
5065+
5066+
/// @nodoc
5067+
extension LoopbackAudioTrackTypeExt on LoopbackAudioTrackType {
5068+
/// @nodoc
5069+
static LoopbackAudioTrackType fromValue(int value) {
5070+
return $enumDecode(_$LoopbackAudioTrackTypeEnumMap, value);
5071+
}
5072+
5073+
/// @nodoc
5074+
int value() {
5075+
return _$LoopbackAudioTrackTypeEnumMap[this]!;
5076+
}
5077+
}
5078+
5079+
///The configuration of custom audio track
5080+
@JsonSerializable(explicitToJson: true, includeIfNull: false)
5081+
class LoopbackAudioTrackConfig {
5082+
/// @nodoc
5083+
const LoopbackAudioTrackConfig(
5084+
{this.appName, this.volume, this.loopbackType});
5085+
5086+
/// The target app name of the loopback audio track
5087+
@JsonKey(name: 'appName')
5088+
final String? appName;
5089+
5090+
/// The volume of the loopback audio track
5091+
@JsonKey(name: 'volume')
5092+
final int? volume;
5093+
5094+
/// The loopback type refer to LOOPBACK_AUDIO_TRACK_TYPE
5095+
@JsonKey(name: 'loopbackType')
5096+
final LoopbackAudioTrackType? loopbackType;
5097+
5098+
/// @nodoc
5099+
factory LoopbackAudioTrackConfig.fromJson(Map<String, dynamic> json) =>
5100+
_$LoopbackAudioTrackConfigFromJson(json);
5101+
5102+
/// @nodoc
5103+
Map<String, dynamic> toJson() => _$LoopbackAudioTrackConfigToJson(this);
5104+
}
5105+
50505106
/// The options for SDK preset voice beautifier effects.
50515107
@JsonEnum(alwaysCreate: true)
50525108
enum VoiceBeautifierPreset {

0 commit comments

Comments
 (0)