上一节中我们已经搭建出了用于操作的环境,这一节我们要实现的一个小目标,就是将电脑摄像头拍到的内容实时显示到网页上。同时我们一起学习下原理,并做一些小拓展。
操作环境
- 效果展示: Chrome 83
- IDE: Hbuilder X
实现效果
这一节的完整代码可以在上一节下载的step-01
目录中找到。
修改index.html
内容如下
<!DOCTYPE html>
<html>
<head>
<title>Realtime communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<h1>Realtime communication with WebRTC</h1>
<video autoplay playsinline></video>
<script src="js/main.js"></script>
</body>
</html>
添加了一个video
标签,以及一个script
标签引入js/main.js
这个脚本。
接着去修改引入的这个main.js
脚本
'use strict';
// On this codelab, you will be streaming only video (video: true).
const mediaStreamConstraints = {
video: true,
};
// Video element where stream will be placed.
const localVideo = document.querySelector('video');
// Local stream that will be reproduced on the video.
let localStream;
// Handles success by adding the MediaStream to the video element.
function gotLocalMediaStream(mediaStream) {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
}
// Handles error by logging a message to the console with the error message.
function handleLocalMediaStreamError(error) {
console.log('navigator.getUserMedia error: ', error);
}
// Initializes media stream.
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
之后刷新下测试网页http://127.0.0.1:8887
,页面会询问是否允许打开摄像头,选择允许,然后就可以看到如下效果,我这里处于隐私考虑给我自己的显示内容加了马赛克
如果没有显示出摄像头内容,建议检查下浏览器版本。
几个概念
HTML5中的Audio和Video API
我们可以使用<video>
和<audio>
在h5页面中嵌入视频和音频,并且可以通过类似<video controls></video>
这样的方式给音视频加上默认的控制器,但是大多数操作还是用鼠标完成,灵活度非常不够。
同时,h5还引入了一个叫做HTMLMediaElement
的API接口提供通过程序来控制页面中音视频播放器的方法,例如HTMLMediaElement.play()
和HTMLMediaElement.pause()
等等。另外两个接口HTMLVideoElement
和HTMLAudioElement
都继承自HTMLMediaElement
,分别适用于<audio>
和<video>
两个标签,我们这里就是使用<video>
标签来进行的API操作。
src和srcObject属性
有必要区分下HTMLMediaElement.src
和HTMLMediaElement.srcObject
这两个属性。前者是标签的一个属性,可以赋值或者返回一个url,会显示在页面中;后者是一个HTMLMediaElement
接口的一个属性,可以赋值或者返回一个MediaStream
对象,或者是MediaSource
、Blob
以及File
对象,并不会显示在页面上。
截至2020年3月,只有Safari支持对srcObject赋值除了MediaStream以外的其余类型的对象,所以如果要使用其他浏览器进行操作,建议通过
URL.createObjectURL()
生成一个URL然后赋值给src
MediaStream对象
MediaStream代表一个流媒体对象,一个对象包含好几个track,例如音频track以及视频track。有两种方式可以创造MediaStream对象,要么通过MediaStream()
构造方法,要么通过调用MediaDevices.getUserMedia()
,本小节使用的就是第二种方法。
实现原理
首先来看下面的这段代码
const mediaStreamConstraints = {
video: true,
// video: { width: 1280, height: 720 },
};
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
正如上面所说的那样,这里调用navigator.mediaDevices.getUserMedia()
方法。首次在某网页使用这个方法的话,浏览器还会像下面这样询问用户的授权
过后用户还可以在浏览器的设置中去个性化或者删除网站可使用的权限。
如果用户同意授权,该方法会返回一个Promise
对象,执行异步操作,如果resolve,会返回一个MediaStream对象,供gotLocalMediaStream函数去使用。如果用户拒绝授权,该方法或返回一个错误。
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
/* use the stream */
})
.catch(function(err) {
/* handle the error */
});
该方法接受一个参数constraints,用于针对audio和video分别设置是否可以被获取。当然除了布尔值,还可以类似下面这样设定想要获取的视频分辨率
{
video: { width: 1280, height: 720 }
}
浏览器会尽力去满足要求,但是如果条件不允许也会返回其余规格的视频。更多的设置可以参考MediaDevices.getUserMedia()
。
获取到了摄像头的流媒体以后,再来看下一段代码就比较好理解了
const localVideo = document.querySelector('video');
let localStream;
function gotLocalMediaStream(mediaStream) {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
}
promise在resolve之后会传递mediaStream对象到这个函数,函数内再将该对象传递给页面内的video标签的srcObject接口,完成将摄像头内容在页面展示的功能。
这里的全局变量localStream
是为了方便在console里面对视频对象进行一些操作而定义的。
扩展
视频分辨率切换
视频分辨率通过MediaDevices.getUserMedia()
的constraints参数来进行修改,在页面放置几个按钮,点击不同按钮传递不同的参数去调用该方法即可达到目的。但是要注意,因为流媒体是不支持动态切换的,必须要先停掉当前的视频源重新获取才可以进行分辨率切换。
修改h5页面如下
<!DOCTYPE html>
<html>
<head>
<title>Realtime communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<h1>Realtime communication with WebRTC</h1>
<button type="button" id="qvga">QVGA</button>
<button type="button" id="vga">VGA</button>
<button type="button" id="hd">HD</button>
<p id="info"></p>
<video autoplay playsinline></video>
<script src="js/choose.js"></script>
</body>
</html>
在刚才的基础上添加了3个按钮用来切换分辨率以及一个p标签显示当前视频的分辨率。所引入的choose.js
文件如下
const qvga = document.querySelector('#qvga');
const vga = document.querySelector('#vga');
const hd = document.querySelector('#hd');
const info = document.querySelector('#info');
const localVideo = document.querySelector('video');
let localStream;
var qvgaConstraints = {
video: {
width: 320,
height: 180
}
};
var vgaConstraints = {
video: {
width: 640,
height: 360
}
};
var hdConstraints = {
video: {
width: 1280,
height: 720
}
};
function gotLocalMediaStream(mediaStream) {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
}
function getMedia(mediaStreamConstraints){
// 这里是重点,必须要先停止才可以
if(localStream){
localStream.getVideoTracks()[0].stop();
}
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
}
function handleLocalMediaStreamError(error) {
console.log('navigator.getUserMedia error: ', error);
}
qvga.onclick=function(){
getMedia(qvgaConstraints);
}
vga.onclick=function(){
getMedia(vgaConstraints);
}
hd.onclick=function(){
getMedia(hdConstraints);
}
localVideo.onplay=function(){
// 必须要设置onplay事件,因为视频没播放的话,宽和高都是0
// videoWidth vs width,一个是视频的pixel,一个是标签的css值
info.innerText=localVideo.videoWidth+'x'+localVideo.videoHeight+'px';
}
这里对3个按钮都添加了点击事件,点击的话就执行getMedia
函数。这个函数里面首先判断全局变量localStream
是否有内容,如果有内容就先localStream.getVideoTracks()[0].stop()
停掉。因为stream不支持热切换,所以必须得先停掉。同时还对video标签添加了播放事件,一旦视频流进来就显示视频的宽和高。注意这里使用的是videoWidth
和videoHeight
而不是css中的width
和height
。
效果如下
视频源切换
Navigator.mediaDevices
会返回一个MediaDevices
对象,前面我们用了该对象的getUserMedia()
方法去获取用户的视频,其实还有另外两个方法也非常常用
-
enumerateDevices()
该方法会返回一个promise,resolve为一个array,每个元素是当前设备上检测到的输入和输出设备。如果要切换视频源,就必须要用到这个方法直到一共有多少可用视频源
-
getDisplayMedia()
该方法会让用户选择全部或者部分屏幕用于录屏,然后和音视频一样也是返回一个promise对象,resolve一个MediaStream对象供使用,后面传递录屏的时候会用到这个方法
修改下index.html
如下
<!DOCTYPE html>
<html>
<head>
<title>Realtime communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
<style type="text/css">
select {
width: 200px;
}
video {
width: 500px;
height: 300px;
margin-top: 20px;
display: block;
}
</style>
</head>
<body>
<h1>Realtime communication with WebRTC</h1>
<!-- 选择视频源 -->
Video:
<select name="" id="video">
</select>
Audio:
<select name="" id="audio">
</select>
<video autoplay playsinline></video>
<script src="js/input.js"></script>
</body>
</html>
这里加了两个下拉选择框,但是没有加任何选项内容,具体选项在js中动态加载。同时元素多了还简单加了点css样式。
引入的input.js
内容如下
const localVideo = document.querySelector('video');
var videoSelect = document.querySelector('select#video');
var audioSelect = document.querySelector('select#audio');
let localStream;
let constraint;
function getDevices(){
return navigator.mediaDevices.enumerateDevices()
}
function gotDevices(devices){
devices.forEach(function(device){
if(device['kind'] == 'audioinput'){
let opt = document.createElement('option');
opt.value = device['deviceId'];
opt.innerText = device['label'];
audioSelect.appendChild(opt);
}else if(device['kind'] == 'videoinput'){
let opt = document.createElement('option');
opt.value = device['deviceId'];
opt.innerText = device['label'];
videoSelect.appendChild(opt);
}
})
changeConstraint();
}
function changeConstraint(){
constraint = {video: {deviceId: videoSelect.value}, audio: {deviceId: audioSelect.value}};
getMedia(constraint);
}
function gotLocalMediaStream(mediaStream) {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
}
function getMedia(mediaStreamConstraints){
// 这里是重点,必须要先停止才可以
if(localStream){
localStream.getVideoTracks()[0].stop();
}
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
}
function handleLocalMediaStreamError(error) {
console.log('navigator.getUserMedia error: ', error);
}
getDevices().then(gotDevices)
audioSelect.onchange=changeConstraint;
videoSelect.onchange=changeConstraint;
这里首先是通过getDevices().then(gotDevices)
对下拉框添加了内容,因为默认的音频和视频输入设备都是第一个,所以直接可以使用,如下
function getDevices(){
return navigator.mediaDevices.enumerateDevices()
}
function gotDevices(devices){
devices.forEach(function(device){
if(device['kind'] == 'audioinput'){
let opt = document.createElement('option');
opt.value = device['deviceId'];
opt.innerText = device['label'];
audioSelect.appendChild(opt);
}else if(device['kind'] == 'videoinput'){
let opt = document.createElement('option');
opt.value = device['deviceId'];
opt.innerText = device['label'];
videoSelect.appendChild(opt);
}
})
changeConstraint();
}
第一个函数返回一个promise对象,resolve以后的array传递到第二个函数,通过forEach
的循环,将音视频的输入设备分别添加到各自的下拉框选项中。最后调用changeConstraint
函数去根据当前下拉框的内容获取音视频并且传递到video
标签。
然后是给两个下拉框添加了onchange
事件,每次内容变化都会重新去获取
audioSelect.onchange=changeConstraint;
videoSelect.onchange=changeConstraint;
最后的效果如下
总结
总结下这一节的知识点
navigator.mediaDevices.getUserMedia
方法可以获取音视频,传递的唯一一个参数是字典形式表示的对音视频的限制,例如来源,分辨率等等navigator.mediaDevices
还有几个方法可以用来获取用户屏幕或者是设备列表- 但是注意,上面返回的内容都是promise对象,通过then去将resolve后的内容进行利用
- stream要传递给video标签的srcObject接口,而不是src属性
- stream不能直接操作,所以赋值给一个全局变量
localMedia
,通过这个全局变量在js中操作已经获得的stream,例如停止、静音或者获取播放时长等等 - 当然,我们还可以对显示出来的视频通过css的filter字段加一些滤镜,大家可以自己玩玩
这一节我们成功获取了用户设备的音视频,并且做了很多个性化的东西,例如分辨率,音视频源,并且显示在了网页上。但是毕竟现在一直是在本地操作,下一节我们就要试着将获取的内容传递出去了。
参考
- Google的官方WebRTC培训网站:https://codelabs.developers.google.com/codelabs/webrtc-web/#0
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。