@ -0,0 +1,41 @@ |
|||||
|
import flvJs from "flv.js"; |
||||
|
import { getCameraStream } from "@screen/pages/Home/components/RoadAndEvents/utils/httpList.js"; |
||||
|
|
||||
|
/** |
||||
|
* |
||||
|
* @param {HTMLElement} container 容器 |
||||
|
* @param {{camId?: string; url?: string}?} options {camId: 相机ID; url: 直播地址} |
||||
|
* @returns |
||||
|
*/ |
||||
|
export async function openVideoStream(container, { camId, url } = {}) { |
||||
|
if (camId) { |
||||
|
const { code, data } = await getCameraStream(camId).catch(() => ({})); |
||||
|
|
||||
|
if (code != 200) return; |
||||
|
|
||||
|
url = data.liveUrl; |
||||
|
} |
||||
|
|
||||
|
if (!url) return; |
||||
|
|
||||
|
const flvPlayer = flvJs.createPlayer({ |
||||
|
type: "flv", |
||||
|
url: url, |
||||
|
isLive: true, |
||||
|
hasVideo: true, |
||||
|
hasAudio: true, |
||||
|
}); |
||||
|
|
||||
|
console.log( |
||||
|
"%c [ flvPlayer ]-26-「videoStream.js」", |
||||
|
"font-size:15px; background:#b2b540; color:#f6f984;", |
||||
|
flvPlayer |
||||
|
); |
||||
|
|
||||
|
flvPlayer.attachMediaElement(container); |
||||
|
|
||||
|
flvPlayer.load(); |
||||
|
flvPlayer.play(); |
||||
|
|
||||
|
return flvPlayer; |
||||
|
} |
@ -0,0 +1,166 @@ |
|||||
|
<template> |
||||
|
<div class="video-container"> |
||||
|
<div class="header"> |
||||
|
<ElSelect @change="showVideo" v-model="cameraId"> |
||||
|
<ElOption v-for="item in urls" |
||||
|
:key="item.id" :label="item.deviceName" :value="item.iotDeviceId"> |
||||
|
</ElOption> |
||||
|
</ElSelect> |
||||
|
</div> |
||||
|
<Transition name="fade" mode="out-in"> |
||||
|
<video controls autoplay muted class="video-stream" v-bind="$attrs" ref="videoContainerRef" /> |
||||
|
</Transition> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import { HttpLivePlayer, openLiveVideo } from "./videoStream.js" |
||||
|
|
||||
|
import { |
||||
|
getNearCameraNew, |
||||
|
} from "@screen/pages/Home/components/RoadAndEvents/utils/httpList.js"; |
||||
|
|
||||
|
export default { |
||||
|
name: 'VideoControls', |
||||
|
components: { |
||||
|
}, |
||||
|
props: { |
||||
|
// 桩号 |
||||
|
pileNum: { |
||||
|
type: String, |
||||
|
default: null |
||||
|
}, |
||||
|
rangeIndex: { |
||||
|
type: String, |
||||
|
default: null |
||||
|
}, |
||||
|
showHeader: { |
||||
|
type: Boolean, |
||||
|
default: true |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
active: "video", |
||||
|
player: null, |
||||
|
urls:[], |
||||
|
cameraId: null |
||||
|
} |
||||
|
}, |
||||
|
async mounted() { |
||||
|
this.playVideo(); |
||||
|
this.$once("hook:beforeDestroy", () => this.player?.destroy()); |
||||
|
}, |
||||
|
methods: { |
||||
|
|
||||
|
async playVideo() { |
||||
|
this.player?.destroy(); |
||||
|
|
||||
|
let {code,data} = await getNearCameraNew(this.pileNum).catch(() => ({})); |
||||
|
if ( |
||||
|
code != 200 || |
||||
|
(Array.isArray(data) ? !data?.length : !Object.keys(data || {}).length) |
||||
|
) { |
||||
|
Message.warning("未获取到附近的相机信息"); |
||||
|
return; |
||||
|
} |
||||
|
if(data[this.rangeIndex]){ |
||||
|
this.urls = data[this.rangeIndex] |
||||
|
this.cameraId = data[this.rangeIndex][0]['iotDeviceId'] |
||||
|
this.showVideo(); |
||||
|
} |
||||
|
}, |
||||
|
showVideo(){ |
||||
|
this.player = new HttpLivePlayer(this.$refs.videoContainerRef, { camId: this.cameraId}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang='scss' scoped> |
||||
|
.video-container { |
||||
|
position: relative; |
||||
|
background-color: #000; |
||||
|
height: 240px; |
||||
|
overflow: hidden; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
|
||||
|
.fade-enter-active, |
||||
|
.fade-leave-active { |
||||
|
transition: opacity 0.24s ease; |
||||
|
} |
||||
|
|
||||
|
.fade-enter-from, |
||||
|
.fade-leave-to { |
||||
|
opacity: 0; |
||||
|
} |
||||
|
|
||||
|
.header { |
||||
|
width: 100%; |
||||
|
z-index: 999; |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
.radio { |
||||
|
background: #265A70; |
||||
|
border-radius: 41px 41px 41px 41px; |
||||
|
overflow: hidden; |
||||
|
opacity: 1; |
||||
|
border: 1px solid #3DE8FF; |
||||
|
font-size: 12px; |
||||
|
// font-family: PingFang SC, PingFang SC; |
||||
|
font-weight: 400; |
||||
|
color: #FFFFFF; |
||||
|
line-height: 14px; |
||||
|
height: fit-content; |
||||
|
|
||||
|
// -webkit-background-clip: text; |
||||
|
// -webkit-text-fill-color: transparent; |
||||
|
|
||||
|
.active { |
||||
|
background-color: rgba(61, 232, 255, 1); |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
background-color: rgba(38, 90, 112, 1); |
||||
|
padding: 4px 9px; |
||||
|
display: inline-block; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
&:hover { |
||||
|
background-color: rgba(61, 232, 255, 1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.btn { |
||||
|
background: #265A70; |
||||
|
border-radius: 6px 6px 6px 6px; |
||||
|
opacity: 1; |
||||
|
border: 1px solid #3DE8FF; |
||||
|
font-size: 12px; |
||||
|
// font-family: PingFang SC, PingFang SC; |
||||
|
font-weight: 400; |
||||
|
color: #FFFFFF; |
||||
|
line-height: 14px; |
||||
|
padding: 3px 9px; |
||||
|
cursor: pointer; |
||||
|
// -webkit-background-clip: text; |
||||
|
// -webkit-text-fill-color: transparent; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
.video-stream, |
||||
|
img { |
||||
|
height: 100%; |
||||
|
max-height: 100%; |
||||
|
max-width: 100%; |
||||
|
} |
||||
|
|
||||
|
.video-stream { |
||||
|
width: 100%; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,238 @@ |
|||||
|
import flvJs from "flv.js"; |
||||
|
import mpegTsJs from "mpegts.js"; |
||||
|
import { Message } from "element-ui"; |
||||
|
// mpegts.js
|
||||
|
import { |
||||
|
getCameraStream, |
||||
|
getNearCamera, |
||||
|
getNearCameraNew, |
||||
|
} from "@screen/pages/Home/components/RoadAndEvents/utils/httpList.js"; |
||||
|
|
||||
|
const ErrorTypesCn = { |
||||
|
NetworkError: "网络错误", |
||||
|
MediaError: "媒体错误", |
||||
|
OtherError: "其他错误", |
||||
|
}; |
||||
|
/** |
||||
|
* flv 视频测试 |
||||
|
* https://bilibili.github.io/flv.js/demo/
|
||||
|
* https://www.zngg.net/tool/detail/FlvPlayer
|
||||
|
* https://xqq.im/mpegts.js/demo/arib.html
|
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* flv 视频流 |
||||
|
*/ |
||||
|
const testFlvUrl = |
||||
|
"https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/flv/xgplayer-demo-720p.flv"; |
||||
|
// "https://v17.dogevideo.com/vcloud/17/v/20190424/1556036075_818c4125ec9c8cbc7a7a8a7cc1601512/1057/e51d88e79732fb952ebdbb4a57aa628a.flv?vkey=D9EAC2&tkey=17062414799ec7c466dc&auth_key=1706255879-zQK6JYToEeRiUark-0-6e363fb7709e783e64efc919d2267bdc";
|
||||
|
|
||||
|
/** |
||||
|
* |
||||
|
* @param {HTMLElement} container 容器 |
||||
|
* @param {{camId?: string; url?: string}?} options {camId: 相机ID; url: 直播地址} |
||||
|
* @returns |
||||
|
*/ |
||||
|
export async function openVideoStream(container, { camId, url } = {}) { |
||||
|
if (camId) { |
||||
|
const { code, data } = await getCameraStream(camId).catch(() => ({})); |
||||
|
|
||||
|
if (code != 200) return; |
||||
|
|
||||
|
url = data.liveUrl; |
||||
|
} |
||||
|
|
||||
|
if (!url) return; |
||||
|
|
||||
|
// console.log(flvJs.getFeatureList().mseLivePlayback);
|
||||
|
|
||||
|
const player = flvJs.createPlayer({ |
||||
|
type: "flv", |
||||
|
url: url, |
||||
|
isLive: true, |
||||
|
hasVideo: true, |
||||
|
hasAudio: true, |
||||
|
}); |
||||
|
|
||||
|
player.attachMediaElement(container); |
||||
|
|
||||
|
player.load(); |
||||
|
player.play(); |
||||
|
|
||||
|
player.on(flvJs.Events.ERROR, (e) => {}); |
||||
|
|
||||
|
return player; |
||||
|
} |
||||
|
|
||||
|
async function getUrl({ camId} = {}) { |
||||
|
|
||||
|
const { code, data } = await getCameraStream(camId).catch(() => ({})); |
||||
|
if (code != 200) { |
||||
|
Message.warning("未获取到当前相机的播放地址"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
let url = data.liveUrl; |
||||
|
if (!url) { |
||||
|
Message.warning("未获取到当前相机的播放地址"); |
||||
|
return Promise.reject("获取 url 失败!"); |
||||
|
} |
||||
|
|
||||
|
return url; |
||||
|
} |
||||
|
export class HttpLivePlayer { |
||||
|
/** |
||||
|
* @type { flvJs.Player } |
||||
|
*/ |
||||
|
player; |
||||
|
|
||||
|
/** |
||||
|
* @type { HTMLVideoElement } |
||||
|
*/ |
||||
|
container; |
||||
|
|
||||
|
url; |
||||
|
|
||||
|
// 解码 帧
|
||||
|
lastDecodedFrames; |
||||
|
|
||||
|
constructor(container, options) { |
||||
|
this.container = container; |
||||
|
if (!flvJs.getFeatureList().mseLiveFlvPlayback) |
||||
|
return Message.error("浏览器不支持播放 flv 视频流"); |
||||
|
getUrl(options).then((url) => { |
||||
|
this.url = url; |
||||
|
this.initLiveVideo(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
destroy() { |
||||
|
if (!this.player) return; |
||||
|
|
||||
|
this.player.pause(); |
||||
|
this.player.unload(); |
||||
|
this.player.detachMediaElement(); |
||||
|
this.player.destroy(); |
||||
|
this.player = null; |
||||
|
} |
||||
|
|
||||
|
initLiveVideo() { |
||||
|
this.destroy(); |
||||
|
this.player = null; |
||||
|
this.lastDecodedFrames = null; |
||||
|
|
||||
|
if (!this.url) return; |
||||
|
|
||||
|
this.player = flvJs.createPlayer( |
||||
|
{ |
||||
|
type: "flv", |
||||
|
url: this.url, |
||||
|
isLive: true, |
||||
|
}, |
||||
|
{ |
||||
|
autoCleanupSourceBuffer: true, |
||||
|
// enableWorker: true, // 启用分离的线程进行转换
|
||||
|
enableStashBuffer: false, // 关闭IO隐藏缓冲区 如果您需要实时(最小延迟)来进行实时流播放,则设置为false
|
||||
|
stashInitialSize: 128, |
||||
|
isLive: true, |
||||
|
lazyLoad: true, |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
this.player.attachMediaElement(this.container); |
||||
|
|
||||
|
this.player.load(); |
||||
|
this.player.play(); |
||||
|
|
||||
|
// this.container.addEventListener("progress", () => {
|
||||
|
// let end = this.player.buffered.end(0); //获取当前buffered值(缓冲区末尾)
|
||||
|
// let delta = end - this.player.currentTime; //获取buffered与当前播放位置的差值
|
||||
|
|
||||
|
// // 延迟过大,通过跳帧的方式更新视频
|
||||
|
// if (delta > 10 || delta < 0) {
|
||||
|
// this.player.currentTime = this.player.buffered.end(0) - 1;
|
||||
|
// return;
|
||||
|
// }
|
||||
|
|
||||
|
// // 追帧
|
||||
|
// if (delta > 1) {
|
||||
|
// this.container.playbackRate = 1.1;
|
||||
|
// } else {
|
||||
|
// this.container.playbackRate = 1;
|
||||
|
// }
|
||||
|
// });
|
||||
|
|
||||
|
this.player.on(flvJs.Events.ERROR, (errorType, errorDetail, errorInfo) => { |
||||
|
console.log("errorType", errorType); |
||||
|
console.log("errorDetail", errorDetail); |
||||
|
console.log("errorInfo", errorInfo); |
||||
|
Message.warning( |
||||
|
`视频流加载失败, ${ErrorTypesCn[errorType] || "其他错误"}` |
||||
|
); |
||||
|
this.initLiveVideo(); |
||||
|
}); |
||||
|
|
||||
|
// 视频断流
|
||||
|
this.player.on(flvJs.Events.STATISTICS_INFO, (res) => { |
||||
|
if (this.lastDecodedFrames != res.decodedFrames) { |
||||
|
this.lastDecodedFrames = res.decodedFrames; |
||||
|
} else { |
||||
|
this.lastDecodedFrames = 0; |
||||
|
// this.destroy();
|
||||
|
// this.initLiveVideo();
|
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* https://juejin.cn/post/6855577308271476743
|
||||
|
* https://www.cnblogs.com/xiahj/p/flvExtend.html
|
||||
|
* 使用 mpegTsJs |
||||
|
* @param {*} container |
||||
|
* @param {*} options |
||||
|
*/ |
||||
|
export async function openLiveVideo(container, options) { |
||||
|
if (!mpegTsJs.getFeatureList().mseLivePlayback) |
||||
|
return Message.error("浏览器不支持播放 flv 视频流"); |
||||
|
const url = await getUrl(options).catch(() => {}); |
||||
|
console.log( |
||||
|
"%c [ url ]-212-「videoStream.js」", |
||||
|
"font-size:15px; background:#6f87e8; color:#b3cbff;", |
||||
|
url, |
||||
|
options |
||||
|
); |
||||
|
|
||||
|
if (!url) return; |
||||
|
|
||||
|
const player = mpegTsJs.createPlayer( |
||||
|
{ |
||||
|
type: "flv", |
||||
|
url, |
||||
|
isLive: true, |
||||
|
}, |
||||
|
{ |
||||
|
autoCleanupSourceBuffer: true, |
||||
|
enableWorker: true, // 启用分离的线程进行转换
|
||||
|
// enableStashBuffer: false, // 关闭IO隐藏缓冲区 如果您需要实时(最小延迟)来进行实时流播放,则设置为false
|
||||
|
// stashInitialSize: 128,
|
||||
|
isLive: true, |
||||
|
lazyLoad: true, |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
player.attachMediaElement(container); |
||||
|
|
||||
|
container.addEventListener("play", () => { |
||||
|
try { |
||||
|
let end = player.buffered.end(0) - 1; |
||||
|
|
||||
|
player.currentTime = end; |
||||
|
} catch (error) {} |
||||
|
}); |
||||
|
|
||||
|
player.load(); |
||||
|
player.play(); |
||||
|
|
||||
|
return player; |
||||
|
} |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |