背景

近几年直播行业飞速发展,但是由于Web端这方面功能的长时间缺失,使得直播端以客户端为主;WebRTC 的出现使得网页也可以成为直播端。那么究竟WebRTC是什么呢?

WebRTC,即Web Real-Time Communication,web实时通信技术。简单地说就是在web浏览器里面引入实时通信,包括音视频通话等,它使得实时通信变成一种标准功能,任何Web应用都无需借助第三方插件和专有软件,而是通过JavaScript API即可完成;而且WebRTC提供了视频会议的核心技术,包括音视频的采集、编解码、网络传输、展示等功能,还支持跨平台,包括主流的PC和移动端设备。

下面介绍下需要用到的几个API:

getUserMedia

我们可以通过调用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,其他浏览器暂未测试;具体浏览器兼容性可参考官方文档,本文档底部有链接)。

navigator.mediaDevices.enumerateDevices()

如果枚举成功将会返回一个包含MediaDeviceInfo实例的数组,它包含了可用的多媒体输入输出设备的信息。

下面是调用代码示例。

navigator.mediaDevices.enumerateDevices().then((devices) => {console.log(devices, '-----enumerateDevices------');
});

设备参数说明:

  1. deviceId:设备id,具有唯一性
  2. groupId:设备组id,不具有唯一性
  3. kind:设备类别(audioinput:音频输入设备,audiooutput:音频输出设备,videoinput:视频输入设备)
  4. label:设备名称(未经过授权允许的设备,label值为空,授权允许后可拿到label的值,如下两图所示)

获取的所有设备截图(未授权):

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下,电脑外接摄像头后拔出设备,此时还有可能获取到拔出的设备信息,在进行切换的时候会有问题,可以采用在页面进行友好提示处理这种情况。

屏幕共享

MediaDevices.getDisplayMedia

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的点对点连接的过程为:

  1. 呼叫端给接收端发送一个offer信息。在发送给接收端之前先调用setLocalDescription存储本地offer描述。
  2. 接收端收到offer消息后,先调用setRemoteDescription存储远端offer,再创建一个answer信息给呼叫端。

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);// 接收到answerlocalConnection.setRemoteDescription(answer);})
});

至此我们已经介绍完毕浏览器设备检测采集和屏幕分享的基本流程,但是光有这些可还远远不够,一套完整的直播体系包括音视频采集、处理、编码和封装、推流到服务器、服务器流分发、播放器流播放等等。如果想节省开发成本,可以使用第三方SDK。下面简单介绍下使用声网SDK发起直播的流程。

浏览器要求:

  1. Chrome 58+
  2. Firefox 56+
  3. Safari 11+(屏幕共享不可用)
  4. Opera 45+(屏幕共享不可用)
  5. QQ 10+(屏幕共享不可用)
  6. 360 安全浏览器 9.1+(屏幕共享不可用)

设备检测

调用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 插件idmediaSource: '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, // 视频帧左上角的横轴位置,默认为0y: 0, // 视频帧左上角的纵轴位置,默认为0width: 1280, // 视频帧宽度,默认为640height: 720, // 视频帧高度,默认为360zOrder: 0, // 视频帧所处层数;取值范围为 [0,100];默认值为 0,表示该区域图像位于最下层alpha: 1.0, // 视频帧的透明度,默认值为 1.0uid: 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那边给的答复是:因为缓存问题,会以第一次推流设置的参数为准,将会在下个版本中修复。

