赞
踩
近几年直播行业飞速发展,但是由于Web端这方面功能的长时间缺失,使得直播端以客户端为主;WebRTC 的出现使得网页也可以成为直播端。那么究竟WebRTC是什么呢?
WebRTC,即Web Real-Time Communication,web实时通信技术。简单地说就是在web浏览器里面引入实时通信,包括音视频通话等,它使得实时通信变成一种标准功能,任何Web应用都无需借助第三方插件和专有软件,而是通过JavaScript API即可完成;而且WebRTC提供了视频会议的核心技术,包括音视频的采集、编解码、网络传输、展示等功能,还支持跨平台,包括主流的PC和移动端设备。
下面介绍下需要用到的几个API:
我们可以通过调用navigator.mediaDevices.getUserMedia(constraints)去初始化一个本地的音视频流,然后把直播流通过video标签播放。代码如下:
html:
- <div id="container">
- <video id="gum-local" autoplay playsinline></video>
- <button id="showVideo">Open camera</button>
- <button id="switchVideo">switch camera</button>
- </div>
js:
- 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));
示例效果:
当然,如果有多个设备,就需要考虑设备选择和设备切换的问题。那就需要用到下面的这个API。
我们看看如何用原生的Web API去获取设备(以下示例代码可适用于Chrome,其他浏览器暂未测试;具体浏览器兼容性可参考官方文档,本文档底部有链接)。
如果枚举成功将会返回一个包含MediaDeviceInfo实例的数组,它包含了可用的多媒体输入输出设备的信息。
下面是调用代码示例。
- navigator.mediaDevices.enumerateDevices().then((devices) => {
- console.log(devices, '-----enumerateDevices------');
- });
设备参数说明:
获取的所有设备截图(未授权):
deviceInfo--所有设备信息(未授权)
videoinput已授权截图:
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那边给的答复是:因为缓存问题,会以第一次推流设置的参数为准,将会在下个版本中修复。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。