前端播放视频(前端播放视频框架 js)
1.m3u8
这种格式前端video标签可以直接播放,也可以使用封装好的插件,
在线播放地址(mr158.cn/m3u8test/)
vue项目demo
<template> <video ref="videoPlayer" class="video-js vjs-big-play-centered"></video> </template> <script> import videoPlayer from 'video.js' import 'video.js/dist/video-js.css' export default { name: 'com-VideoPlayer', props: { sources: { type: Array, default() { return [] }, }, options: { type: Object, default() { return {} }, }, }, data() { return { player: null, } }, watch: { sources(newValue) { const src = newValue && newValue.length > 0 ? newValue[0].src : '' if (src && this.player) { this.player.src(src) this.player.load() } }, }, mounted() { const defaultOptions = { autoplay: true, controls: true, preload: 'auto', muted: true, } this.player = videoPlayer( this.$refs.videoPlayer, { ...defaultOptions, ...this.options, sources: this.sources }, function onPlayerReady() {}, function onPlayerError() {} ) }, beforeDestroy() { if (this.player) { this.player.dispose() } }, } </script> <style lang="scss" scoped> .video-js { width: 100%; video { width: 100%; height: 100%; } } .video-js .vjs-tech { /* 视频填充整个容器 */ object-fit: cover; } </style> 复制代码
2.rtsp
这种视频前端不能直接播放,需要用插件
rtsp(x协议)://admin(用户名):ys123456(密码)@112.27.144.16(IP地址):554(端口)/Streaming/Channels/2
相关解决方案链接
(1.Vue项目中使用海康威视web插件:evestorm.github.io/posts/762/)
方案1(已购买海康服务器)
当例如海康的rtsp的视频,如果海康那边推上平台了,要海康提供AppKey 和
API网关提供的appkey API网关提供的secret
代码(组件内代码)
<template> <div class="video-dialog" v-if="showVideo"> <div class="close"> <button type="primary" @click="stopVideo">关闭视频</button> </div> <div id="videoPlayerBox" ref="videoPlayerBox"> <div id="playBox" v-html="oWebControl === null ? playText : ''">11</div> </div> </div> </template> <script> export default { name: "videoPlayer", props: [ "videoVisible", "monitorDeviceNo", "monitorDeviceName", "videoInfo", "layoutGet", "hideWin", "destoryWin", ], data() { return { showVideo:true, oWebControl: null, pubKey: "", // 公钥 appkey: "", secret: "", ip: "", port: 1443, width: `${document.documentElement.clientWidth}` * 0.5, height: 400, // 弹框高度 playHeight: `${document.documentElement.clientHeight}` * 0.78, layout: "1x1", left: "", top: "", buttonIDs: "0,16,256,257,258,259,260,512,513,514,515,516,517,768,769", initCount: 0, playMode: 0, // 0 预览 1回放 playText: "启动中。。。", cameraIndexCode: "d6c11f20e1ce4aea91d0817576fcbfb3", // 监控点编号 }; }, methods: { setLayout() { if (this.layoutGet == 1) { this.layout = "1x1"; // this.width = `${document.documentElement.clientWidth}` * 0.6 / 1 // this.playHeight = `${document.documentElement.clientHeight}` * 0.7 / 1 } else if (this.layoutGet == 2) { this.layout = "2x2"; // this.width = `${document.documentElement.clientWidth}` * 0.6 / 2 // this.playHeight = `${document.documentElement.clientHeight}` * 0.7 / 2 } else if (this.layoutGet == 3) { this.layout = "3x3"; // this.width = `${document.documentElement.clientWidth}` * 0.6 / 3 // this.playHeight = `${document.documentElement.clientHeight}` * 0.7 / 3 } else if (this.layoutGet == 4) { this.layout = "4x4"; // this.width = `${document.documentElement.clientWidth}` * 0.6 / 4 // this.playHeight = `${document.documentElement.clientHeight}` * 0.7 / 4 } }, show() { // console.log(this.videoInfo, 78787) this.setLayout(); // 设置top left let bodyW = document.body.clientWidth; let bodyH = document.body.clientHeight; this.left = bodyW / 2 - this.width / 2; this.top = bodyH / 3 - this.height / 3; let videoInfo = this.videoInfo; this.appkey = videoInfo[0].ekey; this.secret = videoInfo[0].esecret; this.cameraIndexCode = videoInfo[0].eNote; this.ip = videoInfo[0].eaddress.split(":")[0]; this.port = parseInt(videoInfo[0].eaddress.split(":")[1]); this.stopVideo(); setTimeout(() => { this.$nextTick(() => { this.initPlugin(() => { this.previewVideo(); }); }); }, 500); }, hide() { this.handleClose(); }, handleClose() { this.stopVideo(); }, stopVideo() { if (this.oWebControl) { this.oWebControl.JS_RequestInterface({ funcName: "stopAllPreview", }); this.oWebControl.JS_HideWnd(); this.oWebControl.JS_Disconnect().then( () => { // 断开与插件服务连接成功 console.log("断开与插件服务连接成功"); }, () => { // 断开与插件服务连接失败 console.log("断开与插件服务连接失败"); } ); this.oWebControl = null; this.showVideo=false; } }, // 推送消息 cbIntegrationCallBack() { // console.log(oData, '推送消息'); }, // RSA加密 setEncrypt(value) { /* eslint-disable */ let encrypt = new JSEncrypt(); encrypt.setPublicKey(this.pubKey); return encrypt.encrypt(value); }, initPlugin(callback) { /* eslint-disable */ let that = this; this.oWebControl = new WebControl({ szPluginContainer: "playBox", // 指定容器id iServicePortStart: 15900, // 指定起止端口号,建议使用该值 iServicePortEnd: 15909, szClassId: "23BF3B0A-2C56-4D97-9C03-0CB103AA8F11", // 用于IE10使用ActiveX的clsid cbConnectSuccess: function () { // 创建WebControl实例成功 that.oWebControl .JS_StartService("window", { // WebControl实例创建成功后需要启动服务 dllPath: "./VideoPluginConnect.dll", // 值"./VideoPluginConnect.dll"写死 }) .then( function () { // 启动插件服务成功 that.oWebControl.JS_SetWindowControlCallback({ // 设置消息回调 cbIntegrationCallBack: that.cbIntegrationCallBack, }); that.oWebControl .JS_CreateWnd("playBox", that.width, that.playHeight) .then(function () { // JS_CreateWnd创建视频播放窗口,宽高可设定 that.init(callback); // 创建播放实例成功后初始化 }); }, function () { // 启动插件服务失败 } ); }, cbConnectError: function () { // 创建WebControl实例失败 that.oWebControl = null; that.playText = "插件未启动,正在尝试启动,请稍候..."; WebControl.JS_WakeUp("VideoWebPlugin://"); // 程序未启动时执行error函数,采用wakeup来启动程序 // WebControl.JS_WakeUp("VideoWebPlugin://"); // 程序未启动时执行 that.initCount++; if (that.initCount < 3) { setTimeout(function () { that.initPlugin(); }, 3000); } else { that.playText = '插件启动失败,请检查插件是否安装!<a target="_blank" style="color: #30a8ff;text-decoration: underline;" href="http://xx.com/VideoWebPlugin.zip">下载地址(软件大小:62.7MB)</a>'; } }, cbConnectClose: function (bNormalClose) { // 异常断开:bNormalClose = false // JS_Disconnect正常断开:bNormalClose = true that.oWebControl = null; }, }); }, // 获取公钥 getPubKey(callback) { this.oWebControl .JS_RequestInterface({ funcName: "getRSAPubKey", argument: JSON.stringify({ keyLength: 1024, }), }) .then((oData) => { if (oData.responseMsg.data) { this.pubKey = oData.responseMsg.data; callback(); } }); }, init(callback) { let that = this; this.getPubKey(() => { // 请自行修改以下变量值 let appkey = this.appkey; // 综合安防管理平台提供的appkey,必填 let secret = that.setEncrypt(this.secret); // 综合安防管理平台提供的secret,必填 let ip = this.ip; // 综合安防管理平台IP地址,必填 let playMode = this.playMode; // 初始播放模式:0-预览,1-回放 let port = this.port; // 综合安防管理平台端口,若启用HTTPS协议,默认443 let snapDir = "D:\SnapDir"; // 抓图存储路径 let videoDir = "D:\VideoDir"; // 紧急录像或录像剪辑存储路径 let layout = this.layout; // playMode指定模式的布局 let enableHTTPS = 1; // 是否启用HTTPS协议与综合安防管理平台交互,是为1,否为0 let encryptedFields = "secret"; // 加密字段,默认加密领域为secret let showToolbar = 0; // 是否显示工具栏,0-不显示,非0-显示 let showSmart = 0; // 是否显示智能信息(如配置移动侦测后画面上的线框),0-不显示,非0-显示 let buttonIDs = this.buttonIDs; // 自定义工具条按钮 // /// 请自行修改以上变量值 that.oWebControl .JS_RequestInterface({ funcName: "init", argument: JSON.stringify({ appkey: appkey, // API网关提供的appkey secret: secret, // API网关提供的secret ip: ip, // API网关IP地址 playMode: playMode, // 播放模式(决定显示预览还是回放界面) port: port, // 端口 snapDir: snapDir, // 抓图存储路径 videoDir: videoDir, // 紧急录像或录像剪辑存储路径 layout: layout, // 布局 enableHTTPS: enableHTTPS, // 是否启用HTTPS协议 encryptedFields: encryptedFields, // 加密字段 showToolbar: showToolbar, // 是否显示工具栏 showSmart: showSmart, // 是否显示智能信息 buttonIDs: buttonIDs, // 自定义工具条按钮 }), }) .then((oData) => { that.oWebControl.JS_Resize(that.width, that.playHeight); // 初始化后resize一次,规避firefox下首次显示窗口后插件窗口未与DIV窗口重合问题 if (callback) { callback(); } }); }); }, // 视频预览功能 previewClick() { if (!this.oWebControl) { return; } // 如果是回放,重新初始化 if (this.playMode === 1) { this.playMode = 0; this.oWebControl.JS_HideWnd(); this.initPlugin(() => { this.previewVideo(); }); } else if (this.playMode === 0) { this.previewVideo(); } }, previewVideo() { for (let i = 0; i < this.videoInfo.length; i++) { // let cameraIndexCode = this.cameraIndexCode; // 获取输入的监控点编号值,必填 if (this.videoInfo[i].enote) { let cameraIndexCode = this.videoInfo[i].enote; // 获取输入的监控点编号值,必填 let streamMode = 0; // 主子码流标识:0-主码流,1-子码流 let transMode = 0; // 传输协议:0-UDP,1-TCP let gpuMode = 0; // 是否启用GPU硬解,0-不启用,1-启用 // let wndId = -1; // 播放窗口序号(在2x2以上布局下可指定播放窗口) let wndId = i + 1; // 播放窗口序号(在2x2以上布局下可指定播放窗口) console.log(cameraIndexCode, 8989); this.oWebControl.JS_RequestInterface({ funcName: "startPreview", argument: JSON.stringify({ cameraIndexCode: cameraIndexCode.trim(), // 监控点编号 streamMode: streamMode, // 主子码流标识 transMode: transMode, // 传输协议 gpuMode: gpuMode, // 是否开启GPU硬解 wndId: wndId, // 可指定播放窗口 }), }); } } }, // 回放 playBack() { if (!this.oWebControl) { return; } // 如果是预览 if (this.playMode === 0) { this.playMode = 1; this.oWebControl.JS_HideWnd(); this.initPlugin(() => { this.backVideo(); }); } else if (this.playMode === 1) { this.backVideo(); } }, backVideo() { let cameraIndexCode = this.cameraIndexCode; // 前30天 let date = new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000); let month = date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1; // 开始时间当天00点 let str = date.getFullYear() + "/" + month + "/" + date.getDate() + " 00:00:00"; let startTime = String( parseInt(new Date(str).getTime() / 1000) - 3 * 60 * 60 ); let endTime = String(parseInt(date.getTime() / 1000)); this.oWebControl.JS_RequestInterface({ funcName: "startPlayback", argument: JSON.stringify({ cameraIndexCode: cameraIndexCode.trim(), // 监控点编号 startTimeStamp: startTime, // 录像查询开始时间戳,单位:秒 endTimeStamp: endTime, // 录像查询结束时间戳,单位:秒 recordLocation: 1, // 录像存储类型 0-中心存储 1-设备存储 transMode: 0, // 传输协议 ,0-UDP 1-TCP gpuMode: 0, // 是否开启 GPU 硬解,0-不开启 1-开启 wndId: -1, //可指定播放窗口 }), }); }, // 设置窗口裁剪,当因滚动条滚动导致窗口需要被遮住的情况下需要JS_CuttingPartWindow部分窗口 setWndCover() { let iWidth = $(window).width(); let iHeight = $(window).height(); let oDivRect = $("#playBox").get(0).getBoundingClientRect(); let iCoverLeft = oDivRect.left < 0 ? Math.abs(oDivRect.left) : 0; let iCoverTop = oDivRect.top < 0 ? Math.abs(oDivRect.top) : 0; let iCoverRight = oDivRect.right - iWidth > 0 ? Math.round(oDivRect.right - iWidth) : 0; let iCoverBottom = oDivRect.bottom - iHeight > 0 ? Math.round(oDivRect.bottom - iHeight) : 0; iCoverLeft = iCoverLeft > this.width ? this.width : iCoverLeft; iCoverTop = iCoverTop > this.playHeight ? this.playHeight : iCoverTop; iCoverRight = iCoverRight > this.width ? this.width : iCoverRight; iCoverBottom = iCoverBottom > this.playHeight ? this.playHeight : iCoverBottom; this.oWebControl.JS_RepairPartWindow( 0, 0, this.width + 1, this.playHeight ); // 多1个像素点防止还原后边界缺失一个像素条 if (iCoverLeft != 0) { this.oWebControl.JS_CuttingPartWindow( 0, 0, iCoverLeft, this.playHeight ); } if (iCoverTop != 0) { this.oWebControl.JS_CuttingPartWindow(0, 0, this.width + 1, iCoverTop); // 多剪掉一个像素条,防止出现剪掉一部分窗口后出现一个像素条 } if (iCoverRight != 0) { this.oWebControl.JS_CuttingPartWindow( this.width - iCoverRight, 0, iCoverRight, this.playHeight ); } if (iCoverBottom != 0) { this.oWebControl.JS_CuttingPartWindow( 0, this.playHeight - iCoverBottom, this.width, iCoverBottom ); } }, // 拖拽窗口 onmousedown(e) { let that = this; let cWidth = document.body.clientWidth; let cHeight = document.body.clientHeight; this.$refs.videoPlayerBox.onmousemove = function (el) { let ev = el || window.event; ev.preventDefault(); // 解决点击标题窗口抖动的问题 if (Math.abs(ev.movementX) === 0 && Math.abs(ev.movementY) === 0) { return; } that.left += ev.movementX; that.top += ev.movementY; // 顶部不能超出,左侧、右侧、底部可以超出一半 if (that.top < 0) { that.top = 0; } if (that.top > cHeight - that.height / 2) { that.top = cHeight - that.height / 2; } if (that.left < -that.width / 2) { that.left = -that.width / 2; } if (that.left > cWidth - that.width / 2) { that.left = cWidth - that.width / 2; } that.oWebControl.JS_Resize(that.width, that.playHeight); }; this.$refs.videoPlayerBox.onmouseup = function () { this.onmousemove = null; this.onmouseup = null; }; // 阻止默认事件 if (e.preventDefault) { e.preventDefault(); } else { return false; } }, onmouseleave(e) { // 拖拽过快鼠标划出标题位置,重新拖拽是,鼠标还未移入标题,由于上一次的onmouseup方法没有执行,导致鼠标靠近弹框,弹框移动,解决方法,给onmousemove和onmouseup赋值为null this.$refs.videoPlayerBox.onmousemove = null; this.$refs.videoPlayerBox.onmouseup = null; // 解决拖拽过快,播放器残影问题 this.oWebControl.JS_Resize(this.width, this.playHeight); }, }, mounted() { // let hksystem = JSON.parse(sessionStorage.getItem('hksystem')); // this.appkey = hksystem.appKey; // this.secret = hksystem.appSecret; // this.ip = hksystem.host.split(':')[0]; // this.port = hksystem.host.split(':')[1]; // 监听resize事件,使插件窗口尺寸跟随DIV窗口变化 // $(window).resize(() => { // if (this.oWebControl != null) { // this.oWebControl.JS_Resize(this.width, this.playHeight); // // this.setWndCover(); // } // }); // 监听滚动条scroll事件,使插件窗口跟随浏览器滚动而移动 // $(window).scroll(() => { // if (this.oWebControl != null) { // this.oWebControl.JS_Resize(this.width, this.playHeight); // this.setWndCover(); // } // }); // 标签关闭 $(window).unload(() => { if (this.oWebControl != null) { this.oWebControl.JS_HideWnd(); // 先让窗口隐藏,规避可能的插件窗口滞后于浏览器消失问题 this.oWebControl.JS_Disconnect().then( () => { // 断开与插件服务连接成功 }, () => { // 断开与插件服务连接失败 } ); } }); // this.setLayout() this.show(); }, watch: { monitorDeviceNo: { handler(newV, oldV) { this.cameraIndexCode = newV; if (newV && this.playMode === 0) { this.previewVideo(); } else if (newV && this.playMode === 1) { this.backVideo(); } }, }, layoutGet: { handler(newV, oldV) { console.log(newV); if (newV && newV != oldV) { this.show(); // this.previewVideo() } }, }, hideWin: { handler(newV, oldV) { if (newV) { this.oWebControl.JS_HideWnd(); this.showVideo=false } else { this.oWebControl.JS_ShowWnd(); } }, }, destoryWin: { handler(newV, oldV) { this.stopVideo(); }, }, videoInfo: { handler(newV, oldV) { if (newV) { console.log(newV, 2222); this.show(); } }, }, }, }; </script> <style lang="scss" scoped> .video-dialog { // width:790px; // height: 100%; left:480px; top:100px; position: fixed; z-index: 10000; .close{ width:100%; display: flex; align-items: center; justify-content: center; img{ width:30px; height:auto; } } } #videoPlayerBox { // width: 100%; // height: 100%; z-index: 4012; .header { height: 0.24rem; width: 100%; display: flex; align-items: center; font-size: 0.1rem; color: #fff; text-align: center; justify-content: center; background: #142c50; } .closeBtn { width: 0.12rem; height: 0.12rem; position: absolute; top: 0.06rem; right: 0.06rem; cursor: pointer; } .topbutton { height: 26px; padding-top: 6px; padding-left: 14px; background-color: #0d1f3a; cursor: pointer; .topBtns { padding: 0 12px 6px; border-bottom: 1px solid #0d1f3a; color: #fff; } .activeBtn { color: #4ba2ff; border-color: #4ba2ff; } } #playBox { // width: 100%; // height: 100%; color: #ffffff; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 12px; } } </style> 复制代码
调用处代码
<template> <div class="list_view"> <div class="item_view" v-for="(item, index) in testList" :key="index"> <rtsp-video :hideWin="maskOpen" :destoryWin="destroy" :data-active="updateTime" ref="videoRef" :videoInfo="[form]" layoutGet="1" >22</rtsp-video> 33 <div class="name">{{ item.name }}</div> <demo-video :list="curShowList" :layout="layout"></demo-video> </div> </div> </template> <script> import TitleBar from '@/components/TitleBar' import AutoLoadMixin from '@/plugin/AutoLoadMixin' import rtspVideo from "@/components/RTSPVideo"; // import demoVideo from "@/components/demoVideo" export default { components: { TitleBar, rtspVideo, // demoVideo }, mixins: [AutoLoadMixin], data() { return { currentIndex: 0, curShowList: [ { cameraIndexCode: '2', streamMode: 'rtsp', transMode: 'rtsp' } ], destroy: false, maskOpen: false, updateTime: 1, activeTab: "1", form: { areaId: 6, createBy: null, createTime: "2021-09-07 14:09:38", deviceCode: "59560b1dcbdc49ba986149b5d48c60ec", deviceCount: "767", deviceName: "会所西通道", eaddress: "219.146.242.46:1443", ekey: "25891522", endTime: null, enote: "59560b1dcbdc49ba986149b5d48c60ec", ertsp: null, esecret: "X1LHdWow0bztDywl48j9", isWarn: 25, latitude: 39.988395, left: 834.829268292683, longitude: 116.496051, online: 0, params: {}, parentId: null, parentName: null, picName: null, picUrl: "http://123.57.155.205/comprehensive/72a6894a56cc47458078ec7bc963de30/72a6894a56cc47458078ec7bc963de30-202151721.png", remark: null, rootArea: null, searchValue: null, spaceName: null, startTime: null, top: 714.0044642857142, typeId: 285, typeName: "人脸抓拍摄像机", updateBy: null, updateTime: null, } } }, } </script> 复制代码
方案2(vlc插件播放)
直接使用rtsp地址来进行播放
(rtsp://admin:ys123456@112.27.144.16:554/Streaming/Channels/2)
使用vlc插件来进行播放,只支持win 并且只能使用ie浏览器或者360兼容模式(vue项目需要配置来兼容ie浏览器)
方案3(ffmpeg转码)
rtsp 视频流,没法在网页中直接播放。在技术支持的群里问了管理员也说没有网页端播放的方案,那只能自己找了。
网上找了一圈很多都是较老的技术了,有用老版本的 chrome 安装 vlc 插件的来播放的,有用 flash 来播放的,而且很多博客抄来抄去都差不多。
最后找到两个看着靠谱的方案:
一种是用 ffmpeg 的 wasm 版直接前端解析转码 rtsp 视频流,转成前端能直接播放的格式
第二种是写一个服务还是调用 ffmpeg 将 rtsp 视频流转码,给前端返回新的视频地址
看到这两个方案我感觉第二种是肯定行得通的,第一种方案暂时不清楚,没怎么用过前端 wasm 技术,只能再去搜索一下。
针对第一种方案,去 =github= 上搜索了一下看看别人有没有用这个的,结果发现暂时还不支持 rtsp 视频流,这是2020年11月4号的回复,后续也没见有啥更新,当他暂时不支持,那就选第二种方案。
方案二的实现就分两步:
首先是转码,想着可以用 java 调用 ffmpeg 来实现,但是要转成什么视频流又没有思路。
第二步就是找前端能播放视频流的插件,转码的最终目的是为了前端网页中能播放,所以先看看有什么播放器能支持什么格式。
之前在很多网站看视频的时候发现网页端播放的视频很多都以 m3u8 结尾的。 之前好奇下载过发现这是一个文件,里面记录了很多视频分片,最后查了知道这个叫 hls,搜索了之后找到了网页播放 hls 的插件 hls.js。 所以现在目标就明确了,首先将 rtsp 转成 hls, 再用 hls.js 来播放。
转码服务
一开始想着自己用 java 调用 ffmpeg 来做转码服务,实现起来应该也不难,但想着这种应该是比较常见的需求,看看有没有轮子。 经过一番搜索,还真找到了一个 rtsp-stream,真的是帮了大忙,节约了好多时间,感谢开源大佬的无私奉献。 这个就是正好完全解决了我的需求,而且是用 Go 写的,部署简单,跨平台,而且作者还很贴心的构建了 Docker 镜像。
根据文档介绍,转换接口是 start 接口,会返回一个 m3u8 的链接,前端播放器只需要播放这个链接就行。 这些都可以交给后端来实现,后端只需要暴露一个接口返回一个 m3u8 的链接即可。流程如下:
前端播放
这里用的是 react, 用 vue 或者别的应该差不多,首先是加载 hls.js 插件
useEffect(() => { if (hlsRef.current) { hlsRef.current.detachMedia(); } setLoading(true); stopVideo(); const meta = document.createElement('meta'); meta.name = 'referrer'; meta.content = 'no-referrer'; document.getElementsByTagName('head')[0].appendChild(meta); const script = document.createElement('script'); if (script.readyState) { // IE script.onreadystatechange = () => { if (script.readyState === 'loaded' || script.readyState === 'complete') { script.onreadystatechange = null; initVideo(); } }; } else { // 其他浏览器 script.onload = () => { initVideo(); }; } script.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest'; document.getElementsByTagName('head')[0].appendChild(script); return () => { if (hlsRef.current) { hlsRef.current.detachMedia(); } stopVideo(); }; }, []); 复制代码
initVideo() 方法就是具体要设置播放的操作,首先是获取播放地址,然后再将 hls 实例绑定到 video 标签上。
function initVideo() { const requestBody = { cameraCode: '1' }; axios.post('/video/get-url', requestBody).then(res => { setUrlInfo(res); const video = document.getElementById('realTime'); const videoSrc = res.uri; if (window.Hls.isSupported()) { const hls = new window.Hls(); hlsRef.current = hls; hls.loadSource(videoSrc); hls.attachMedia(video); } setLoading(false); }).catch(e => { // eslint-disable-next-line no-console console.log('err: ', e.message); message.error('获取本地视频失败'); setLoading(false); }); } 复制代码
最后是加上一个 video 标签:
<video id="realTime" controls="controls" className={`${prefixCls}-video`} /> 复制代码
注意事项
hls.js 只支持 H.264 编码的视频,不支持 H.265 编码的视频。如果遇到转码没问题,返回的链接能在 vlc 等播放器播放,但在网页中进度条正常,就是黑屏无图像时,可以去 nvr800 查看一下视频编码。
结束播放时可以给服务器发送一个请求停止转码,避免浪费资源。
作者:怕是个胖虎哟
链接:https://juejin.cn/post/7034052661125054471