2.5、实战案例(二)
文章导读:继上节的内容,本节要完成视频采集参数以及滤镜参数的动态配置。推荐阅读方式:实操。
为了更好的讲解代码,这里还是把软件的界面展示出来,如下图2.5.1。
图 2.5.1 (软件界面截图)
首先实现第一个功能:动态设置视频采集参数。
不管是视频采集的参数还是滤镜的参数,我们可以看成是同一种业务,因此我们新建一个对象来管理:configManager,目前该对象有两个方法:读取视频采集参数getConstrains和读取滤镜参数getFilterConfig。先来实现第一个方法——getConstrains ,逻辑是:通过JS代码读取表单元素的值,并且返回一个参数对象。因此先为对应的表单元素加上id,方便操作,代码如下。
<div class="constraints-item"> <label>是否启用音频</label> <input id="useAudio" type="checkbox"> </div> <div class="constraints-item"> <label>视频宽</label> <input value="300" id="videoWidth" type="number"> </div> <div class="constraints-item"> <label>视频高</label> <input value="200" id="videoHeight" type="number"> </div> <div class="constraints-item"> <label>采集设备选择</label> <select aria-placeholder="请选择采集的设备" name="" id="devicesList"> </select> </div> <div class="constraints-item"> <label>视频帧率</label><input value="30" id="frameRate" type="number"> </div>
上述代码中,除了为表单元素设置id外,还设置了默认值,但“采集设备选择”这个表单控件(id="devicesList")比较特殊,需要通过webrtc API 读取到本机可用的摄像头列表才可赋值,那什么时候读取摄像头设备列表呢?根据2.1节 学习到的内容,首先得通过getUserMedia读取默认设备的媒体流,触发浏览器的对本域名程序访问摄像机、麦克风的权限询问,且通过了用户手动允许后,我们才能“顺畅”的访问其他webrtc API ,如enumerateDevices(列举本机可用的媒体设备列表)。按照这个思路,只有“打开摄像头”的逻辑会调用getUserMedia,所以,我们将在打开摄像头成功之后去列举并显示可用媒体设备列表,当然,这部分的逻辑依旧属于摄像机的管理操作,于是我们在cameraManager中新增一个方法——enumerateVideoDevices,其功能顾名思义就是列举出视频输入设备,其他的设备如音频输入、音频输出暂时不考虑。
接下来分析下cameraManager的enumerateVideoDevices的逻辑:首先通过webrtc的相关API读取到可用的设备列表,注意:可用设备列表包含了 “音频输入(audioinpt)、音频输出(audiooutput)、视频输入(videoinput)”三种设备类型,而我们要筛选出视频输入(videoinput)设备出来即可;其次,筛选出来的视频输入设备后,我们要动态创建dom元素放到”<select aria-placeholder="请选择采集的设备" name="" id="devicesList">“标签中,供用户选择。代码如下。
// 加载摄像头供用户选择 async enumerateVideoDevices() { let devices = await navigator.mediaDevices.enumerateDevices() let devicesListDom = domManager.getDom("devicesList"); devicesListDom.innerHTML = ""; // 清空子元素 if (devices) { for (let d of devices) { if (d && (d.kind == 'videoinput')) { let element = document.createElement("option"); if (element) { element.value = d.deviceId; element.innerHTML = d.label; devicesListDom.appendChild(element); } } } } },
至此,cameraManager对象就多了enumerateVideoDevices方法,该方法在哪里被调用?调用代码如下,即在cameraManager的openCamera方法中。代码如下。
//开启摄像头 async openCamera(mediaStreamconstrains) { let media = await navigator.mediaDevices.getUserMedia(mediaStreamconstrains); this.enumerateVideoDevices();//调用列举可用视频设备列表 this.mediaStream = media; return media; },
到这里,视频采集参数的“初始化”工作完成了, 回到正题——动态设置视频采集参数。接下来就需要通过getConstrains方法来读取这些参数,代码如下。
//配置参数管理对象 const configManager = { //读取采集配置参数 getConstrains() { /** 读取视频采集参数 start */ let constrains = { audio: false, video: {} } // 读取是否开启音频 constrains.audio = domManager.getDom("useAudio").checked; constrains.video.width = domManager.getDom("videoWidth").value; constrains.video.height = domManager.getDom("videoHeight").value; constrains.video.frameRate = domManager.getDom("frameRate").value; constrains.video.deviceId = domManager.getDom("devicesList").value; /** 读取视频采集参数 end */ return constrains; }, }
上述代码中,需要注意的是“useAudio”这个表单元素是一个“选择按钮”,判断其是否被选中需要判断checked的值。
至此,视频采集参数管理方法(getConstrains)实现完成,接下来考虑何时调用,因为配置采集参数的功能是点击“配置参数”按钮触发的,所以我们需要在事件管理器——eventManager中新增一个监听,监听“更新配置“按钮的点击事件。 代码如下。
// 更新配置事件监听 domManager.getDom("updateConstrains").onclick = () => { // 读取视频采集配置信息 let constrains = configManager.getConstrains() //先关闭摄像头 cameraManager.closeCamera(); // 再根据新的配置参数打开摄像头 cameraManager.openCamera(constrains).then(media => { domManager.getDom("myvideo").srcObject = media; statusManager.openedCamera(); }).catch(err => { console.log("读取媒体失败", error) }) }
上述代码没有什么难点,唯一需要注意的地方就是更新采集配置时需要重新打开摄像头,所以上述代码的逻辑顺序:读取配置参数 ---> 关闭摄像头 ---> 使用新参数重新打开摄像头。至此“更新配置”的功能搞定。
第二个功能:更新滤镜。
先分析下更新滤镜功能,滤镜功能并不是webrtc底层提供的,而是应用层实现的,说得通俗点就是:通过css来实现的。CSS3中预设了多种基础的滤镜特效,开发者可以任意的叠加这些基础滤镜来实现“炫酷”的效果,滤镜的叠加的方式也很简答:filter:滤镜2(参数) 滤镜2(参数) ... 。 所以这个功能可以这么实现:首先、读取滤镜参数;其次, 把滤镜参数拼成CSS的filter参数格式添加到视频元素中,所以同样在configManager对象中新增一个getFilterConfig方法来管理滤镜参数。代码如下。
表单元素的HTML代码:
<div class="constraints-item"> <label>高斯模糊 blur(px)</label><input id="blur" type="number"> </div> <div class="constraints-item"> <label>亮度 brightness(%)</label><input id="brightness" type="number"> </div> <div class="constraints-item"> <label>对比度 contrast(%)</label><input id="contrast" type="number"> </div> <div class="constraints-item"> <label>透明度 opacity(%)</label><input id="opacity" type="number"> </div> <div class="constraints-item"> <label>深褐色 sepia(%)</label><input id="sepia" type="number"> </div>
configManager的getFilterConfig方法,代码如下。
//读取滤镜配置参数 getFilterConfig() { let blur = domManager.getDom("blur").value; let brightness = domManager.getDom("brightness").value; let contrast = domManager.getDom("contrast").value; let opacity = domManager.getDom("opacity").value; let sepia = domManager.getDom("sepia").value; let styleString = ""; if (blur) { styleString += `blur(${blur}px) ` } if (brightness) { styleString += `brightness(${brightness}%) ` } if (contrast) { styleString += `contrast(${contrast}%) ` } if (opacity) { styleString += `opacity(${opacity}%) ` } if (sepia) { styleString += `sepia(${sepia}%) ` } return styleString; }
创建滤镜样式的方法写完了,接下来就要考虑调用的问题,按照需求,我们只需在点击“更新滤镜”按钮时调用,所以在eventManager 的 eventInit新增一个事件监听,代码如下。
// 更新滤镜参数 domManager.getDom("updateFilter").onclick = () => { domManager.getDom("myvideo").style.filter = configManager.getFilterConfig() }
到这里,两个功能已经写完了,下面展示下完整的代码,如下。
HTML:
<fieldset> <legend>视频采集参数</legend> <div class="constraints-item"> <label>是否启用音频</label> <input id="useAudio" type="checkbox"> </div> <div class="constraints-item"> <label>视频宽</label> <input value="300" id="videoWidth" type="number"> </div> <div class="constraints-item"> <label>视频高</label> <input value="200" id="videoHeight" type="number"> </div> <div class="constraints-item"> <label>采集设备选择</label> <select aria-placeholder="请选择采集的设备" name="" id="devicesList"> </select> </div> <div class="constraints-item"> <label>视频帧率</label><input value="30" id="frameRate" type="number"> </div> <div class="operator"> <button disabled id="updateConstrains">更新配置</button> <button disabled id="startRecord">开始录制</button> <button disabled id="stopRecord">结束录制</button> </div> </fieldset> <fieldset> <legend>视频滤镜参数</legend> <div class="constraints-item"> <label>高斯模糊 blur(px)</label><input id="blur" type="number"> </div> <div class="constraints-item"> <label>亮度 brightness(%)</label><input id="brightness" type="number"> </div> <div class="constraints-item"> <label>对比度 contrast(%)</label><input id="contrast" type="number"> </div> <div class="constraints-item"> <label>透明度 opacity(%)</label><input id="opacity" type="number"> </div> <div class="constraints-item"> <label>深褐色 sepia(%)</label><input id="sepia" type="number"> </div> <div class="operator"> <button disabled id="updateFilter">更新滤镜</button> </div> </fieldset>
配置管理对象——configManager:
const configManager = { //读取采集配置参数 getConstrains() { /** 读取视频采集参数 start */ let constrains = { audio: false, video: {} } // 读取是否开启音频 constrains.audio = domManager.getDom("useAudio").checked; constrains.video.width = domManager.getDom("videoWidth").value; constrains.video.height = domManager.getDom("videoHeight").value; constrains.video.frameRate = domManager.getDom("frameRate").value; constrains.video.deviceId = domManager.getDom("devicesList").value; /** 读取视频采集参数 end */ return constrains; }, //读取滤镜配置参数 getFilterConfig() { let blur = domManager.getDom("blur").value; let brightness = domManager.getDom("brightness").value; let contrast = domManager.getDom("contrast").value; let opacity = domManager.getDom("opacity").value; let sepia = domManager.getDom("sepia").value; let styleString = ""; if (blur) { styleString += `blur(${blur}px) ` } if (brightness) { styleString += `brightness(${brightness}%) ` } if (contrast) { styleString += `contrast(${contrast}%) ` } if (opacity) { styleString += `opacity(${opacity}%) ` } if (sepia) { styleString += `sepia(${sepia}%) ` } return styleString; } }
摄像头管理对象cameraManager,新增“enumerateVideoDevices”方法,代码如下:
// 摄像头管理对象 const cameraManager = { mediaStream: null, // 摄像头列表 cameraList: [], //开启摄像头 async openCamera(mediaStreamconstrains) { let media = await navigator.mediaDevices.getUserMedia(mediaStreamconstrains); this.enumerateVideoDevices(); this.mediaStream = media; return media; }, // 关闭摄像头 closeCamera() { if (this.mediaStream) { let trackes = this.mediaStream.getTracks(); if (trackes) { trackes.forEach(track => { if (track) { track.stop(); } }); } this.mediaStream = null; } }, // 加载摄像头供用户选择 async enumerateVideoDevices() { let devices = await navigator.mediaDevices.enumerateDevices() let devicesListDom = domManager.getDom("devicesList"); devicesListDom.innerHTML = ""; // 清空子元素 if (devices) { for (let d of devices) { if (d && (d.kind == 'videoinput')) { let element = document.createElement("option"); if (element) { element.value = d.deviceId; element.innerHTML = d.label; devicesListDom.appendChild(element); } } } } }, }
事件管理对象——eventManager,新增了两个监听:监听updateConstrains按钮、监听updateFilter按钮。代码如下:
// 事件方法管理对象 const eventManager = { // 初始化按钮事件的监听 eventInit() { // 打开摄像头 domManager.getDom("openCamera").onclick = () => { let constrains = configManager.getConstrains() cameraManager.openCamera(constrains).then(media => { domManager.getDom("myvideo").srcObject = media; statusManager.openedCamera(); }).catch(err => { console.log("读取媒体失败", error) }) } // 关闭摄像头 domManager.getDom("closeCamera").onclick = () => { cameraManager.closeCamera() statusManager.systemReady(); } // 更新配置 domManager.getDom("updateConstrains").onclick = () => { let constrains = configManager.getConstrains() //先关闭摄像头 cameraManager.closeCamera(); // 再根据新的配置参数打开摄像头 cameraManager.openCamera(constrains).then(media => { domManager.getDom("myvideo").srcObject = media; statusManager.openedCamera(); }).catch(err => { console.log("读取媒体失败", error) }) } // 更新滤镜参数 domManager.getDom("updateFilter").onclick = () => { domManager.getDom("myvideo").style.filter = configManager.getFilterConfig() } }, }
最后做下总结:本节没有新讲任何知识点,而是实现了更新采集配置和更新滤镜配置两个功能,为读者增加了实战的经验,为下一节的的录制功能做准备。下一节,我们来实现本软件的最后一个功能:视频的录制和保存。