feat:视频列表更换播放方式

This commit is contained in:
zjc
2025-02-21 13:40:03 +08:00
parent d547312c74
commit 23fbbcdb78
8 changed files with 3894 additions and 38 deletions

3514
src/assets/adapter.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 KiB

View File

@@ -0,0 +1,315 @@
var WebRtcStreamer = (function() {
/**
* Interface with WebRTC-streamer API
* @constructor
* @param {string} videoElement - id of the video element tag
* @param {string} srvurl - url of webrtc-streamer (default is current location)
*/
var WebRtcStreamer = function WebRtcStreamer (videoElement, srvurl) {
if (typeof videoElement === "string") {
this.videoElement = document.getElementById(videoElement);
} else {
this.videoElement = videoElement;
}
this.srvurl = srvurl || location.protocol+"//"+window.location.hostname+":"+window.location.port;
this.pc = null;
this.mediaConstraints = { offerToReceiveAudio: true, offerToReceiveVideo: true };
this.iceServers = null;
this.earlyCandidates = [];
}
WebRtcStreamer.prototype._handleHttpErrors = function (response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
/**
* Connect a WebRTC Stream to videoElement
* @param {string} videourl - id of WebRTC video stream
* @param {string} audiourl - id of WebRTC audio stream
* @param {string} options - options of WebRTC call
* @param {string} stream - local stream to send
* @param {string} prefmime - prefered mime
*/
WebRtcStreamer.prototype.connect = function(videourl, audiourl, options, localstream, prefmime) {
this.disconnect();
// getIceServers is not already received
if (!this.iceServers) {
console.log("Get IceServers");
fetch(this.srvurl + "/api/getIceServers")
.then(this._handleHttpErrors)
.then( (response) => (response.json()) )
.then( (response) => this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream, prefmime))
.catch( (error) => this.onError("getIceServers " + error ))
} else {
this.onReceiveGetIceServers(this.iceServers, videourl, audiourl, options, localstream, prefmime);
}
}
/**
* Disconnect a WebRTC Stream and clear videoElement source
*/
WebRtcStreamer.prototype.disconnect = function() {
if (this.videoElement?.srcObject) {
this.videoElement.srcObject.getTracks().forEach(track => {
track.stop()
this.videoElement.srcObject.removeTrack(track);
});
}
if (this.pc) {
fetch(this.srvurl + "/api/hangup?peerid=" + this.pc.peerid)
.then(this._handleHttpErrors)
.catch( (error) => this.onError("hangup " + error ))
try {
this.pc.close();
}
catch (e) {
console.log ("Failure close peer connection:" + e);
}
this.pc = null;
}
}
/*
* GetIceServers callback
*/
WebRtcStreamer.prototype.onReceiveGetIceServers = function(iceServers, videourl, audiourl, options, stream, prefmime) {
this.iceServers = iceServers;
this.pcConfig = iceServers || {"iceServers": [] };
try {
this.createPeerConnection();
var callurl = this.srvurl + "/api/call?peerid=" + this.pc.peerid + "&url=" + encodeURIComponent(videourl);
if (audiourl) {
callurl += "&audiourl="+encodeURIComponent(audiourl);
}
if (options) {
callurl += "&options="+encodeURIComponent(options);
}
if (stream) {
this.pc.addStream(stream);
}
// clear early candidates
this.earlyCandidates.length = 0;
// create Offer
this.pc.createOffer(this.mediaConstraints).then((sessionDescription) => {
console.log("Create offer:" + JSON.stringify(sessionDescription));
console.log(`video codecs:${Array.from(new Set(RTCRtpReceiver.getCapabilities("video")?.codecs?.map(codec => codec.mimeType)))}`)
console.log(`audio codecs:${Array.from(new Set(RTCRtpReceiver.getCapabilities("audio")?.codecs?.map(codec => codec.mimeType)))}`)
if (prefmime != undefined) {
//set prefered codec
const [prefkind] = prefmime.split('/');
const codecs = RTCRtpReceiver.getCapabilities(prefkind).codecs;
const preferedCodecs = codecs.filter(codec => codec.mimeType === prefmime);
console.log(`preferedCodecs:${JSON.stringify(preferedCodecs)}`);
this.pc.getTransceivers().filter(transceiver => transceiver.receiver.track.kind === prefkind).forEach(tcvr => {
if(tcvr.setCodecPreferences != undefined) {
tcvr.setCodecPreferences(preferedCodecs);
}
});
}
this.pc.setLocalDescription(sessionDescription)
.then(() => {
fetch(callurl, { method: "POST", body: JSON.stringify(sessionDescription) })
.then(this._handleHttpErrors)
.then( (response) => (response.json()) )
.catch( (error) => this.onError("call " + error ))
.then( (response) => this.onReceiveCall(response) )
.catch( (error) => this.onError("call " + error ))
}, (error) => {
console.log ("setLocalDescription error:" + JSON.stringify(error));
});
}, (error) => {
alert("Create offer error:" + JSON.stringify(error));
});
} catch (e) {
this.disconnect();
alert("connect error: " + e);
}
}
WebRtcStreamer.prototype.getIceCandidate = function() {
fetch(this.srvurl + "/api/getIceCandidate?peerid=" + this.pc.peerid)
.then(this._handleHttpErrors)
.then( (response) => (response.json()) )
.then( (response) => this.onReceiveCandidate(response))
.catch( (error) => this.onError("getIceCandidate " + error ))
}
/*
* create RTCPeerConnection
*/
WebRtcStreamer.prototype.createPeerConnection = function() {
console.log("createPeerConnection config: " + JSON.stringify(this.pcConfig));
this.pc = new RTCPeerConnection(this.pcConfig);
var pc = this.pc;
pc.peerid = Math.random();
pc.onicecandidate = (evt) => this.onIceCandidate(evt);
pc.onaddstream = (evt) => this.onAddStream(evt);
pc.oniceconnectionstatechange = (evt) => {
console.log("oniceconnectionstatechange state: " + pc.iceConnectionState);
if (this.videoElement) {
if (pc.iceConnectionState === "connected") {
this.videoElement.style.opacity = "1.0";
}
else if (pc.iceConnectionState === "disconnected") {
this.videoElement.style.opacity = "0.25";
}
else if ( (pc.iceConnectionState === "failed") || (pc.iceConnectionState === "closed") ) {
this.videoElement.style.opacity = "0.5";
} else if (pc.iceConnectionState === "new") {
this.getIceCandidate();
}
}
}
pc.ondatachannel = function(evt) {
console.log("remote datachannel created:"+JSON.stringify(evt));
evt.channel.onopen = function () {
console.log("remote datachannel open");
this.send("remote channel openned");
}
evt.channel.onmessage = function (event) {
console.log("remote datachannel recv:"+JSON.stringify(event.data));
}
}
try {
var dataChannel = pc.createDataChannel("ClientDataChannel");
dataChannel.onopen = function() {
console.log("local datachannel open");
this.send("local channel openned");
}
dataChannel.onmessage = function(evt) {
console.log("local datachannel recv:"+JSON.stringify(evt.data));
}
} catch (e) {
console.log("Cannor create datachannel error: " + e);
}
console.log("Created RTCPeerConnnection with config: " + JSON.stringify(this.pcConfig) );
return pc;
}
/*
* RTCPeerConnection IceCandidate callback
*/
WebRtcStreamer.prototype.onIceCandidate = function (event) {
if (event.candidate) {
if (this.pc.currentRemoteDescription) {
this.addIceCandidate(this.pc.peerid, event.candidate);
} else {
this.earlyCandidates.push(event.candidate);
}
}
else {
console.log("End of candidates.");
}
}
WebRtcStreamer.prototype.addIceCandidate = function(peerid, candidate) {
fetch(this.srvurl + "/api/addIceCandidate?peerid="+peerid, { method: "POST", body: JSON.stringify(candidate) })
.then(this._handleHttpErrors)
.then( (response) => (response.json()) )
.then( (response) => {console.log("addIceCandidate ok:" + response)})
.catch( (error) => this.onError("addIceCandidate " + error ))
}
/*
* RTCPeerConnection AddTrack callback
*/
WebRtcStreamer.prototype.onAddStream = function(event) {
console.log("Remote track added:" + JSON.stringify(event));
this.videoElement.srcObject = event.stream;
var promise = this.videoElement.play();
if (promise !== undefined) {
promise.catch((error) => {
console.warn("error:"+error);
this.videoElement.setAttribute("controls", true);
});
}
}
/*
* AJAX /call callback
*/
WebRtcStreamer.prototype.onReceiveCall = function(dataJson) {
console.log("offer: " + JSON.stringify(dataJson));
var descr = new RTCSessionDescription(dataJson);
this.pc.setRemoteDescription(descr).then(() => {
console.log ("setRemoteDescription ok");
while (this.earlyCandidates.length) {
var candidate = this.earlyCandidates.shift();
this.addIceCandidate(this.pc.peerid, candidate);
}
this.getIceCandidate()
}
, (error) => {
console.log ("setRemoteDescription error:" + JSON.stringify(error));
});
}
/*
* AJAX /getIceCandidate callback
*/
WebRtcStreamer.prototype.onReceiveCandidate = function(dataJson) {
console.log("candidate: " + JSON.stringify(dataJson));
if (dataJson) {
for (var i=0; i<dataJson.length; i++) {
var candidate = new RTCIceCandidate(dataJson[i]);
console.log("Adding ICE candidate :" + JSON.stringify(candidate) );
this.pc.addIceCandidate(candidate).then( () => { console.log ("addIceCandidate OK"); }
, (error) => { console.log ("addIceCandidate error:" + JSON.stringify(error)); } );
}
this.pc.addIceCandidate();
}
}
/*
* AJAX callback for Error
*/
WebRtcStreamer.prototype.onError = function(status) {
console.log("onError:")
console.log(status);
}
return WebRtcStreamer;
})();
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
window.WebRtcStreamer = WebRtcStreamer;
}
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = WebRtcStreamer;
}
export { WebRtcStreamer };

