近几年直播行业飞速发展,可是因为Web端这方面功能的长时间缺失,使得直播端以客户端为主;WebRTC 的出现使得网页也能够成为直播端。那么究竟WebRTC是什么呢?javascript
WebRTC,即Web Real-Time Communication,web实时通讯技术。简单地说就是在web浏览器里面引入实时通讯,包括音视频通话等,它使得实时通讯变成一种标准功能,任何Web应用都无需借助第三方插件和专有软件,而是经过JavaScript API便可完成;并且WebRTC提供了视频会议的核心技术,包括音视频的采集、编解码、网络传输、展现等功能,还支持跨平台,包括主流的PC和移动端设备。html
下面介绍下须要用到的几个API:java
咱们能够经过调用navigator.mediaDevices.getUserMedia(constraints)去初始化一个本地的音视频流,而后把直播流经过video标签播放。代码以下: web
html:chrome
<div id="container"> <video id="gum-local" autoplay playsinline></video> <button id="showVideo">Open camera</button> <button id="switchVideo">switch camera</button> </div>
js:json
const constraints = { audio: false, video: true }; async function init(e) { try { const stream = await navigator.mediaDevices.getUserMedia(constraints); const video = document.querySelector('video'); video.srcObject = stream; } catch (e) { console.log(e, 'stream init error'); } } document.querySelector('#showVideo').addEventListener('click', (e) => init(e));
示例效果: canvas
固然,若是有多个设备,就须要考虑设备选择和设备切换的问题。那就须要用到下面的这个API。windows
咱们看看如何用原生的Web API去获取设备(如下示例代码可适用于Chrome,其余浏览器暂未测试;具体浏览器兼容性可参考官方文档,本文档底部有连接)。数组
若是枚举成功将会返回一个包含MediaDeviceInfo实例的数组,它包含了可用的多媒体输入输出设备的信息。浏览器
下面是调用代码示例。
navigator.mediaDevices.enumerateDevices().then((devices) => { console.log(devices, '-----enumerateDevices------'); });
设备参数说明:
获取的全部设备截图(未受权):
videoinput已受权截图:
获取到设备列表后,可设置navigator.mediaDevices.getUserMedia(constraints)的constraints参数选择所用设备。
const { audioList, videoList } = await getDevices(); const constraints = { audio: { deviceId: audioList[0].deviceId }, video: { deviceId: videoList[0].deviceId } }; navigator.mediaDevices.getUserMedia(constraints);
然而,咱们在更换deviceId切换设备的时候发现一些异常状况。在某些deviceId之间切换时,摄像头画面或者是麦克风采集处并无发生变化。进一步调试发现,这些切换后没有发生变化的deviceId都具备相同的groupId。所以,相同groupId下的设备,选择一个用于切换便可。
筛选麦克风、摄像头设备示例:
function getDevices() { return new Promise((resolve) => { navigator.mediaDevices.enumerateDevices().then((devices) => { const audioGroup = {}; const videoGroup = {}; const cameraList = []; const micList = []; devices.forEach((device, index) => { if ((!device.groupId || !audioGroup[device.groupId]) && device.kind === 'audioinput') { micList.push(device); audioGroup[device.groupId] = true; } if ((!device.groupId || !videoGroup[device.groupId]) && device.kind === 'videoinput') { cameraList.push(device); videoGroup[device.groupId] = true; } }); resolve({ cameraList, micList }); }); }); }
注意:在Chrome下,电脑外接摄像头后拔出设备,此时还有可能获取到拔出的设备信息,在进行切换的时候会有问题,能够采用在页面进行友好提示处理这种状况。
Chrome 72+、Firefox 66+版本已经实现了WebRTC规范中的MediaDevices.getDisplayMedia,具有屏幕共享功能。
navigator.mediaDevices.getDisplayMedia({ video: true, audio: false }).then(stream => { video.srcObject = stream; }).catch(err => { console.error(err); });
示例效果:
对于Chrome 72如下的版本,想要实现屏幕共享的功能须要借助Chrome插件去获取screen(显示器屏幕)、application windows(应用窗口)和browser tabs(浏览器标签页)。 Chrome插件:由manifest.json和script.js组成。
manifest.json 填入一些基本数据。
background中scripts传入需执行的js文件。
添加permissions: ['desktopCapture'],用来开启屏幕共享的权限。
externally_connectable用来声明哪些应用和网页能够经过runtime.connect
和runtime.sendMessage
链接到插件。
{ "manifest_version": 2, "name": "Polyv Web Screensharing", "permissions": [ "desktopCapture" ], "version": "0.0.1", "background": { "persistent": false, "scripts": [ "script.js" ] }, "externally_connectable": { "matches": ["*://localhost:*/*"] } }
script.js
// script.js chrome.runtime.onMessageExternal.addListener( function(request, sender, sendResponse) { if (request.getStream) { // Gets chrome media stream token and returns it in the response. chrome.desktopCapture.chooseDesktopMedia( ['screen', 'window', 'tab'], sender.tab, function(streamId) { sendResponse({ streamId: streamId }); }); return true; // Preserve sendResponse for future use } } );
在页面中开始屏幕共享。经过chrome.runtime.sendMessage发送消息到Chrome插件调起屏幕共享。获取到streamId后,经过mediaDevices.getUserMedia获得stream。
const EXTENSION_ID = '<EXTENSION_ID>'; const video = $('#videoId'); chrome.runtime.sendMessage(EXTENSION_ID, { getStream: true }, res => { console.log('res: ', res); if (res.streamId) { navigator.mediaDevices.getUserMedia({ video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: res.streamId } } }).then((stream) => { video.srcObject = stream; video.onloadedmetadata = function(e) { video.play(); }; }) } else { // 取消选择 } });
而Firefox 66版本如下,不须要像Chrome借助插件才能实现屏幕共享。Firefox 33以后能够直接经过使用mediaDevices.getUserMedia,指定约束对象mediaSource为screen、window、application来实现屏幕共享。不过在Firefox中,一次只能指定一种mediaSource。
navigator.mediaDevices.getUserMedia({ video: { mediaSource: 'window' } }).then(stream => { video.srcObject = stream; });
WebRTC的RTCPeerConnection能够创建点对点链接通讯,RTCDataChannel提供了数据通讯的能力。
WebRTC的点对点链接的过程为:
RTCDataChannel提供了send方法和message事件。使用起来与WebSocket相似。
因为没有服务器,如下代码为呼叫端和接收端在同一页面上,RTCPeerConnection对象之间是如何进行数据交互。
// 建立数据通道 sendChannel = localConnection.createDataChannel('通道名称', options); sendChannel.binaryType = 'arraybuffer'; sendChannel.onopen = function() { sendChannel.send('Hi there!'); }; sendChannel.onmessage = function(evt) { console.log('send channel onmessage: ', evt.data); }; // 远端接收实例 remoteConnection = new RTCPeerConnection(servers); remoteConnection.onicecandidate = function(evt) { if (evt.candidate) { localConnection.addIceCandidate(new RTCIceCandidate(evt.candidate)); } }; // 当一个RTC数据通道已被远端调用createDataChannel()添加到链接中时触发 remoteConnection.ondatachannel = function() { const receiveChannel = event.channel; receiveChannel.binaryType = 'arraybuffer'; //接收到数据时触发 receiveChannel.onmessage = function(evt) { console.log('onmessage', evt.data); // log: Hi there! }; receiveChannel.send('Nice!'); }; // 监听是否有媒体流 remoteConnection.onaddstream = function(e) { peerVideo.srcObject = e.stream; }; localConnection.addStream(stream); // 建立呼叫实例 localConnection.createOffer().then(offer => { localConnection.setLocalDescription(offer); remoteConnection.setRemoteDescription(offer); remoteConnection.createAnswer().then(answer => { remoteConnection.setLocalDescription(answer); // 接收到answer localConnection.setRemoteDescription(answer); }) });
至此咱们已经介绍完毕浏览器设备检测采集和屏幕分享的基本流程,可是光有这些可还远远不够,一套完整的直播体系包括音视频采集、处理、编码和封装、推流到服务器、服务器流分发、播放器流播放等等。若是想节省开发成本,可使用第三方SDK。下面简单介绍下使用声网SDK发起直播的流程。
浏览器要求:
调用AgoraRTC.getDevices获取当前浏览器检测到的全部可枚举设备,kind为'videoinput'是摄像头设备,kind为'audioinput'是麦克风设备,而后经过createStream初始化一个本地的流。 获取设备:
AgoraRTC.getDevices((devices) => { const audioGroup = {}; const videoGroup = {}; const cameraList = []; const micList = []; devices.forEach((device, index) => { if ((!device.groupId || !audioGroup[device.groupId]) && device.kind === 'audioinput') { micList.push(device); audioGroup[device.groupId] = true; } if ((!device.groupId || !videoGroup[device.groupId]) && device.kind === 'videoinput') { cameraList.push(device); videoGroup[device.groupId] = true; } }); return { cameraList, micList }; });
初始化本地流:
// uid:自定义频道号,cameraId设备Id const stream = AgoraRTC.createStream({ streamID: uid, audio: false, video: true, cameraId: cameraId, microphoneId: microphoneId }); stream.init(() => { // clientCamera <div id="clientCamera" ></div> stream.play('clientCamera', { muted: true }); }, err => { console.error('AgoraRTC client init failed', err); });
stream.init()初始化直播流;若是当前浏览器摄像头权限为禁止,则调用失败,可捕获报错Media access NotAllowedError: Permission denied; 若摄像头权限为询问,浏览器默认弹窗是否容许使用摄像头,容许后调用play()可看到摄像头捕获的画面。 若是不传入cameraId,SDK会默认获取到设备的deviceId,若是权限是容许,一样会显示摄像头画面。
顺利拿到cameraId和microphoneId后就能够进行直播。经过SDK提供的createStream建立一个音视频流对象。执行init方法初始化成功以后,播放音视频(见上文)。最后经过client发布流以及推流到CDN(见下文)。
Web 端屏幕共享,经过建立一个屏幕共享的流来实现的。Chrome屏幕共享须要下载插件,在建立的流的时候还须要传入插件的extensionId。
const screenStream = AgoraRTC.createStream({ streamID: <uid>, audio: false, video: false, screen: true, extensionId: <extensionId>, // Chrome 插件id mediaSource: 'screen' // Firefox });
经过AgoraRTC.createStream建立的音视频流,经过publish发送到第三方服务商的SD-RTN(软件定义实时传输网络)。
client.publish(screenStream, err => { console.error(err); });
别的浏览器能够经过监听到stream-added事件,经过subscribe订阅远端音视频流。
client.on('stream-added', evt => { const stream = evt.stream; client.subscribe(stream, err => { console.error(err); }); });
再经过startLiveStreaming推流到CDN。
// 编码 client.setLiveTranscoding(<coding>); client.startLiveStreaming(<url>, true)
在推摄像头流的时候,关闭摄像头,须要推一张占位图。这个时候先用canvas画图,而后用WebRTC提供的captureStream捕获静态帧。再调用getVideoTracks,制定AgoraRTC.createStream的videoSource为该值。视频源如来自 canvas,须要在 canvas 内容不变时,每隔 1 秒从新绘制 canvas 内容,以保持视频流的正常发布。
const canvas = document.createElement('canvas'); renderCanvas(canvas); setInterval(() => { renderCanvas(canvas); }, 1000); canvasStream = canvas.captureStream(); const picStream = AgoraRTC.createStream({ streamID: <uid>, video: true, audio: false, videoSource: canvasStream.getVideoTracks()[0] }); // 画图 function renderCanvas(canvas) { ... }
一个client只能推一个流,因此在进行屏幕共享的时候,须要建立两个client,一个发送屏幕共享流,一个发送视频流。屏幕共享流的video字段设为false。视频流的video字段设为true。而后先经过setLiveTranscoding合图再推流。
const users = [ { x: 0, // 视频帧左上角的横轴位置,默认为0 y: 0, // 视频帧左上角的纵轴位置,默认为0 width: 1280, // 视频帧宽度,默认为640 height: 720, // 视频帧高度,默认为360 zOrder: 0, // 视频帧所处层数;取值范围为 [0,100];默认值为 0,表示该区域图像位于最下层 alpha: 1.0, // 视频帧的透明度,默认值为 1.0 uid: 888888, // 旁路推流的用户 ID }, { x: 0, y: 0, width: 1280, height: 720, zOrder: 1, alpha: 1.0, uid: 999999 } ]; var liveTranscoding = { width: 640, height: 360, videoBitrate: 400, videoFramerate: 15, lowLatency: false, audioSampleRate: AgoraRTC.AUDIO_SAMPLE_RATE_48000, audioBitrate: 48, audioChannels: 1, videoGop: 30, videoCodecProfile: AgoraRTC.VIDEO_CODEC_PROFILE_HIGH, userCount: user.length, backgroundColor: 0x000000, transcodingUsers: users, }; client.setLiveTranscoding(liveTranscoding);
由于业务需求是摄像头和屏幕共享能够切换,摄像头和屏幕共享的分辨率和码率均不相同,屏幕共享须要更高的分辨率和码率。可是开发中发现切换时设置码率无效。SDK那边给的答复是:由于缓存问题,会以第一次推流设置的参数为准,将会在下个版本中修复。
参考文献:
MediaDevices.getUserMedia()
MedaiDevices.enumerateDevices()
HTMLMediaElement
MediaDevices/getDisplayMedia