目录
flvjs与FLV有什么区别和联系?
flv.js
是 HTML5 Flash 视频(FLV)播放器,纯原生 JavaScript 开发,没有用到 Flash。由 bilibili 网站开源(Github)。
概览:
一个实现了在 HTML5 视频中播放 FLV 格式视频的 JavaScript 库。它的工作原理是将 FLV 文件流转码复用成 ISO BMFF(MP4 碎片)片段,然后通过 Media Source Extensions 将 MP4 片段喂进浏览器。
flv.js 是使用 ECMAScript 6 编写的,然后通过 Babel Compiler 编译成 ECMAScript 5,使用 Browserify 打包。
功能:
FLV 容器,具有 H.264 + AAC 编解码器播放功能
多部分分段视频播放
HTTP FLV 低延迟实时流播放
FLV 通过 WebSocket 实时流播放
兼容 Chrome, FireFox, Safari 10, IE11 和 Edge
十分低开销,并且通过你的浏览器进行硬件加速
FLV
HTTP FLV则是将RTMP封装在HTTP协议之上的,可以更好的穿透防火墙等。rtmp和http-flv的视频格式都是flv格式的,只是传输协议而不同。rtmp是tcp的传输协议,而http-flv是http长链接的传输协议。
总结
flvjs是一个H5播放器。FLV是一种协议。flvjs可以用于播放FLV格式的视频。
几种视频流比较。
协议 | http-flv | rtmp | hls |
传输方式 | http流 | tcp流 | http流 |
视频封装格式 | flv | flv | Ts文件 |
延迟 | 低 | 低 | 高 |
数据分段 | 连续流 | 连续流 | 切片文件 |
h5播放 | flv.js | video.js | hls.js |
vue中使用flvjs
1.使用npm安装flv.js
npm install --save flv.js
2.新建FlvLive.vue文件,在文件中引入
import flvjs from 'flv.js'
<script>
if (flvjs.isSupported()) { // 判断当前浏览器是否支持flv。
var videoElement = document.getElementById('videoElement');
var flvPlayer = flvjs.createPlayer({
type: 'flv',
// isLive: true,
// hasAudio: false,
url:'http://127.0.0.1:8000/live/abc.flv'
});
flvPlayer.attachMediaElement(videoElement); // 挂载video标签。
flvPlayer.load();
flvPlayer.play(); // 播放
}
</script>
封装一个flv函数
function playVideo(demo, url) {
demo = document.getElementById(demo);
if (demo) {
demo.pause()
demo.unload()
demo.detachMediaElement()
demo.destroy()
demo = null
}
if (flvjs.isSupported()) {
var flvPlayer = flvjs.createPlayer({
type: 'flv',
hasAudio: false,
url: url
});
flvPlayer.attachMediaElement(demo);
flvPlayer.load(); //加载
}
demo.play();
//断开流链接,若不断开会一直占用带宽
function destoryVideo() {
this.flvPlayer.pause();
this.flvPlayer.unload();
this.flvPlayer.detachMediaElement();
this.flvPlayer.destroy();
this.flvPlayer = null;
},
H265格式请使用Jessibuca
支持h265和h264格式
GitHub - langhuihui/jessibuca: Jessibuca是一款开源的纯H5直播流播放器
配置项:
let isShow = true
window.flvPlayer = new Jessibuca({
container: demo,
autoWasm: true,
background: "",
controlAutoHide: false,
debug: false,
decoder: "static/js/jessibuca/decoder.js",
forceNoOffscreen: true,
hasAudio: false,
hasVideo: true,
heartTimeout: 5,
heartTimeoutReplay: true,
heartTimeoutReplayTimes: 3,
hiddenAutoPause: false,
hotKey: false,
isFlv: false,
isFullResize: false,
isNotMute: false,
isResize: false,
keepScreenOn: false,
loadingText: "请稍等, 视频加载中......",
loadingTimeout: 10,
loadingTimeoutReplay: true,
loadingTimeoutReplayTimes: 3,
openWebglAlignment: false,
operateBtns: {
fullscreen: isShow,
screenshot: isShow,
play: isShow,
audio: isShow,
record: false
},
recordType: "webm",
rotate: 0,
showBandwidth: false,
supportDblclickFullscreen: false,
timeout: 10,
useMSE: true, //pc端true 渲染为canvas标签,false为video标签;移动端与之相反
useOffscreen: false,
useWCS: true,
useWebFullScreen: false,
videoBuffer: 0,
wasmDecodeAudioSyncVideo: true,
wasmDecodeErrorReplay: true,
wcsUseVideoRender: true
},);
Android端webView灰色按钮(默认的播放按钮)问题
android端自动起播在首帧出来之前会有一个灰色的播放按钮闪现,不同的手机或者android版本会略有不同,这个是webview中video内置的poster导致,前端无法隐藏
方案:
1.先设置useMSE:false保证是video标签渲染
1.设置Video的poster属性为一个透明的图片 推荐使用poster="https://via.placeholder.com/1x1"
document.querySelector('video').poster="https://via.placeholder.com/1x1"
// 透明 base64
<video poster="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" />
// or
<video poster="https://via.placeholder.com/1x1" />
// or
<video poster="noposter" />
poster="" 直接给空字符串会被忽略,所以需要设置一个透明的图片或者noposter
封装多画面组件
效果图:
cameraPlays.vue
<!-- 多画面播放视频 -->
<template>
<div style="height: 100%; width: 100%" class="video_box">
<transition name="modal" tag="div">
<div style="height: 100%; width: 100%" v-if="visible">
<div class="" style="height: 100%; width: 100%">
<div
class="user_skills comm-item"
@mouseenter="onHover"
@mouseleave="onLeave"
style="
width: 100%;
padding-top: 0px;
height: 103%;
position: relative;
"
>
<div
style="width: 100%; height: 96%; z-index: 9"
class="cameraPlay avatar video-avatar"
></div>
<slot></slot>
</div>
</div>
</div>
</transition>
<!-- 全屏视频监控 -->
<transition name="modal" tag="div">
<div id="center" style="z-index: 99999" v-if="hasShowVideo">
<div class="video_dialog fullBox" :style="{ left: left, top: top }">
<div id="dialog_title" style="z-index: 99">
<div
class="dialog_name"
style="
margin-top: -3px;
line-height: 98px;
text-align: center;
font-size: 46px;
"
>
视频监控
</div>
<div class="close" @click="closeFullVideo">X</div>
</div>
<div
class="user_skills comm-item"
style="width: 100%; padding-top: 0px; height: 103%"
>
<div
style="width: 100%; height: 96%"
id=""
class="cameraPlayFull avatar video-avatar"
></div>
<div class="video-control cameraPop">
<div
class="top"
@mousedown="cameraControlDiversion('up')"
@mouseup="cameraControlDiversion('up', 0)"
></div>
<div
class="right"
@mousedown="cameraControlDiversion('right')"
@mouseup="cameraControlDiversion('right', 0)"
></div>
<div class="content"></div>
<div
class="bottom"
@mousedown="cameraControlDiversion('down')"
@mouseup="cameraControlDiversion('down', 0)"
></div>
<div
class="left"
@mousedown="cameraControlDiversion('left')"
@mouseup="cameraControlDiversion('left', 0)"
></div>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script type="text/ecmascript-6">
import qxUtils from "@baseJs/utils";
import url from "@baseJs/interface";
import qxParams from "@baseJs/params";
import axios from "axios";
let reg = /^ws(s)?:\/\/(.*?)\//,
zxCamera = null;
function initData() {
cameraId = qxParams.cameraObj.id;
deviceId = qxParams.cameraObj.deviceId;
channelNo = qxParams.cameraObj.channelNo;
liveUrl = qxParams.cameraObj.liveUrl;
sourceType = qxParams.cameraObj.sourceType;
}
function create(demo) {
let isShow = true
let flvPlayer = "flvPlayer_" + demo;
window[flvPlayer] = new Jessibuca({
container: demo,
autoWasm: true,
background: "",
controlAutoHide: false,
debug: false,
decoder: "static/js/jessibuca/decoder.js",
forceNoOffscreen: true,
hasAudio: false,
hasVideo: true,
heartTimeout: 5,
heartTimeoutReplay: true,
heartTimeoutReplayTimes: 3,
hiddenAutoPause: false,
hotKey: false,
isFlv: false,
isFullResize: false,
isNotMute: false,
isResize: false,
keepScreenOn: false,
loadingText: "请稍等, 视频加载中......",
loadingTimeout: 10,
loadingTimeoutReplay: true,
loadingTimeoutReplayTimes: 3,
openWebglAlignment: false,
operateBtns: {
fullscreen: isShow,
screenshot: isShow,
play: isShow,
audio: isShow,
record: false,
},
recordType: "webm",
rotate: 0,
showBandwidth: false,
supportDblclickFullscreen: true,
timeout: 10,
useMSE: true,
useOffscreen: false,
useWCS: true,
useWebFullScreen: false,
videoBuffer: 0,
wasmDecodeAudioSyncVideo: true,
wasmDecodeErrorReplay: true,
wcsUseVideoRender: true,
});
[flvPlayer].onLog = (msg) => console.error(msg);
[flvPlayer].onRecord = (status) => console.log("onRecord", status);
[flvPlayer].onPause = () => console.log("onPause");
[flvPlayer].onPlay = () => console.log("onPlay");
[flvPlayer].onFullscreen = (msg) => console.log("onFullscreen", msg);
[flvPlayer].onMute = (msg) => console.log("onMute", msg);
}
class ZxControlDiversion {
constructor(...args) {
let { id: cameraId, deviceId, channelNo, liveUrl, sourceType } = args[0]
this.deviceId = deviceId
this.channelNo = channelNo
this.liveUrl = liveUrl
this.sourceType = sourceType
this.cameraId = cameraId
}
loginSPPT() {
return new Promise((resolve, reject) => {
axios
.get(url.ZX_USER_LOGIN, {
params: {
password: "52cad73a70f28ce0bc5858be2283e415",
username: "zxsl",
// username: "admin"
},
})
.then((res) => {
if (res.data.code === 0) {
resolve(res);
}
});
});
}
playStart() {
return new Promise((resolve, reject) => {
if (qxParams.cameraObj.accessToken) {
// 设置请求头信息
const headers = {
"Content-Type": "application/json",
"Access-Token": qxParams.cameraObj.accessToken,
};
// 创建 axios 实例并设置默认的请求头
const instance = axios.create({
baseURL: `${url.ZX_PLAT_START}/${this.deviceId}/${this.channelNo}`,
timeout: 5000,
headers: headers,
});
instance
.get("")
.then((re) => {
if (re.data.code == 0) {
this.liveUrl = re.data.data.ws_flv;
// console.log(qxParams.cameraObj)
resolve(re);
}
reject(re.data.msg);
})
.catch((err) => {
reject(err.msg);
});
} else {
this.loginSPPT().then((res) => {
qxParams.cameraObj.accessToken = res.data.data.accessToken;
// 设置请求头信息
const headers = {
"Content-Type": "application/json",
"Access-Token": qxParams.cameraObj.accessToken,
};
// 创建 axios 实例并设置默认的请求头
const instance = axios.create({
baseURL: `${url.ZX_PLAT_START}/${this.deviceId}/${this.channelNo}`,
timeout: 5000,
headers: headers,
});
instance
.get("")
.then((re) => {
if (re.data.code == 0) {
this.liveUrl = re.data.data.ws_flv;
console.log(`this.deviceId ==>${this.deviceId}/${this.channelNo}`, this.liveUrl);
resolve(re);
}
reject(re.data.msg);
})
.catch((err) => {
reject(err.msg);
});
});
}
});
}
playStop() {
// 设置请求头信息
const headers = {
"Content-Type": "application/json",
"Access-Token": qxParams.cameraObj.accessToken,
};
// 创建 axios 实例并设置默认的请求头
const instance = axios.create({
baseURL: `${url.ZX_PLAT_STOP}/${this.deviceId}/${this.channelNo}`,
timeout: 1500,
headers: headers,
});
instance.get("").then((r) => { });
}
// command八项控制指令,允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop
static zxControl(type) {
let data = {
command: type,
};
axios.post(
`${url.ZX_CONTROL}/${this.deviceId}/${this.channelNo}`,
toFormData(Object.assign(data, controlData)),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Access-Token": qxParams.cameraObj.accessToken,
},
}
);
}
// 预置点控制
static zxPresetControl(type, index) {
let code;
let data = {};
switch (type) {
case "rename": //设置
code = 129;
data = {
cmdCode: code,
parameter1: 0,
parameter2: index,
combindCode2: 0,
};
break;
case "move":
code = 130;
data = {
cmdCode: code,
parameter1: 0,
parameter2: index,
combindCode2: 0,
};
break;
case "del":
params.$this.$message({
message: "暂不支持删除",
offset: 100,
});
return;
code = 131;
break;
default:
break;
}
// let data = {
// command: code,
// parameter2: index,
// }
// let data1 = Object.assign(data, presetControlData)
axios.post(
`${url.ZX_FRONT_END_COMMAND}/${deviceId}/${channelNo}`,
toFormData(data),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Access-Token": qxParams.cameraObj.accessToken,
},
}
);
}
// 巡航控制
static zxCruiseControl(type, index) {
let data = {};
switch (type) {
case "moveCruise":
data = {
cmdCode: 136,
parameter1: index,
parameter2: 0,
combindCode2: 0,
};
break;
default:
break;
}
axios.post(
`${url.ZX_FRONT_END_COMMAND}/${deviceId}/${channelNo}`,
toFormData(data),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Access-Token": qxParams.cameraObj.accessToken,
},
}
);
}
static zxGetCruisePath(fun) {
qxParams.cameraObj.presetList = [{ presetID: 1 }];
fun(qxParams.cameraObj.presetList);
}
}
// 点播/发送心跳
function guohandleEventOther(...args) {
return new Promise((resolve, reject) => {
if (window.flvPlayer && params.playType == "jessibuca") {
jessDestroy();
}
let { id: cameraId, deviceId, channelNo, liveUrl, sourceType } = args[0]
if (sourceType * 1 === 1) {
zxCamera
.playStart()
.then((res) => {
console.log("res===>", res);
resolve(res);
})
.catch((err) => {
// showError('点播超时')
});
}
if (sourceType * 1 === 2) {
setInterval(() => {
// let newUrl = replaceCameraUrl(url.OUTSIDE_V1_CAMERA_HOLDING)
let newUrl = url.OUTSIDE_V1_CAMERA_HOLDING;
axios.get(newUrl, {
params: {
cameraId,
},
});
}, 10000);
// promise(1)
resolve(1);
}
});
}
function getInternet() {
if (params.isInternet !== null) return;
return new Promise((resolve, reject) => {
const instance = axios.create({
baseURL:
"https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css",
timeout: 1500,
});
instance
.get("")
.then((res) => {
params.isInternet = true;
resolve(true);
})
.catch((err) => {
params.isInternet = false;
// params.isInternet = true
reject(false);
});
});
}
function getCameraReplace() {
if (params.cameraReplaceUrl !== null) return;
return new Promise(async (resolve, reject) => {
let res = await axios.get(url.GET_CONFIG_BY_CODE, {
params: {
code: "replacePreLiveUrl",
},
});
if (res.data.code == "0") {
if (res.data.data !== null) {
params.cameraReplaceUrl = res.data.data.value;
} else {
params.cameraReplaceUrl = "";
}
}
console.log("object :>> ", res.data);
resolve(params.cameraReplaceUrl);
});
}
function jessDestroy() {
// liveUrl = null
// window.flvPlayer.destroy()
// window.flvPlayer = null
zxCamera.playStop();
}
function toFormData(data) {
let tmp = new FormData();
for (var key in data) {
tmp.append(key, data[key])
}
// tmp.append("regionIds","");
return tmp
}
export default {
name: "cameraPaly",
props: {
boxId: {
required: true,
type: String,
},
left: {
type: String,
default: "7%",
},
top: {
type: String,
default: "50px",
},
},
data() {
return {
isShowControl: true,
visible: false,
cameraInfo: {},
fullData: {},
hasShowVideo: false,
isCloseVideo: false,
};
},
components: {},
created() { },
mounted() { },
methods: {
showFullVideo(data) {
console.log("data==>", data);
this.hasShowVideo = true;
if (data) {
this.cameraInfo = data;
this.isCloseVideo = true;
console.log("playVideo", this.cameraInfo);
this.$nextTick(() => {
let dom = document.querySelector(".cameraPlayFull");
dom.setAttribute("id", this.boxId + "Full");
if (this.cameraInfo) {
let { id, channelNo, deviceId, liveUrl, lng, lat, sourceType } =
this.cameraInfo;
qxParams.cameraObj.id = id;
qxParams.cameraObj.channelNo = channelNo;
qxParams.cameraObj.deviceId = deviceId;
qxParams.cameraObj.liveUrl = liveUrl;
qxParams.cameraObj.sourceType = sourceType;
qxParams.cameraObj.lng = lng;
qxParams.cameraObj.lat = lat;
}
qxUtils.guohandleEventOther().then((res) => {
let dom = document.getElementById(this.boxId + "Full");
if (dom) {
dom.style.background = "none";
qxUtils.playLive(this.boxId + "Full");
}
});
});
} else {
this.$nextTick(() => {
let dom = document.querySelector(".cameraPlayFull");
dom.setAttribute("id", this.boxId + "Full");
console.log(dom);
qxUtils.guohandleEventOther().then((res) => {
let dom = document.getElementById(this.boxId + "Full");
if (dom) {
console.log(dom);
dom.style.background = "none";
qxUtils.playLive(this.boxId + "Full");
}
});
});
}
},
closeVideo() {
this.hasShowVideo = false;
this.visible = true;
this.$emit("closeFullVideoBox");
},
closeFullVideo() {
if (!this.isCloseVideo) {
this.hasShowVideo = false;
this.visible = true;
this.$nextTick(() => {
qxUtils.guohandleEventOther().then((res) => {
let dom = document.getElementById(this.boxId);
if (dom) {
dom.style.background = "none";
qxUtils.playLive(this.boxId);
}
});
});
return;
}
this.closeVideo();
},
playVideo(cameraInfo,parentName) {
this.fullData = cameraInfo;
this.visible = true;
this.$nextTick(() => {
let dom = document.querySelector(`${parentName} .cameraPlay`);
// let dom = document.querySelector(`.cameraPlay`);
// console.log('this.boxId==>',this.boxId)
dom.setAttribute("id", this.boxId);
if (cameraInfo) {
zxCamera = new ZxControlDiversion(cameraInfo)
guohandleEventOther(cameraInfo).then((res) => {
let dom = document.getElementById(this.boxId);
// console.log('dom==>',dom,res.data.data.flv)
if (dom) {
this.playLive(this.boxId, res.data.data.flv);
}
});
}
});
},
playLive(boxId,liveUrl ) {
let demo = document.getElementById(boxId);
// if (window.flvPlayer) {
// this.jessDestroy(demo);
// }
// 调用播放
create(demo);
let flvPlayer = "flvPlayer_" + demo;
window[flvPlayer].play(liveUrl);
},
cameraControlDiversion(...args) {
let { channelNo, deviceId } = args[0];
let { type, status } = args[1];
let data = {
command: type,
};
let controlData = {
horizonSpeed: 100,
verticalSpeed: 100,
zoomSpeed: 30
}
if (status === 0) {
data.command ='stop'
axios.post(
`${url.ZX_CONTROL}/${deviceId}/${channelNo}`,
toFormData(Object.assign(data, controlData)),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Access-Token": qxParams.cameraObj.accessToken,
},
}
);
}else {
axios.post(
`${url.ZX_CONTROL}/${deviceId}/${channelNo}`,
toFormData(Object.assign(data, controlData)),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Access-Token": qxParams.cameraObj.accessToken,
},
}
);
}
},
onHover() {
this.isShowControl = true;
},
onLeave() {
// this.isShowControl = false;
},
},
};
</script>
<style scoped>
.fullBox {
padding: 0 20px 20px;
}
.top,
.left,
.right,
.bottom {
z-index: 99991;
}
</style>
index.vue
<template>
<div>
<div class="item_con" style="padding:0">
<div
id="videoPlay1"
style="width: 456px;height: 231px;margin: 20px;"
@mouseenter="onHover"
@mouseleave="onLeave"
>
<cameraPlay :boxId="'video_x1'" ref="cameraPalyRef1">
<div class="video-control cameraPop" v-if="isShowControl">
<div
class="top"
@mousedown="cameraControlDiversion1('up', 1)"
@mouseup="cameraControlDiversion1('up', 0)"
></div>
<div
class="right"
@mousedown="cameraControlDiversion1('right', 1)"
@mouseup="cameraControlDiversion1('right', 0)"
></div>
<div class="content"></div>
<div
class="bottom"
@mousedown="cameraControlDiversion1('down', 1)"
@mouseup="cameraControlDiversion1('down', 0)"
></div>
<div
class="left"
@mousedown="cameraControlDiversion1('left', 1)"
@mouseup="cameraControlDiversion1('left', 0)"
></div>
</div>
</cameraPlay>
</div>
</div>
</div>
</template>
<script type="text/ecmascript-6">
import cameraPlay from "./cameraPlays";
export default {
data() {
return {
isShowControl:false,
cameraInfo2: {
id: 0,
channelNo: "34020000001320000067",
cameraInfo1: {
id: 0,
channelNo: "34020000001320000067",
deviceId: "44010200492000000067",
liveUrl:
"http://58.144.221.11:30101/rtp/44010200492000000067_34020000001320000067.live.flv",
lng: "",
lat: "",
sourceType: 1
}
}
},
components: {
cameraPlay,
},
methods: {
onHover() {
this.isShowControl = true;
},
onLeave() {
this.isShowControl = false;
},
onHover2() {
this.isShowControl2 = true;
},
onLeave2() {
this.isShowControl2 = false;
},
cameraControlDiversion1(type, status) {
let data = {
type: type,
status: status
};
this.$refs.cameraPalyRef1.cameraControlDiversion(this.cameraInfo1, data);
},
cameraControlDiversion2(type, status) {
let data = {
type: type,
status: status
};
this.$refs.cameraPalyRef2.cameraControlDiversion(this.cameraInfo2, data);
},
}
}
</script>
<style lang="stylesheet/stylus"></style>