Skip to content

Commit a08c401

Browse files
committed
feat: adapt loopback interfaces
1 parent 14d29d0 commit a08c401

File tree

10 files changed

+514
-4
lines changed

10 files changed

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

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)