View File

@@ -1,6 +1,7 @@
<template> <template>
<div class="core-video"> <div class="core-video">
<div class="title">核心景区视频</div> <div class="title">核心景区视频</div>
<ul class="list"> <ul class="list">
<li <li
class="item" class="item"
@@ -21,7 +22,6 @@
:controls="false" :controls="false"
style="object-fit: cover" style="object-fit: cover"
> >
<source src="" type="application/x-mpegURL" />
</video> </video>
</div> </div>
</li> </li>
@@ -36,37 +36,55 @@
import { getVideoListApi, postRefreshApi } from '@/api/home' import { getVideoListApi, postRefreshApi } from '@/api/home'
import Hls from 'hls.js' import Hls from 'hls.js'
import { mode, baseUrl, proBaseUrl } from '@/utils/config'
let list = ref([]) let list = ref([])
let src = ref('') let src = ref('')
let cameraIndexCode = ref('') let cameraIndexCode = ref('')
let videoShow = ref(false) let videoShow = ref(false)
let webRtcServerList = ref([])
const handleItem = (item) => { const handleItem = (item) => {
src.value = item.hlsUrl // src.value = item.hlsUrl
src.value = item.rtspUrl
cameraIndexCode.value = item.cameraIndexCode cameraIndexCode.value = item.cameraIndexCode
videoShow.value = true videoShow.value = true
} }
const getVideoList = async () => { const getVideoList = async () => {
let res = await getVideoListApi({ let res = await getVideoListApi({
type: 'rtsp',
businessVideoDisplayPosition: '核心景区视频' businessVideoDisplayPosition: '核心景区视频'
}) })
console.log(res, '视频列表')
list.value = res.data list.value = res.data
nextTick(() => { nextTick(() => {
list.value.forEach(async (item, index) => { list.value.forEach(async (item, index) => {
var video = document.getElementById(`video${index}`) let webRtcServer = new WebRtcStreamer(
const hls = new Hls({ `video${index}`,
maxBufferLength: 10, // 最大缓冲长度(秒) `${mode == 'dev' ? baseUrl : proBaseUrl}/webrtc`
maxMaxBufferLength: 15, // 缓冲区长度的上限 )
maxBufferSize: 20 * 1000 * 1000 // 最大缓冲大小(字节) webRtcServer.connect(item.rtspUrl)
}) webRtcServerList.value.push(webRtcServer)
hls.loadSource(item.hlsUrl)
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play()
})
}) })
}) })
// nextTick(() => {
// list.value.forEach(async (item, index) => {
// var video = document.getElementById(`video${index}`)
// const hls = new Hls({
// maxBufferLength: 10, // 最大缓冲长度(秒)
// maxMaxBufferLength: 15, // 缓冲区长度的上限
// maxBufferSize: 20 * 1000 * 1000 // 最大缓冲大小(字节)
// })
// hls.loadSource(item.hlsUrl)
// hls.attachMedia(video)
// hls.on(Hls.Events.MANIFEST_PARSED, () => {
// video.play()
// })
// })
// })
} }
onMounted(() => { onMounted(() => {
getVideoList() getVideoList()

View File

@@ -1,10 +1,12 @@
<template> <template>
<div class="dialog"> <div class="dialog">
<el-dialog v-model="modelValue" align-center :modal="false" :show-close="false" z-index="9999"> <el-dialog v-model="modelValue" align-center :modal="false" :show-close="false" :z-index="9999">
<div v-if="src"> <div v-if="src" class="dialog-box">
<video class="video" ref="videoRef" muted autoplay controls style="object-fit: cover"> <!-- <video class="video" ref="videoRef" muted autoplay controls style="object-fit: cover">
<source src="" type="application/x-mpegURL" /> <source src="" type="application/x-mpegURL" />
</video> </video> -->
<video class="video" id="bigVideo" muted autoplay controls style="object-fit: cover" />
<div class="action-box"> <div class="action-box">
<!-- <div class="action-item"> <!-- <div class="action-item">
<img src="@/assets/images/plus.png" alt="" /> <img src="@/assets/images/plus.png" alt="" />
@@ -32,7 +34,6 @@
</div> </div>
<p v-else class="none">暂无信号</p> <p v-else class="none">暂无信号</p>
<img class="close" src="@/assets/images/close.png" @click="handleClose" /> <img class="close" src="@/assets/images/close.png" @click="handleClose" />
<div></div>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
@@ -42,6 +43,7 @@
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { postVideoControlApi } from '@/api/monitor' import { postVideoControlApi } from '@/api/monitor'
import { mode, baseUrl, proBaseUrl } from '@/utils/config'
const Z00M_IN = 'ZOOM_IN' // 焦距变大 const Z00M_IN = 'ZOOM_IN' // 焦距变大
const Z00M_OUT = 'ZOOM_OUT' // 焦距变小 const Z00M_OUT = 'ZOOM_OUT' // 焦距变小
@@ -65,6 +67,7 @@
let modelValue = defineModel() let modelValue = defineModel()
let videoRef = ref() let videoRef = ref()
let webRtcServer = null
watch( watch(
() => modelValue.value, () => modelValue.value,
@@ -97,19 +100,22 @@
}) })
} }
const handleClose = () => { const handleClose = () => {
webRtcServer.disconnect()
modelValue.value = false modelValue.value = false
} }
const init = () => { const init = () => {
const hls = new Hls({ webRtcServer = new WebRtcStreamer('bigVideo', `${mode == 'dev' ? baseUrl : proBaseUrl}/webrtc`)
maxBufferLength: 10, // 最大缓冲长度(秒) webRtcServer.connect(props.src)
maxMaxBufferLength: 15, // 缓冲区长度的上限 // const hls = new Hls({
maxBufferSize: 15 * 1000 * 1000 // 最大缓冲大小(字节 // maxBufferLength: 10, // 最大缓冲长度(秒
}) // maxMaxBufferLength: 15, // 缓冲区长度的上限
hls.loadSource(props.src) // maxBufferSize: 15 * 1000 * 1000 // 最大缓冲大小(字节)
hls.attachMedia(videoRef.value) // })
hls.on(Hls.Events.MANIFEST_PARSED, () => { // hls.loadSource(props.src)
videoRef.value.play() // hls.attachMedia(videoRef.value)
}) // hls.on(Hls.Events.MANIFEST_PARSED, () => {
// videoRef.value.play()
// })
} }
</script> </script>
@@ -158,23 +164,26 @@
} }
:deep(.el-dialog) { :deep(.el-dialog) {
position: relative; position: relative;
width: vw(1660); width: vw(1940);
background: transparent !important;
}
.dialog-box {
padding: vw(40) vw(30) vw(30) vw(30); padding: vw(40) vw(30) vw(30) vw(30);
background-image: url('@/assets/images/one-video-bg.png'); background-image: url('@/assets/images/video-bg.png');
background-size: 100% 100%; background-size: 100% 100%;
} }
:deep(.el-dialog__header) { :deep(.el-dialog__header) {
padding-bottom: 0 !important; padding-bottom: 0 !important;
} }
.video { .video {
width: vw(1600); width: vw(1814);
height: vw(900); height: vw(790);
} }
.close { .close {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
right: vw(40); right: vw(70);
top: vw(50); top: vw(80);
width: vw(60); width: vw(60);
z-index: 9999; z-index: 9999;
} }

View File

@@ -7,6 +7,9 @@ import '@/styles/common.scss'
import '@/assets/fonts/index.css' import '@/assets/fonts/index.css'
import 'element-plus/theme-chalk/el-message-box.css' import 'element-plus/theme-chalk/el-message-box.css'
import 'element-plus/theme-chalk/el-message.css' import 'element-plus/theme-chalk/el-message.css'
import '@/assets/adapter.min.js'
import '@/assets/webrtcstreamer.js'
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(createPinia())

View File

@@ -1,11 +1,10 @@
export const baseUrl = 'http://36.138.38.16:8001' // http://36.138.38.16:8001 export const baseUrl = 'http://36.138.38.16:8001' // http://36.138.38.16:8001
// export const proBaseUrl = 'http://192.168.77.200'
export const proBaseUrl = 'http://192.168.77.200' export const proBaseUrl = 'http://192.168.77.200'
export const socketBaseUrl = 'ws://36.138.38.16:81' // ws://36.138.38.16:81 export const socketBaseUrl = 'ws://36.138.38.16:81' // ws://36.138.38.16:81
export const proSocketBaseUrl = 'ws://192.168.77.200:8060' export const proSocketBaseUrl = 'ws://192.168.77.200:8060'
export const mode = 'dev' // 测试 dev 正式 pro export const mode = 'pro' // 测试 dev 正式 pro
export const devToken = export const devToken =
'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImE1OWFmNWYwLTU3OWItNDJkNy1hZDJhLTY0Y2JlODA5ZWI1NiJ9.BTxvu6jUWbN0qONWf5K6VzXopE8T8qXzKuX-mij21VJT4U0LdgnqToyqeNDQ2OyJ6cvpdJBzQ9mEEb-dnwrTpQ' 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImE1OWFmNWYwLTU3OWItNDJkNy1hZDJhLTY0Y2JlODA5ZWI1NiJ9.BTxvu6jUWbN0qONWf5K6VzXopE8T8qXzKuX-mij21VJT4U0LdgnqToyqeNDQ2OyJ6cvpdJBzQ9mEEb-dnwrTpQ'

View File

@@ -37,7 +37,6 @@
watch( watch(
() => dataRes.value, () => dataRes.value,
(val) => { (val) => {
console.log(val, '景区接受消息')
if (val) { if (val) {
switch (val.type) { switch (val.type) {
case 'userPortrait': case 'userPortrait':
@@ -87,7 +86,6 @@
const sendCarShip = (e) => { const sendCarShip = (e) => {
timer = setInterval(() => { timer = setInterval(() => {
if (isConnected.value) { if (isConnected.value) {
console.log('定时发送车船消息11')
sendMessage( sendMessage(
JSON.stringify({ JSON.stringify({
action: 'start', action: 'start',