自从2024年12月12日从广州南沙广汽的项目准备上线完毕,即刻奔赴京城进行下一个项目,原计划元旦前结束,但是2025年1月6号也就是今天,我还在酒店待着,眼瞅着放了一个星期的带薪假,好好整理一下这中间接近一个月的项目收获,重点就聚焦于flv.js实现海康威视监控实时播放,逆天的是支持九宫格视频浏览器实时播放。
常见的视频编码格式有哪些
- H.264(AVC):一种广泛使用的视频编码标准,提供了较高的压缩率,同时保持了较好的视频质量。
- H.265(HEVC):下一代视频编码标准,提供了比H.264更高的压缩率,但计算复杂度也更高。
- VP8:由Google开发的一种视频编码格式,用于WebM视频格式。
- VP9:VP8的继任者,提供了更高的压缩率和质量。
- AV1:一种开放、免专利费的视频编码格式,旨在替代H.264和HEVC。
flv.js为何物
项目中如何使用
海康RTSP转flv并实现h5页面播放_海康flv地址-CSDN博客
使用ffmpeg生成flv视频流在网页上显示海康摄像头图视频 - 柳暗花明8963 - 博客园
海康威视摄像头视频在web端播放FFmpeg + nginx-http-flv-module + flv.js(无需安装插件) - 掘金
自己系统集成海康威视摄像机和录像机实现监控与回放(二开海康威视摄像机)_摄像头系统集成-CSDN博客
flv.js优化
flv优势
- 由于浏览器对原生Video标签采用了硬件加速,性能很好,支持高清。
- 去掉对Flash的依赖
flv限制
- FLV里所包含的视频编码必须是
H.264,音频编码必须是AAC或MP3, IE11和Edge浏览器不支持MP3音频编码,所以FLV里采用的编码最好是H.264+AAC,这个让音视频服务兼容不是问题。****
- 对于录播,依赖
原生HTML5 Video标签和 Media Source Extensions API
flv.js 原理
flv.js只做了一件事,在获取到FLV格式的音视频数据后通过原生的JS去解码FLV数据,再通过Media Source Extensions API 喂给原生HTML5 Video标签。(HTML5 原生仅支持播放 mp4/webm 格式,不支持 FLV)
这里提到mse,讲述一下mse的原理
MSE 的核心原理
- 脱离容器格式限制:
-
- 原生
<video>标签仅支持完整的媒体文件(如 MP4、WebM),无法直接处理流式数据。 - MSE 允许开发者将媒体数据分块传输到视频标签,绕过浏览器对容器格式的严格限制。
- 原生
- 核心对象与流程:
-
MediaSource:表示一个媒体资源管道,绑定到<video>标签的src属性。SourceBuffer:用于向MediaSource中追加具体的媒体数据块(如视频片段或音频片段)。- 流程:
const mediaSource = new MediaSource();
videoElement.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', () => {
const sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.64001f, mp4a.40.2"');
// 向 sourceBuffer 中追加数据(如 FLV 解封装后的 MP4 片段)
sourceBuffer.appendBuffer(segmentData);
});
MSE 在 flv.js 中的作用
flv.js 的工作流程与 MSE 的配合如下:
- FLV 解封装:
-
- FLV 是一种流媒体封装格式,包含 H.264/H.265 视频和 AAC/MP3 音频。
- flv.js 解析 FLV 数据,分离出视频裸流(H.264 NALU)和音频裸流(AAC/MP3) 。
- 转封装为 fMP4:
-
- 将分离出的音视频数据**重新封装为 Fragmented MP4(fMP4)**格式。
- fMP4 是 MSE 支持的格式,可直接通过
SourceBuffer传输。
- 通过 MSE 传输:
-
- 将 fMP4 数据块通过
SourceBuffer.appendBuffer()传输到MediaSource。 - 浏览器内部解码并渲染到
<video>标签。
- 将 fMP4 数据块通过
流程:
FLV数据 → flv.js解封装 → H.264/AAC裸流 → 封装为fMP4 → MSE传输 → 视频标签播放
MSE 的关键特性
- 分段加载与实时性:
-
- 支持动态追加数据,适合直播流(如 Web 直播、实时监控)。
- 可通过
appendBuffer()和remove()管理缓冲区,避免内存溢出。
- 自适应码率(ABR) :
-
- 根据网络状况切换不同码率的媒体片段(需配合 DASH/HLS 等协议)。
- 低延迟优化:
-
- 通过调整缓冲区策略(如
bufferLag控制)减少播放延迟。
- 通过调整缓冲区策略(如
- 兼容性:
-
- 支持所有现代浏览器(Chrome、Firefox、Edge、Safari),但不支持 IE11。
延迟优化
延迟产生的三端
- 推流端
- 流媒体服务
- 播放端
延迟产生的原因
- 主播端在采集到一段时间的音视频原数据后,因为音视频原数据庞大需要先压缩数据:
-
- 通过H264视频编码压缩数据数据
- 通过PCM音频编码压缩音频AAC数据
- 压缩完后再通过FLV容器格式封装压缩后的数据,封装成一个FLV TAG
- 再把FLV TAG通过RTMP协议推流到音视频服务器,音视频服务器再从RTMP协议里解析出FLV TAG。
- 音视频服务器再通过HTTP协议通过和浏览器建立的长链接流式把FLV TAG传给浏览器。
- flv.js 获取FLV TAG后解析出压缩后的音视频数据喂给Video播放。
详细解释
flv.js 是一个用于在浏览器中播放 FLV 格式视频流的 JavaScript 库,它通过 MSE(Media Source Extensions)来实现这一点。
GOP(Group of Pictures)
- 定义:GOP 是视频压缩中的一个概念,指的是一组连续的画面,通常以一个 I 帧(关键帧)开始,后面跟着多个 P 帧(预测帧)和/或 B 帧(双向预测帧)。
- 优化方法:缩短 GOP 长度意味着减少从一次 I 帧到下一次 I 帧之间的时间。这样可以减少延迟,因为新观众加入直播时不需要等待太长时间就能接收到下一个 I 帧,从而快速开始解码播放。
- 缺点:缩短 GOP 长度会降低视频的压缩率,因为 I 帧包含的信息量大,频繁的 I 帧会增加数据量,从而降低传输效率。
音视频服务器的 I 帧缓存
- 优化方法:关闭 I 帧缓存可以减少服务器端的延迟,因为服务器会更快地将数据推送到客户端。
- 缺点:关闭缓存可能导致用户在开始观看直播时需要更长时间才能看到首屏,因为需要等待下一个 I 帧
音视频服务器的 Buffer
- 优化方法:减少服务器端的缓冲区大小可以减少数据在服务器端的停留时间,从而减少延迟。
- 缺点:减少缓冲区大小可能会降低服务器的处理效率,因为它需要更频繁地处理和发送数据。
浏览器端 flv.js 的 Buffer
- 优化方法:减少 flv.js 的缓冲区大小可以减少在浏览器端的数据积累,使得视频播放更快地对新数据进行响应。
- 缺点:减少缓冲区大小可能会降低浏览器端的处理效率,因为它需要更频繁地处理数据。
flv.js 的 Worker 配置
- 配置代码解释:
-
enableWorker: true:开启 Web Worker,允许 flv.js 在一个单独的线程中运行解码和渲染任务,这样可以避免主线程被阻塞,提高解析速度。enableStashBuffer: false:关闭 stash buffer,这可以减少在 flv.js 中的数据缓冲,从而减少延迟。stashInitialSize: 128:设置初始 stash buffer 的大小为 128KB。尽管enableStashBuffer被设置为false,但这个配置仍然指定了初始大小,以备需要时使用。
通过上述优化措施,可以在一定程度上减少直播延迟。但是,需要注意的是,优化延迟可能会牺牲视频质量、服务器性能或用户体验。因此,在实际应用中,需要根据具体情况和需求进行权衡。
解决
- 主播端采集时收集了一段时间的音视频原数据,它专业的叫法是GOP。缩短这个收集时间(也就是减少GOP长度)可以优化延迟,但这样做的坏处是导致视频压缩率不高,传输效率低。
- 关闭音视频服务器的I桢缓存可以优化延迟,坏处是用户看到直播首屏的时间变大。
- 减少音视频服务器的buffer可以优化延迟,坏处是音视频服务器处理效率降低。
- 减少浏览器端flv.js的buffer可以优化延迟,坏处是浏览器端处理效率降低。
- 浏览器端开启flv.js的Worker,多进程运行flv.js提升解析速度可以优化延迟,这样做的flv.js配置代码是:
● {
enableWorker: true,
enableStashBuffer: false,
stashInitialSize: 128,
}
问题思考
- 如果监控视频用了mse进行浏览器播放,node端是不是就不需要ffmepg了,两者之间是否有冲突?
两者不冲突。等于说mse是浏览器的video标签实现过程,ffmepg是将监控视频的rstp转化为h.264的过程
监控视频在浏览器中播放通常涉及到视频的编码、传输和解码。MSE(Media Source Extensions)是Web API的一部分,允许JavaScript动态地构建媒体流,用于在浏览器中播放。以下是关于您问题的解答:
- MSE与FFmpeg的关系:
-
- MSE主要用于在浏览器端处理媒体流,它定义了媒体缓冲、解码和渲染的接口,但它本身并不负责视频的编码工作。
- FFmpeg是一个强大的多媒体处理工具,能够进行视频的编解码、转码、流处理等多种操作。
- 是否需要FFmpeg:
-
- 如果监控视频已经是以浏览器支持的格式(如H.264编码的MP4或WebM)编码好的,并且可以直接通过MSE进行播放,那么在Node.js服务器端不一定需要FFmpeg。
- 如果视频源不是浏览器直接支持的格式,或者需要实时转码、处理视频流(例如,调整分辨率、码率或格式转换),那么在服务器端使用FFmpeg进行转码处理后再通过MSE播放是常见的做法。
- 两者之间是否有冲突:
-
- MSE和FFmpeg在功能上是互补的,通常不会存在冲突。MSE在浏览器端工作,而FFmpeg在服务器端工作。
- 在实际应用中,FFmpeg可以用来预处理视频流,使其适应网络传输和浏览器解码的需要,然后通过HTTP流或WebSocket等方式发送到客户端,客户端使用MSE进行播放。
总结来说,是否在Node端使用FFmpeg取决于您的具体需求。如果需要进行视频的预处理或转码,FFmpeg是很有用的工具,并不会与浏览器端的MSE发生冲突。它们可以很好地协同工作,以提供流畅的视频播放体验。
- MSE(Media Source Extensions) :
-
- MSE是Web API的一部分,它扩展了HTML5
<video>和<audio>标签的功能,允许开发者通过JavaScript直接创建媒体流,而不是依赖于浏览器内置的媒体处理能力。 - MSE使得开发者可以更灵活地处理媒体流,比如实现自适应流(Adaptive Streaming)或自定义媒体缓冲逻辑。
- 当使用MSE时,浏览器端的
<video>标签被用作媒体的播放界面,而MSE负责管理媒体数据的加载、缓冲和解码过程。
- MSE是Web API的一部分,它扩展了HTML5
- FFmpeg:
-
- FFmpeg是一个开源的多媒体框架,能够处理视频和音频的录制、转换和流处理等多种操作。
- 在监控视频的场景中,FFmpeg常用于将RTSP(Real Time Streaming Protocol)流转换为H.264编码的视频流。RTSP是用于控制流媒体传输的协议,常用于IP摄像头等监控设备。
- FFmpeg可以将RTSP流解码,然后重新编码为H.264格式,这种格式通常可以直接被Web浏览器支持,并通过MSE进行播放。
所以,概括来说,FFmpeg通常用于服务器端,将监控摄像头生成的RTSP视频流转换成Web友好的格式(如H.264),而MSE则是在浏览器端,允许开发者使用JavaScript来控制<video>标签的媒体播放过程,包括加载和播放由FFmpeg转换后的视频流。这两者共同工作,实现了监控视频在Web浏览器中的播放。
Node-media监控实现
问题指引
网络摄像头所输出的网络协议一般为rstp协议,而浏览器无法支持rstp协议,因而浏览器无法直接通过地址解析播放监控视频。
目前的方案有两种
- 通过浏览器插件的方式,安装插件播放RTSP流;
- 通过HTML5 js引擎视频解码播放RTSP流;
而第一种基本已被淘汰,淘汰的原因为
- 安装繁琐。
- 浏览器不同适配不一致,维护也繁琐。
解决方案概述
视频推出的视频流编码格式为rstp,而浏览器无法直接播放rstp格式的编码视频,因此需要通过后端基于ffmepg工具进行转码,node在这里是起到了中间转码的作用。
需要三部分工具
- ffmpeg工具
- nodeJS转码
- 前端播放
参考资料
下载ffmepg
安装教程:blog.csdn.net/m0_47449768…
配置环境变量
我的电脑 => 属性 => 高级系统设置 => 环境变量 => Path => 添加 D:\DevelopSoftware\ffmpeg\bin
node.js编写转码服务
新建 index.js
var express = require('express')
var expressWebSocket = require('express-ws')
var ffmpeg = require('fluent-ffmpeg')
var webSocketStream = require('websocket-stream/stream')
var WebSocket = require('websocket-stream')
var http = require('http')
ffmpeg.setFfmpegPath('ffmpeg')
// config
let rtspServerPort = 2156
function localServer() {
let app = express()
app.use(express.static(__dirname))
expressWebSocket(app, null, {
perMessageDeflate: true
})
// :id是动态参数, 前端调用时传递, 可以去掉
app.ws('/rtsp/:id/', rtspRequestHandle)
app.listen(rtspServerPort)
console.log('express listened on port : ' + rtspServerPort)
}
function rtspRequestHandle(ws, req) {
console.log('rtsp request handle')
const stream = webSocketStream(
ws,
{
binary: true,
browserBufferTimeout: 1000000
},
{
browserBufferTimeout: 1000000
}
)
let url = req.query.url
console.log('rtsp url:', url)
console.log('rtsp params:', req.params)
try {
ffmpeg(url)
.addInputOption('-rtsp_transport', 'tcp', '-buffer_size', '102400') // 这里可以添加一些 RTSP 优化的参数
.on('start', function () {
console.log(url, 'Stream started.')
})
.on('codecData', function () {
console.log(url, 'Stream codecData.')
// 摄像机在线处理
})
.on('error', function (err) {
console.log(url, 'An error occured: ', err.message)
})
.on('end', function () {
console.log(url, 'Stream end!')
// 摄像机断线的处理
})
.outputFormat('flv')
.videoCodec('copy')
.noAudio()
.pipe(stream)
} catch (error) {
console.log(error)
}
}
localServer()
运行node.js
node index.js
后端实现
环境准备
- 首先需要准备一台海康威视的摄像头,并且将该摄像头的音频码变为aac
- 准备前端版本:node版本为v16.20.2,npm版本为8.19.4
- 下载并安装ffmpeg并配置到环境变量中
- 使用命令下载node-media-server2.4.9版本的流媒体服务器
npm install node-media-server@2.4.9
代码实现
- 创建一个js器启动流媒体服务,在这里我创建一个app.js
const NodeMediaServer= require('node-media-server');
console.log("参数为",process.argv)
const ip = process.argv[2]; // 获取传递的第一个参数作为IP地址
const config = {
rtmp: {
port: 1935,
chunk_size: 60000,
gop_cache: true,
ping: 60,
ping_timeout: 30
},
//端口是登录nms服务器后台查看界面
http: {
port: 8000,
mediaroot: './media/',
allow_origin: '*',
},
relay: {
ffmpeg: 'D:/java/ffmpeg/ffmpeg/bin/ffmpeg.exe',
tasks: [
{
app: 'live',
mode: 'static',
edge: 'rtsp://admin:3edc$RFV@'+ip+':554/Streaming/Channels/101 -an -vcodec h264 -acodec aac -f flv',//rtsp
name: 'technology',
rtsp_transport : 'tcp', //['udp', 'tcp', 'udp_multicast', 'http']
},
{
app: 'live',
mode: 'static',
edge: 'rtsp://admin:3edc$RFV@192.168.1.64:554/Streaming/Channels/201 -an -vcodec h264 -acodec aac -f flv',//rtsp
name: 'technology2',
rtsp_transport : 'tcp', //['udp', 'tcp', 'udp_multicast', 'http']
}
]
},
};
var nms = new NodeMediaServer(config)
nms.run();
后端亮点
- 其中直接使用代码的方式去调用ffmpeg,因为开启ffmpeg窗口的方式去推流,从node-media-server去观看会掉帧以及延迟比较高,所以直接放置在node-media-server中使用代码的方式去调用,这样在页面上观看视频不会掉帧以及延迟较低
- 使用命令去启动node-media-server
- 随后去访问node-media-server的地址好,查看是否启动成功
前端实现
引入flv.js
github下载地址:github.com/Bilibili/fl…
flv.js简介
- flvjs.isSupported():判断当前浏览器是否支持播放
- flvPlayer = flvjs.createPlayer(mediaDataSource: MediaDataSource, config?: Config):创建一个播放实例
- flvPlayer.attachMediaElement(mediaElement: HTMLMediaElement):将播放实例注册到video节点
- flvPlayer.load():加载视频
- flvPlayer.play():播放视频
- flvPlayer.pause():视频暂停
- flvPlayer.unload():去除视频加载
- flvPlayer.detachMediaElement():将播放实例从节点中取出
- flvPlayer.destroy():销毁播放实例
视频流播放
// 获取video节点
videoElement = document.getElementById('my-player');
createVideo() {
if (flvjs.isSupported()) {
var videoElement = document.getElementById("videoElement" + this.count);
this.flvPlayer = flvjs.createPlayer(
{
type: "flv",
isLive: true,
hasAudio: false,
url: this.url
},
{
enableWorker: true, //启用分离线程(单独渲染)
enableStashBuffer: false, //关闭IO隐藏缓冲区(减少延迟)从源到缓存区再到端
autoCleanupSourceBuffer: true //自动清除缓存
}
);
this.flvPlayer.attachMediaElement(videoElement);
// this.flvPlayer.load();
if (this.url !== "" && this.url !== null) {
this.flvPlayer.load();
this.flvPlayer.play();
}
}
setInterval(function() {
// console.log(videoElement.buffered,"idididid");
if (videoElement.buffered.length > 0) {
const end = videoElement.buffered.end(0); // 视频结尾时间
const current = videoElement.currentTime; // 视频当前时间
const diff = end - current; // 相差时间
// console.log(diff);
const diffCritical = 4; // 设定了超过4秒以上就进行跳转
const diffSpeedUp = 1; // 设置了超过1秒以上则进行视频加速播放
const maxPlaybackRate = 4; // 自定义设置允许的最大播放速度
let playbackRate = 1.0; // 播放速度
if (diff > diffCritical) {
// this.flvPlayer.load();
// console.log("相差超过4秒,进行跳转");
videoElement.currentTime = end - 1.5;
playbackRate = Math.max(1, Math.min(diffCritical, 16));
} else if (diff > diffSpeedUp) {
// console.log("相差超过1秒,进行加速");
playbackRate = Math.max(1, Math.min(diff, maxPlaybackRate, 16));
}
videoElement.playbackRate = playbackRate;
if (videoElement.paused) {
videoElement.play();
}
}
// if (videoElement.buffered.length) {
// let end = this.flvPlayer.buffered.end(0);//获取当前buffered值
// let diff = end - this.flvPlayer.currentTime;//获取buffered与currentTime的差值
// if (diff >= 0.5) {//如果差值大于等于0.5 手动跳帧 这里可根据自身需求来定
// this.flvPlayer.currentTime = this.flvPlayer.buffered.end(0);//手动跳帧
// }
// }
}, 1000);
this.flvPlayer.on(flvjs.Events.ERROR, (errType, errDetail) => {
// alert("网络波动,正在尝试连接中...");
if (this.flvPlayer) {
this.reloadVideo(this.flvPlayer);
}
// errType是 NetworkError时,对应errDetail有:Exception、HttpStatusCodeInvalid、ConnectingTimeout、EarlyEof、UnrecoverableEarlyEof
// errType是 MediaError时,对应errDetail是MediaMSEError 或MEDIA_SOURCE_ENDED
});
this.flvPlayerList.push(this.flvPlayer);
},
优化
通过监测视频缓冲进度,当播放延迟超过1秒时,采用加速播放来追赶缓冲。通过setInterval定时检查并动态调整playbackRate来减少卡顿,提高用户体验
var int1 = self.setInterval("clock()", 5000);
function clock() {
if (this.player.buffered.length) {
let end = this.player.buffered.end(0);//获取当前buffered值
let diff = end - this.player.currentTime;//获取buffered与currentTime的差值
if (diff >= 1.0) {//如果差值大于等于1.0 手动跳帧 这里可根据自身需求来定
//this.player.currentTime = this.player.buffered.end(0)-0.2;//手动跳帧,卡顿
this.player.playbackRate +=0.1;//采用加速方式追帧
}
else
this.player.playbackRate =1.0;
}
}
视频流销毁
flvPlayer.pause();
flvPlayer.unload();
flvPlayer.detachMediaElement();
flvPlayer.destroy();
flvPlayer = null;
错误相关
SourceBuffer报错
使用报错如下:Failed to read the ‘buffered’ property from ‘SourceBuffer’: This SourceBuffer has been removed from the parent media source.
- 解决方案1:
这种错误提示一般是在flv源发生异常中断的时候产生的。错误提示大多数都在 mse-controller.js 这个模块中。
解决办法:在 mse-controller.js中(flv.js/src/core/mse-controller.js),appendMediaSegment()、_needCleanupSourceBuffer()这些方法的入口处调用检查以下MediaSource的合法性
if (!this._mediaSource || this._mediaSource.readyState !== 'open') {
return;
}
- 解决方案2
- 解决方案3:
在使用flv播放视频流的时候遇到了个错误Uncaught (in promise) DOMException: Failed to read the ‘buffered’ property from ‘SourceBuffer’:This SourceBuffer has been removed from the parent media source.
<template>
<a-modal
v-model:visible="visible"
width="85%"
:footer="null"
title="摄像头预览"
:cancel="handleCanle"
:style="{ top: '128px', left: '90px' }"
:bodyStyle="{ height: '70vh', padding: '0px', display: 'flex', alignItems: 'center', justifyContent: 'center'}"
>
<template v-if="visible">
<video
id="videoElement"
controls
:style="{ height: '90%', width: '90%' }"
></video>
</template>
</a-modal>
</template>
<script setup lang="ts" name="PopupCameraLook">
import { onMounted, ref } from 'vue'
import cameraApi from '@/api/camera/cameraApi'
import flvjs from 'flv.js'
import { message } from 'ant-design-vue'
const visible = ref<boolean>(false)
const flvPlayer = ref()
const popup = (defaultData?) => {
console.log(defaultData)
cameraApi.getCameras(defaultData.id).then((res) => {
createVideo(res.data.preview)
})
}
const createVideo = (id) => {
if (flvjs.isSupported()) {
const videoElement = document.getElementById('videoElement')
flvPlayer.value = null
flvPlayer.value = flvjs.createPlayer(
{
type: 'flv',
url: id, // 你的url地址
isLive: true,
hasAudio: false
}
)
flvPlayer.value.attachMediaElement(videoElement)
flvPlayer.value.load()
setTimeout(function () {
flvPlayer.value.play()
}, 1000)
// 处理视频播放错误的语法
flvPlayer.value.on('error', () => {
message.error('视频加载失败,请稍候重试!')
destroy()
return false
})
}
}
const destroy = () => {
flvPlayer.value.pause() // 暂停播放数据流
flvPlayer.value.unload() // 取消数据流加载
flvPlayer.value.detachMediaElement() // 将播放实例从节点中取出
flvPlayer.value.destroy() // 销毁播放实例
flvPlayer.value.off('error') // 移除错误处理程序
flvPlayer.value = null
}
const handleCanle = () => {
visible.value = false
destroy()
}
const show = (status?: any) => {
visible.value = status
}
onMounted(() => {
//
})
defineExpose({
show,
popup
})
</script>
<style lang="less" scoped>
/* 样式可以保持为空 */
</style>
解决的关键是当弹框关闭的时候,直接调用flv的方法的话不能直接销毁的话,没有完全销毁这个video,然后刚刚开始我给他加了一个key的值,然后创建一个flv对象的时候就自增,看看还会不会报错,结果没有报错,是可以正常获取到视频的,但是视频没有显示出来,然后我加多一个v-if,关闭弹框就把整个video销毁了,然后就没有报错了。
因此总结了一下:之所以会报这个错误的话是因为没有完全销毁这个组件,最简单的做法就是把整个video销毁掉。