如何快速搭建一套视频对讲系统(三)

前面两篇介绍了如何运行 LiveKit 官方给的示例程序,今天来介绍一下怎么修改它的 flutter 示例,使它能符合我的需要搭建出一套视频对讲系统。

首先捋一下要实现的效果,我想为家里实现一个视频对讲系统,店里没人的时候,爸妈可以在手机上远程看店,顾客可以通过屏幕与爸妈交谈。爸妈年纪都大了,所以整个系统越简单越好,界面上的按钮越少越好,然后视频会话要能自动建立,一键建立。

接下来就来简单地说一下怎么修改示例程序,使我们的需求能实现。

官方示例中的文件结构是这样的

熟悉flutter的朋友知道flutter程序里源代码在lib文件夹中。其中pages文件夹是示例程序的几个界面页,打开程序后最先看到的是connect页面,

这个页面中的所有元素都要去掉,Server URL 可以写死,Token可以设置成自动获取的其他的设置都可以采用默认的设置。

点击连接后会进入prejoin页面,这个页面里可以选择摄像头、分辨率、麦克风。这个页面也可以删除,改成默认选择前置摄像头开启麦克风。

点击JOIN后,就能展示摄像头的画面了

当多个用户同时连接时,会显示视频当前的详细属性信息。这些信息也不需要了,也要删除,下方的按钮也比较多,也要删除一些。

接下来详细讲解一下要怎么改,达到我们的目的。

首先是自动生成token,token这里需要一个room及name,room可以设置为一个常量,name对于不同的设备要不同,如果两个name相同的设备同时连接则先连接的会被踢下去。

这里name我就用设备的唯一标识符来实现,设备标识符可以用device_info_plus这个库来实现,我在程序中也参考了这个库的例子程序: https://pub.dev/packages/device_info_plus/example

connect.dart进行修改。connect.dart 获取token:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import 'package:device_info_plus/device_info_plus.dart';
class _ConnectPageState extends State<ConnectPage> {
...
static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
Map<String, dynamic> _deviceData = <String, dynamic>{};

bool _connected = false;
// 获取 token
Future<void> _getToken() async {
await initPlatformState();
var sn="";
switch (defaultTargetPlatform) {
case TargetPlatform.android:
sn=_deviceData["fingerprint"];
break;
case TargetPlatform.iOS:
sn=_deviceData["identifierForVendor"];
break;
case TargetPlatform.linux:
sn=_deviceData["machineId"];
break;
case TargetPlatform.windows:
sn=_deviceData["deviceId"];
break;
case TargetPlatform.macOS:
sn=_deviceData["systemGUID"];
break;
case TargetPlatform.fuchsia:
// TODO: Handle this case.
break;
}
Dio dio = Dio();
Response response;
response = await dio.get(
'http://xxx.xxx.com:8089/getToken?room=shop&name=${sn}');
_tokenCtrl.text = response.toString();
_uriCtrl.text = 'ws://xxx.xxx.com:7880';
}
...
}

connect.dart连接按钮部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class _ConnectPageState extends State<ConnectPage> {
...
// 在连接前先获取token
Future<void> _connect(BuildContext ctx) async {
await _getToken();
try {
setState(() {
_busy = true;
});
// Save URL and Token for convenience
await _writePrefs();
print('Connecting with url: ${_uriCtrl.text}, '
'token: ${_tokenCtrl.text}...');
E2EEOptions? e2eeOptions;
if (_e2ee) {
final keyProvider = await BaseKeyProvider.create();
e2eeOptions = E2EEOptions(keyProvider: keyProvider);
var sharedKey = _sharedKeyCtrl.text;
await keyProvider.setSharedKey(sharedKey);
}

String preferredCodec = 'VP8';
// create new room
final room = Room(
roomOptions: RoomOptions(
adaptiveStream: _adaptiveStream,
dynacast: _dynacast,
defaultAudioPublishOptions: const AudioPublishOptions(
dtx: false,
stopMicTrackOnMute: false
),
defaultVideoPublishOptions: VideoPublishOptions(
simulcast: _simulcast,
videoCodec: preferredCodec,
),
defaultScreenShareCaptureOptions: const ScreenShareCaptureOptions(
useiOSBroadcastExtension: true,
params: VideoParametersPresets.screenShareH720FPS15),
e2eeOptions: e2eeOptions,
defaultCameraCaptureOptions: const CameraCaptureOptions(
maxFrameRate: 30,
params: VideoParametersPresets.h540_169,
),
));
// Create a Listener before connecting
final listener = room.createListener();
// Try to connect to the room
// This will throw an Exception if it fails for any reason.
await room.connect(
_uriCtrl.text,
_tokenCtrl.text,
fastConnectOptions: _fastConnect
? FastConnectOptions(
microphone: const TrackOption(enabled: true),
camera: const TrackOption(enabled: true),
)
: null,
);
await Navigator.push<void>(
ctx,
MaterialPageRoute(builder: (_) => RoomPage(room, listener)),
);
} catch (error) {
print('Could not connect $error');
await ctx.showErrorDialog(error);
} finally {
setState(() {
_busy = false;
});
}
}
...
}

connect.dart界面部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class _ConnectPageState extends State<ConnectPage> {
...
@override
Widget build(BuildContext context) {
if (!_connected) {
_connected = true;
_connect(context);
}

return Scaffold(
body: Container(
alignment: Alignment.center,
child: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 20,
),
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: _busy ? null : () => _connect(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_busy)
const Padding(
padding: EdgeInsets.only(right: 10),
child: SizedBox(
height: 15,
width: 15,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
),
const Text('开始'),
],
),
),
],
),
),
),
),
);
}
...
}

视频连接后的统计数据控件在participant_stats.dart中,把对应的部分删去就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
...
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black.withOpacity(0.3),
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
// child: Column(
// children:
// stats.entries.map((e) => Text('${e.key}: ${e.value}')).toList()),
);
}
}

江达小记