摆脱客户端?网页发起直播势在必行!相关推荐

  1. 帮助中心 html页面,网页端直播

    开始网页直播前,系统会自动检测摄像头.麦克风等设备的可用性,检测正常后,点击[去直播],开始网页直播. (一)网页端直播界面 (二)上课/下课 点击 ,将开始课堂直播. 点击 ,将结束课堂直播. (三 ...

  2. aria2 32bit Android,【各版本整合】32/64位Aria2 Tools - 支持RPC协议 AriaNG(客户端+网页版)...

    [各版本整合]32/64位Aria2 Tools - 支持RPC协议  AriaNG(客户端+网页版) 前言 话说: 然后 我翻了一下我的硬盘. 找到了这个32位的. 不太懂这方面知识的坛友可直接下载 ...

  3. 监控摄像头如何进行互联网网页实时直播

    什么是网络监控摄像头 网络摄像头,也就是IP camera, 简称IPcam.是传统摄像头与网络视频技术相结合的新一代产品,除了具备一般传统摄像机所有的图像捕捉功最新款网络摄像头能外,机内还内置了数字 ...

  4. 钉钉如何发起直播?钉钉电脑版直播发起方法简述

    本篇文章由 泉州SEO www.234yp.com 整理发布,word办公软件 www.234yp.com/Article/155237.html 谢谢合作! word办公软件 之前我们简单介绍一下钉 ...

  5. SRS+OBS实现网页HLS直播功能

    SRS+OBS实现网页HLS直播功能 过程简述:OBS获取场景和源录像,将其以rtmp标准上传至SRS服务器,SRS服务器处理将其转换,以hls标准存储在服务器中,前端页面通过访问其URL获取资源,实 ...

  6. go语言学习第八天==》mysql数据库增删改查、用go语言 客户端(client)发起htttp get请求,post请求,postForm请求,Head请求,Do请求

    go语言学习第八天==>mysql数据库增删改查.用go语言写 客户端(client)发起htttp get请求,post请求,postForm请求,Head请求,Do请求 引包 import的 ...

  7. 浅谈云豹直播系统中关于Android客户端网页加载实现

    为了方便客户快捷地更换直播App内的展示内容,云豹直播系统内置了Web网页,而这种功能主要就是使用本文将要介绍的WebView这一组件实现的. WebView是一个展现web页面的控件,它有以下作用: ...

  8. 网页视频直播、微信视频直播技术解决方案:EasyNVR与EasyDSS流媒体服务器组合之区分不同场景下的直播接入需求...

    背景分析 熟悉EasyNVR产品的朋友们都知道,EasyNVR不仅可以独成体系,而且还可以跟其他系列产品相配合,形成各种不同类型的解决方案,满足各种不同应用场景的实际需求.针对很多设备现场没有固定公网 ...

  9. lwip+freeRTOS 故障容错 客户端主动发起连接

    1.简单的需求 STM32终端为tcp客户端,主动向TCP服务器发起连接,然后进行通信. 流程:STM32网卡初始化-----lwIP初始化--------DHCP-------------creat ...

最新文章

  1. java.lang.ClasNotFoundException:Didnt findclass on path:DexPathList[[zip file
  2. python selenium xpath_python+selenium十四:xpath和contains模糊匹配
  3. cisco路由器针对进行ip限速
  4. 前端获取后端传来的session_java后台如何获取,前台传来的表单数据
  5. Docker+Jenkins+Gitlab+Django应用部署实践
  6. ffmpeg speex转换为mp3或者aac
  7. 前端学习(2478):请求提交
  8. asp.net的处理机制(.ashx/.aspx)
  9. L2-016 愿天下有情人都是失散多年的兄妹(DFS)
  10. Java多线程系列--“JUC原子类”
  11. 股票基金历史数据下载接口集合
  12. mysql个人记账系统_个人记账系统
  13. springboot中使用thymeleaf片段引入出现500错误(易错)
  14. 【Solr】之使用结巴分词模拟搜索商品1
  15. 学习机器学习开始的一些别人的看法
  16. Cellular Pro简介
  17. 浅谈卡尔曼滤波(Kalman Filter)(一)
  18. 禁用和启用google翻译
  19. 电脑搜索不到部分wifi,搜索不到部分2.4G频率的wif,手机开热点电脑搜不到wifi。
  20. HBC Network重塑分布式存储,抢滩区块链数据时代

热门文章

  1. 通过getPixel();和通过bmp.getPixels();方法遍历整张图片的效率比较。
  2. HTML网页字体动态显示
  3. KingbaseES V8R6 集群运维系列 -- 命令行部署repmgr管理集群+switchover测试
  4. freemarker导出Word文档并在其中插入图片
  5. KingbaseES 中的xmin,xmax等系统字段说明
  6. 深圳水上乐园有哪些 而且便宜还好玩
  7. 分别用线性规划和动态规划求解打家劫舍问题(力扣198)
  8. 单片机工程师如何继续提升自己?
  9. 中国银行风险内控与“三道防线”
  10. visio如何找到画线工具