起因

当前程序是以electron + vue作为前端,go作为后端http主服务。最近新来了一个需求,需要能够使用多个PC USB摄像头进行录像,并能够区分到底是哪个摄像头在录,这就需要前端页面能够实时的展示摄像头视频。

实现方式

  1. 浏览器支持(例:chrome),直接调用js的MediaRecorder对象来处理。
  2. 搭建rtmp服务器,通过推流(如:ffmpeg)和拉视频流的方式展示视频。
  3. 后端通过ffmpeg获取视频流,转成图片发给界面展示。

浏览器的MediaRecorder

fileSaver.js 用于浏览器保存文件

<!DOCTYPE html>
<html><head><title>video recoder</title><script src="./fileSaver.js"></script><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><!-- <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> -->
</head>
<style>body {background-color: #EFEDEF;}
</style><body><article style="border:1px solid white;width:400px;height:400px;margin:0 auto;background-color:white;"><section class="experiment" style="width:320px; height:240px;border:1px solid green; margin:50px auto;"><div id="videos-container" style="width:320px; height:240px;"></div></section><section class="experiment" style="text-align:center;border:none; margin-top:20px;"><button id="openCamera">打开摄像头</button><button id="start-recording" disabled>开始录制</button><button id="save-recording" disabled>保存</button><!--<a href="javascript:void(0)" οnclick="send()">发送</a>--></section></article><!-- <div style="display:none;"> --><!-- <div><button οnclick="tGet();">测试https请求</button></div> --><script>var mediaStream;var recorderFile;var stopRecordCallback;var openBtn = document.getElementById("openCamera");var startBtn = document.getElementById("start-recording");var saveBtn = document.getElementById("save-recording");openBtn.onclick = function () {this.disabled = true;startBtn.disabled = false;openCamera();};startBtn.onclick = function () {this.disabled = true;startRecord();};saveBtn.onclick = function () {saver();// alert('Drop WebM file on Chrome or Firefox. Both can play entire file. VLC player or other players may not work.');};var mediaRecorder;var videosContainer = document.getElementById('videos-container');function openCamera() {var len = videosContainer.childNodes.length;for (var i = 0; i < len; i++) {videosContainer.removeChild(videosContainer.childNodes[i]);}var video = document.createElement('video');var videoWidth = 320;var videoHeight = 240;video.controls = false;video.muted = true;video.width = videoWidth;video.height = videoHeight;MediaUtils.getUserMedia(true, false, function (err, stream) {if (err) {throw err;} else {// 通过 MediaRecorder 记录获取到的媒体流console.log("通过 MediaRecorder 记录获取到的媒体流");mediaRecorder = new MediaRecorder(stream);mediaStream = stream;var chunks = [],startTime = 0;video.srcObject = stream;video.play();videosContainer.appendChild(video);mediaRecorder.ondataavailable = function (e) {console.log("ondataavailable:", e)mediaRecorder.blobs.push(e.data);chunks.push(e.data);// f = new Blob([e.data], {//    'type': mediaRecorder.mimeType// });// send1(f);};mediaRecorder.blobs = [];mediaRecorder.onstop = function (e) {recorderFile = new Blob(chunks, {'type': mediaRecorder.mimeType});chunks = [];if (null != stopRecordCallback) {stopRecordCallback();}};}});}// 停止录制function stopRecord(callback) {stopRecordCallback = callback;// 终止录制器mediaRecorder.stop();// 关闭媒体流MediaUtils.closeStream(mediaStream);}var MediaUtils = {/*** 获取用户媒体设备(处理兼容的问题)* @param videoEnable {boolean} - 是否启用摄像头* @param audioEnable {boolean} - 是否启用麦克风* @param callback {Function} - 处理回调*/getUserMedia: function (videoEnable, audioEnable, callback) {navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ||navigator.msGetUserMedia || window.getUserMedia;var constraints = {video: videoEnable,audio: audioEnable};if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {callback(false, stream);})['catch'](function (err) {callback(err);});} else if (navigator.getUserMedia) {navigator.getUserMedia(constraints, function (stream) {callback(false, stream);}, function (err) {callback(err);});} else {callback(new Error('Not support userMedia'));}},/*** 关闭媒体流* @param stream {MediaStream} - 需要关闭的流*/closeStream: function (stream) {if (typeof stream.stop === 'function') {stream.stop();} else {let trackList = [stream.getAudioTracks(), stream.getVideoTracks()];for (let i = 0; i < trackList.length; i++) {let tracks = trackList[i];if (tracks && tracks.length > 0) {for (let j = 0; j < tracks.length; j++) {let track = tracks[j];if (typeof track.stop === 'function') {track.stop();}}}}}}};function startRecord() {mediaRecorder.start();// mediaRecorder.start(5000);setTimeout(function () {// 结束stopRecord(function () {console.log("录制成功!");openBtn.disabled = false;saveBtn.disabled = false;console.log(recorderFile);// 直接保存// saver();// alert("保存成功");// send();});}, 1000 * 5);}function saver() {var file = new File([recorderFile], 'msr-' + (new Date).toISOString().replace(/:|\./g, '-') + '.mp4', {type: 'video/mp4'});saveAs(file);}function send() {var file = new File([recorderFile], 'msr-' + (new Date).toISOString().replace(/:|\./g, '-') + '.mp4', {type: 'video/mp4'});var data = new FormData();data.append("username", "test");data.append("file", file);var req = new XMLHttpRequest();req.open("POST", "http://127.0.0.1:4780/upload");req.send(data);}function send1(bbb) {var file = new File([bbb], 'msr-' + (new Date).toISOString().replace(/:|\./g, '-') + '.webm', {type: 'video/webm'});var data = new FormData();data.append("username", "test");data.append("file", file);var req = new XMLHttpRequest();// req.open("POST", "http://127.0.0.1:4780/upload");req.open("POST", "https://127.0.0.1:7080/upload");req.send(data);}function tGet() {timedGetTest("https://127.0.0.1:7080", 5000)}function timedGetTest(url, time, callback) {var request = new XMLHttpRequest();var timeout = false;var timer = setTimeout(function () {console.log("超时了:", arguments);timeout = true;request.abort();}, time);request.open("GET", url);request.onreadystatechange = function () {console.log(arguments);if (request.readyState !== 4) return;if (timeout) return;clearTimeout(timer);if (request.status === 200) {callback(request.responseText);}}request.send(null);}</script></body></html>

直播推流拉流式

主要使用了开源库livego来作为rtmp服务器(其支持的协议有RTMP/AMF/HLS/HTTP-FLV),具体的详细介绍与使用,可以上github自行查看。

ffmpeg直接获取视频流,转换为图片发送给前端进行展示

用go语言实现,主要的难点就是cgo,需要通过cgo集成C的相关代码,获取视频流,然后转成图片即可。或者使用别人已经集成好的开源库goav进行相应的操作。

总结

本篇主要是讲了下web展示摄像头内容的几种实现方式,做个记录,也了解了一下直播相关的一些知识。通过开源库livego和flv.js搭建了一下直播客户端和服务器,使用OBS Studio进行视频推流,自己随便玩了一下,感兴趣的也可以自己尝试着搭建玩玩。

实时展示摄像头内容(go server + electron-vue client)相关推荐

  1. PHP JS浏览器实时获取摄像头内容(附代码)

    注意 浏览器获取摄像头权限时候比较严格,我百度时必须有ssl证书才可以(应该还有别的方法),我自己使用的https协议调用的 前端JS <!DOCTYPE html> <html&g ...

  2. vue实现点击不同按钮展示不同内容

    效果是:在同一个页面,点击不同按钮,展示不同内容(内容也是在同意页面) 方法是:借助v-show渲染不同的class属性 步骤: 1.先写两个按钮 <div class="right1 ...

  3. vue 中展示PDF内容

    vue 中展示PDF内容 不久前有个需要改的需求,以前是直接根据链接让用户下载对应pdf文件来查看,最主要是给用户查看,然而这种并不是很安全的,其他用户可以进行下载或者使用pdf链接分享给其他人,所以 ...

  4. editor修改样式 vue_手摸手Electron + Vue实战教程(三)

    系列文章: 手摸手Electron + Vue实战教程(一) 手摸手Electron + Vue实战教程(二) ❝ 上一篇我们已经完成了左侧菜单栏的基本样式功能,这一篇我们就主要来开发右侧的Markd ...

  5. 手把手教Electron+vue的使用

    .现如今前端框架数不胜数,尤其是angular.vue吸引一大批前端开发者,在这个高新技术快速崛起的时代,自然少不了各种框架的结合使用.接下来是介绍electron+vue的结合使用. 2.Elect ...

  6. android仿微信图片编辑器,electron/vue可编辑框contenteditable|仿微信截图

    基于Electron+vue实现div可编辑contenteditable插入表情|electron-vue截图功能 为了避免使用 vue 手动建立起 electron 应用程序.electron-v ...

  7. 使用Blazor和SqlTableDependency进行实时HTML页面内容更新

    目录 介绍 背景 使用代码 下载源1.3 MB 介绍 在这个简单的示例中,我们将看到发生在SQL Server数据库表更改时如何更新HTML页面,而无需重新加载页面或从客户端到服务器进行异步调用,而是 ...

  8. 【PPic】基于Electron+Vue+iView的图床应用设计

    其实这个应用并不是那么的特别需求,一来本人写blog越来越少,二来开发工作也是越做越少,再者目前的编辑器几乎都支持直接剪切板上传图片,使图床应用的场景越来越少.不过本人本着不想丢弃技术的内心想法,以及 ...

  9. 【PBL项目实战】户外智慧农场项目实战系列之4——Mind+Mixly双平台ESP32数据上云及云端可视化实时展示

    [PBL项目实战]户外智慧农场项目实战系列之4--Mind+Mixly双平台ESP32数据上云及云端可视化实时展示 原文链接  https://mp.weixin.qq.com/s/r_NeJdPoi ...

  10. 基于ffmpeg+SDL 实时播放摄像头视频

    基于ffmpeg+SDL 实时播放摄像头视频 基本流程 udp接收rtp数据流接收一帧数据后,转换为NAL单元送去解码 (这里特别说明一下,我本次用的接口是支持从连续数据流中自动分割出一个个NAL的, ...

最新文章

  1. 卷积学习与图像识别的技术发展
  2. 【c语言】求两数之和
  3. 用python 玩微信小程序“跳一跳”
  4. mysql主从切换gtid不一致_解决mysql使用GTID主从复制错误问题
  5. springboot最佳实践-SpringBoot应用如何编写
  6. div+css+js 树形菜单
  7. ASP.NET AJAX入门系列(10):Timer控件简单使用
  8. java外部接口图解_java代码实现访问网络外部接口并获取数据的工具类详解
  9. Android Studio 内存不足
  10. iOS 权限判断 跳转对应设置界面
  11. 适合甜蜜节日应用的霓虹海报模板!
  12. 基于人人网的简单爬虫(二)——具体实现
  13. 恒源云(Gpushare)_FAIR CVPR2022新作DVT是个啥?
  14. 二分查找(java代码实现)
  15. postgresql中查询COMMENT注释的语句
  16. android textview 字母数字键盘,android数字键盘怎样设置成默认的
  17. 将PNG序列帧图片合成视频
  18. 凯西与拜耳将在中国共同推广呼吸药物宝丽亚和启尔畅
  19. 详解在ubuntu上使用Jigdo下载debian镜像
  20. 失业的时候大家都在干什么?

热门文章

  1. Fibonacci 斐波那契数列的R语言实现
  2. 前端问题记录(持续更新...)
  3. c语言编写一个程序计算某年某月有几天,c语言:输入某一年的第几天,计算并输出它是这一年的第几月第几日,具体怎样编程...
  4. HCIA-IoT 个人学习总结 Day2
  5. 【点云处理之论文狂读经典版7】—— Dynamic Edge-Conditioned Filters in Convolutional Neural Networks on Graphs
  6. plotyy函数_plotyy函数参数设置
  7. matlab plotyy legend,一幅图中画两个legend及plotyy标注问题
  8. 【SSL】2021-08-19 1100.神秘数列
  9. office2016激活后仍然出现输入激活码
  10. 我爱世界杯--世界杯各界冠军