mediasoup-demo是mediasoup官方提供的一个BS视频会议完整示例。但是有一点不好的地方在于无法选择摄像头,只能是固定使用获取到的摄像头列表的第一个摄像头。这个不实用,有时候单机插多个摄像头来调试也很不方便。
作为C++工程师完全不会前端,居然还是被我蒙出来了。
这篇文章写给两类人:
- 第一类是前端工程师,只是因为刚刚接触mediasoup-demo,还不熟悉,那么我会告诉你需要改哪些文件,按什么逻辑改,你写出来的代码,调整的UI,肯定比我好看。
- 第二类是非前端工程师,我会完整给出怎么去改,同时尝试让你了解为什么要改这些文件和写这些代码。
做出来的效果如图,选择摄像头之后,单击打开关闭摄像头的按钮(摄像机图标的按钮),先关掉当前打开的摄像头,然后再单击一次这个按钮,重新打开摄像头就是打开下拉列表选择的摄像头了。
主要思路、涉及到的文件、对象(前端工程师看完应该就可以自己动手改了):
1)只需要改两个文件:
app/lib/components/Me.jsx
app/stylus/components/Me.styl
这两个文件就是左下角本端的视频框。我在Me.jsx里增加了select标签,然后抱着能用就行的态度在Me.styl里抄了一下按钮的样式作为select标签的样式。
2)如何获取摄像头列表:
在Me.jsx里有获取RoomClient这个对象,变量名是roomClient。静音按钮、开关摄像头等的实现都是调用这个对象的方法的。这个对象的实现在app/lib/RoomClient.js里。RoomClient里有一个成员变量_webcams,存储着“摄像头设备ID”到“摄像头设备对象”(MediaDeviceInfos)的映射(Map)。
3)如何设置要使用的摄像头:
还是在RoomClient里有一个成员变量_webcam(和之前的map只差了最后的s),当前正在使用的摄像头就存储在_webcam的device成员里,存放的就是“摄像头设备对象”(MediaDeviceInfos)。
4)实现
使用什么页面元素显示摄像头列表、放在哪里、什么样式,在什么事件加载这个摄像头列表到UI上,选中的时候怎么将选中的摄像头的“摄像头设备对象”赋值给roomClient._webcam.device,就各显神通了。
给非前端工程师:
非前端工程师接下来跟我开始魔改,只求能用,不保证好看,也不保证是稳定的。
1)先修改app/lib/components/Me.jsx文件,增加select标签,即下拉列表框
按我的理解,这个文件负责生成左下角本地端的html元素,同时包含这些html元素的js代码。核心的代码是render()函数
12 class Me extends React.Component
13 {
// 省略一些内容
21
22 render()
23 {
// 注意这里的roomClient,从this.props获取到了RoomClient对象
24 const {
25 roomClient,
26 connected,
27 me,
28 audioProducer,
29 videoProducer,
30 faceDetection,
31 onSetStatsPeerId
32 } = this.props;
最初render函数就从this.props中获取到了一堆对象,其中就有我们后面要用到的roomClient。
const { xxx, yyy } = this.props;
这样的语句,应该是等价于下面这样拆开来的写法的(如果不是的话,请当我没说)
const xxx = this.props.xxx;
const yyy = this.props.yyy;
而且从this.props中获取到的时候变量就是已经赋值的,可以使用的状态了。roomClient对象我们后面再来用。
然后拉到render函数的最后,return的部分:
75 return (
76 <div
77 data-component='Me'
78 ref={(node) => (this._rootNode = node)}
79 data-tip={tip}
80 data-tip-disable={!tip}
81 >
82 <If condition={connected}>
83 <div className='controls'>
84 <div
85 className={classnames('button', 'mic', micState)}
86 onClick={() =>
87 {
88 micState === 'on'
89 ? roomClient.muteMic()
90 : roomClient.unmuteMic();
91 }}
92 />
// ......
168 </div>
169 );
这里可以看到,render函数最后是返回html标签和一些内嵌js,这些返回的html标签最终在浏览器加载页面的时候会被前端框架的代码嵌入到app/index.html中,而内嵌js最终会整合在一起生成server/public/mediasoup-demo-app.js。
既然是在render中返回html标签,那么要添加下拉列表的标签自然也就写在这里,我在mic也就是麦克风的按钮前面增加了select标签
75 return (
76 <div
77 data-component='Me'
78 ref={(node) => (this._rootNode = node)}
79 data-tip={tip}
80 data-tip-disable={!tip}
81 >
82 <If condition={connected}>
83 <div className='controls'>
<!-- 增加select标签,className是select.cameralist -->
+ <select
<!-- id是cameraListSelect -->
+ id='cameraListSelect'
+ className={classnames('select', 'cameralist')}
+ />
+
84 <div
85 className={classnames('button', 'mic', micState)}
86 onClick={() =>
87 {
88 micState === 'on'
89 ? roomClient.muteMic()
90 : roomClient.unmuteMic();
91 }}
92 />
// ......
168 </div>
169 );
这里只是加了标签,后面会讲到如何写js代码加载设备列表,以及设备选择之后怎么设置。
注意className,以mic的为例
className={classnames('button', 'mic', micState)}
micState是一个变量,从roomClient读取出来,就是当前麦克风是否开启的状态,可取值on、off、unsupported,那么className就有.button.mic.on、.button.mic.off、.button.mic.unsupported三种,注意.button.mic.on这个名称,在下面Me.styl设置的样式就会跟这个对应上。
2)修改app/stylus/components/Me.styl文件,为下拉列表select标签设置样式
styl是一种简化的css语法,经过框架处理之后形成css控制UI的样式。我在看的时候参考了如下的这篇文章(只需要看styl是什么东西,知道语法规则即可):
styl类型文件css,Stylus: 让你更简洁的完成csshttps://blog.csdn.net/weixin_42664597/article/details/119401528
借用他的一张图:
可以看出styl和生成的css的对应关系,可以概括为css不要冒号,不要分号,不要部分花括号、改用缩进描述层级关系,就是styl了。
为了方便非前端工程师理解,这里将原版的Me.styl文件的内容简化,并写了注释
1 [data-component='Me'] {
3 height: 100%;
4 width: 100%;
// 视频上方控件栏的样式
6 > .controls {
// 还设置了别的属性
9 top: 0;
10 left: 0;
// 控件栏里按钮的总样式
18 > .button {
// 还设置了别的属性
20 margin: 4px;
// 设置了.button在PC浏览器的样式,宽28px、高28px
32 +desktop() {
33 width: 28px;
34 height: 28px;
// 还有别的样式
40 }
// 设置了.button在手机浏览器的样式,宽26px、高26px
42 +mobile() {
43 width: 26px;
44 height: 26px;
45 }
// .button的“禁用”样式,设置了透明度等
51 &.disabled {
// 还设置了别的属性
53 opacity: 0.5;
54 }
// .button的“启用”样式,设置了背景色
56 &.on {
57 background-color: rgba(#fff, 0.85);
58 }
// 单独描述“麦克风按钮”的样式
60 &.mic {
// “启用”时样式,样式名完整是.button.mic.on,设置了背景图
61 &.on {
62 background-image: url('/resources/images/icon_mic_black_on.svg');
63 }
// 还有其他的状态的图标
73 }
// ......
112 }
113 }
114 }
注意.button{ &.mic { &.on { ... } } } 是声明麦克风按钮启用时的样式,将中间的{}&去掉,.button.mic.on就是前面Me.jsx的className了,两者就是这样关联上的。
然后我们开始修改,css的属性我也看不懂,但是看不懂、不会写可以抄啊。直接将按钮的样式抄上去,而且不分状态。我将我加的部分贴出来,大家可以自行理解。
1 [data-component='Me'] {
2 position: relative;
3 height: 100%;
4 width: 100%;
5
6 > .controls {
7 position: absolute;
8 z-index: 10;
9 top: 0;
10 left: 0;
11 right: 0;
12 display: flex;
13 flex-direction: row;
14 justify-content: flex-end;
15 align-items: center;
16 pointer-events: none;
// 这一节就是我加的.select.cameralist的样式,或许有很多没用的,但是能正常显示就好
+18 > .select {
// 属性照搬下面.button,包括desktop和mobile这些都是
+19 flex: 0 0 auto;
+20 margin: 4px;
+21 margin-left: 0;
+22 border-radius: 2px;
+23 pointer-events: auto;
+24 background-position: center;
+25 background-size: 75%;
+26 background-repeat: no-repeat;
+27 background-color: rgba(#000, 0.5);
+28 cursor: pointer;
+29 transition-property: opacity, background-color;
+30 transition-duration: 0.15s;
+31
+32 +desktop() {
+33 width: 80px;
+34 height: 28px;
+35 opacity: 0.85;
+36
+37 &:hover {
+38 opacity: 1;
+39 }
+40 }
+41
+42 +mobile() {
+43 width: 80px;
+44 height: 26px;
+45 }
+46
+47 &.unsupported {
+48 pointer-events: none;
+49 }
+50
+51 &.disabled {
+52 pointer-events: none;
+53 opacity: 0.5;
+54 }
+55
+56 &.on {
+57 background-color: rgba(#fff, 0.85);
+58 }
+59
+60 &.cameralist {
+61
+62 }
+63 }
65 > .button {
66 flex: 0 0 auto;
67 margin: 4px;
68 margin-left: 0;
69 border-radius: 2px;
70 pointer-events: auto;
71 background-position: center;
72 background-size: 75%;
73 background-repeat: no-repeat;
74 background-color: rgba(#000, 0.5);
75 cursor: pointer;
76 transition-property: opacity, background-color;
77 transition-duration: 0.15s;
78
79 +desktop() {
80 width: 28px;
81 height: 28px;
82 opacity: 0.85;
83
84 &:hover {
85 opacity: 1;
86 }
87 }
// ......
3)修改app/stylus/components/Me.jsx文件,添加加载设备列表和选中之后设置设备的js代码
本来想select标签有没有下拉列表弹出时触发的事件的,但是没找到,所以用了onFocus获取焦点的时候加载设备列表;列表项选中的事件是onChange。
前面定义了select标签的id是cameraListSelect,是为了在js里能获取到select标签的对象,给它动态添加下拉列表项(是不是有更好更方便的办法呢?没仔细研究)。
下面给出onFocus加载设备列表,onChange设置使用的摄像头的js代码:
<select
id='cameraListSelect'
className={classnames('select', 'cameralist')}
onFocus={() =>
{
const options = document.getElementById('cameraListSelect').options;
options.length = 0;
roomClient._updateWebcams().then(function()
{
for (const camera of roomClient._webcams.values())
{
options.add(new Option(camera.label, camera.deviceId));
}
});
}}
onChange={() =>
{
const cameralist = document.getElementById('cameraListSelect');
const sel = cameralist.options[cameralist.selectedIndex].value;
roomClient._webcam.device = roomClient._webcams.get(sel);
}}
/>
select标签的下拉列表存储在options成员里,onFocus的第一步就是获取cameraListSelect标签对象和它的options成员。
然后length清0,将下拉列表清空。roomClient._webcams前面说了是“摄像头设备ID”到“摄像头设备对象”的映射(Map),调用values获取“摄像头设备对象”的列表,使用for循环遍历,语法不懂不要紧,在其他js或jsx文件里看看,搜索一下for是怎么写的就照着写。
注意:直接访问roomClient._webcams可能没加载,因此调用roomClient._updateWebcams()方法获取roomClient._webcams,而roomClient._updateWebcams()方法有async关键字,是异步执行的,内嵌的js函数不是async函数,不能用await关键字转同步调用,所以使用async函数返回的Promise对象的then回调,在异步函数执行完之后再for循环设置列表
options里面要添加的是Option对象,第一个参数camera.label是摄像头的可见名称,camera.deviceId是摄像头的一个唯一ID,作为每个列表项的值。
onChange的目标就是获取select标签当前选择的项,将对应的摄像头唯一ID拿出来,然后调用roomClient._webcams的get方法获取“摄像头设备对象”(MediaDeviceInfos),赋值给roomClient._webcam.device就大功告成了。