机场视频换webrtc
This commit is contained in:
@@ -17,14 +17,14 @@
|
|||||||
<a-spin size="large" v-if="loading" />
|
<a-spin size="large" v-if="loading" />
|
||||||
<!-- <div id="player" v-else></div> -->
|
<!-- <div id="player" v-else></div> -->
|
||||||
<div class="player" :id="'player_' + sn" v-else>
|
<div class="player" :id="'player_' + sn" v-else>
|
||||||
<jessibuca
|
<stream-player
|
||||||
:currentVedioDatas="currentVedioDatas"
|
:currentVedioDatas="currentVedioDatas"
|
||||||
@jietu="jietu"
|
@jietu="jietu"
|
||||||
ref="jessibucaVideo"
|
ref="jessibucaVideo"
|
||||||
style="width: 100%; height: 100%"
|
style="width: 100%; height: 100%"
|
||||||
:videoUrl="flvUrl"
|
:videoUrl="flvUrl"
|
||||||
:showQu="true"
|
:showQu="true"
|
||||||
></jessibuca>
|
></stream-player>
|
||||||
</div>
|
</div>
|
||||||
<slot name="dingwei"></slot>
|
<slot name="dingwei"></slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,24 +68,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, nextTick, onMounted, ref, watch } from 'vue';
|
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import { CloseOutlined } from '@ant-design/icons-vue';
|
import { CloseOutlined } from '@ant-design/icons-vue';
|
||||||
import { getLiveCapacityBySn, startLivestream,liveStartUrl } from '/@/api/manage';
|
import { getLiveCapacityBySn, startLivestream,liveStartUrl } from '/@/api/manage';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import jswebrtc from '/@/vendors/jswebrtc.js';
|
|
||||||
import ailabel from './ailabel.vue'
|
import ailabel from './ailabel.vue'
|
||||||
import FlvExtend from 'flv-extend';
|
|
||||||
import { useUserAuthor } from '/@/hooks/use-g-author';
|
import { useUserAuthor } from '/@/hooks/use-g-author';
|
||||||
import jessibuca from './live/jessibuca.vue';
|
import streamPlayer from './live/stream-player.vue';
|
||||||
import { debounce } from "lodash-es"
|
|
||||||
import { getAiLive,getAddress,getAiLiveData,saveClueData } from '/@/api/daping';
|
import { getAiLive,getAddress,getAiLiveData,saveClueData } from '/@/api/daping';
|
||||||
|
|
||||||
import { useRoute } from "vue-router";
|
|
||||||
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
|
|
||||||
const { getAiAuthor } = useUserAuthor();
|
const { getAiAuthor } = useUserAuthor();
|
||||||
|
|
||||||
|
|
||||||
@@ -121,6 +112,7 @@ const videoTypeByKey = {
|
|||||||
const deviceSN = ref('');
|
const deviceSN = ref('');
|
||||||
const flvUrl = ref('');
|
const flvUrl = ref('');
|
||||||
const jessibucaVideo = ref();
|
const jessibucaVideo = ref();
|
||||||
|
const isShowData = ref(false);
|
||||||
const droneCameraBtn = ref<any>([]);
|
const droneCameraBtn = ref<any>([]);
|
||||||
|
|
||||||
const xiansuoRef = ref(null)
|
const xiansuoRef = ref(null)
|
||||||
@@ -381,8 +373,6 @@ const getLiveCapacityDatas = async (sn: string) => {
|
|||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
const store:any = inject("storeDataGovernanceDashboard");
|
|
||||||
const aiRtmpVideo = ref('');
|
|
||||||
/**
|
/**
|
||||||
* 打开AI rtsp视频
|
* 打开AI rtsp视频
|
||||||
*/
|
*/
|
||||||
@@ -394,7 +384,6 @@ const openRtspVideo = async () => {
|
|||||||
const rtmpLiveUrl = live.data.aiPullRtmp;
|
const rtmpLiveUrl = live.data.aiPullRtmp;
|
||||||
// 将rtmpLiveUrl中的wss替换为https
|
// 将rtmpLiveUrl中的wss替换为https
|
||||||
const fixedRtmpLiveUrl = typeof rtmpLiveUrl === 'string' ? rtmpLiveUrl.replace('wss', 'https') : rtmpLiveUrl;
|
const fixedRtmpLiveUrl = typeof rtmpLiveUrl === 'string' ? rtmpLiveUrl.replace('wss', 'https') : rtmpLiveUrl;
|
||||||
aiRtmpVideo.value = fixedRtmpLiveUrl;
|
|
||||||
try {
|
try {
|
||||||
createRtmpVideo(fixedRtmpLiveUrl);
|
createRtmpVideo(fixedRtmpLiveUrl);
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
@@ -404,7 +393,6 @@ const openRtspVideo = async () => {
|
|||||||
}
|
}
|
||||||
// }
|
// }
|
||||||
};
|
};
|
||||||
const currDroneItem = ref("")
|
|
||||||
// 获取视频
|
// 获取视频
|
||||||
const getLiveData = async (info: any) => {
|
const getLiveData = async (info: any) => {
|
||||||
console.log(`output->info`,info)
|
console.log(`output->info`,info)
|
||||||
@@ -469,6 +457,12 @@ const getLiveVedioById = async (vl: any) => {
|
|||||||
video_quality: 3,
|
video_quality: 3,
|
||||||
};
|
};
|
||||||
currentVedioDatas.value = videoDataJson
|
currentVedioDatas.value = videoDataJson
|
||||||
|
console.info("[aixiaorui] dock getLiveVedioById", {
|
||||||
|
sn: videoDataJson.sn,
|
||||||
|
videoId: videoDataJson.videoId,
|
||||||
|
roomID: videoDataJson.roomID,
|
||||||
|
url: videoDataJson.url
|
||||||
|
})
|
||||||
|
|
||||||
// setCurrentVedioDatas(videoDataJson)
|
// setCurrentVedioDatas(videoDataJson)
|
||||||
// await onStop(videoDataJson.videoId);
|
// await onStop(videoDataJson.videoId);
|
||||||
@@ -482,10 +476,16 @@ const openLive = async (options: DomByData) => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
// 开启视频
|
// 开启视频
|
||||||
let res = await liveStart(options, videoTypeByKey[videoType]);
|
let res = await liveStart(options, videoTypeByKey[videoType]);
|
||||||
if (res && res.data && res.data.urls && res.data.urls.HTTPS_FLV) {
|
const streamUrl = res?.data?.urls?.HTTP_WEB_RTC || res?.data?.urls?.HTTPS_FLV || res?.data?.url;
|
||||||
|
console.info("[aixiaorui] dock openLive response", {
|
||||||
|
dock_sn: options.sn,
|
||||||
|
videoId: options.videoId,
|
||||||
|
streamUrl,
|
||||||
|
res: res?.data
|
||||||
|
})
|
||||||
|
if (streamUrl) {
|
||||||
console.log('resres',res)
|
console.log('resres',res)
|
||||||
// let resUrl = res.data.urls.HTTPS_FLV;
|
let resUrl = streamUrl;
|
||||||
let resUrl = res.data.url;
|
|
||||||
if (resUrl) {
|
if (resUrl) {
|
||||||
await createRtmpVideo(resUrl);
|
await createRtmpVideo(resUrl);
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,6 @@ const liveStart = async (data: DomByData, url_type: string = '0') => {
|
|||||||
const createRtmpVideo = async (url: string) => {
|
const createRtmpVideo = async (url: string) => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
// 根据 roomID 创建 获取对应的dom
|
// 根据 roomID 创建 获取对应的dom
|
||||||
let remotePlay;
|
|
||||||
/* remotePlay = document.getElementById('player') as Element;
|
/* remotePlay = document.getElementById('player') as Element;
|
||||||
if (remotePlay) {
|
if (remotePlay) {
|
||||||
deleteChilds(remotePlay);
|
deleteChilds(remotePlay);
|
||||||
@@ -526,7 +525,8 @@ const createRtmpVideo = async (url: string) => {
|
|||||||
// 将video加入到 dom中
|
// 将video加入到 dom中
|
||||||
// remotePlay?.appendChild(link);
|
// remotePlay?.appendChild(link);
|
||||||
await nextTick(async () => {
|
await nextTick(async () => {
|
||||||
await rtmpVideoData(url, 'player_'+deviceSN.value);
|
flvUrl.value = url;
|
||||||
|
await jessibucaVideo.value?.play?.(url);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
let videoPalyer = null;
|
let videoPalyer = null;
|
||||||
@@ -657,10 +657,17 @@ function deleteChilds(ele: Element) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
void rtmpVideoData;
|
||||||
|
void deleteChilds;
|
||||||
|
|
||||||
|
|
||||||
const onloadCamera = async () => {
|
const onloadCamera = async () => {
|
||||||
await getDroneCameras(props.sn);
|
await getDroneCameras(props.sn);
|
||||||
|
console.info("[aixiaorui] dock onloadCamera", {
|
||||||
|
sn: props.sn,
|
||||||
|
liveType: props.liveType,
|
||||||
|
droneCameraBtn: droneCameraBtn.value
|
||||||
|
})
|
||||||
if (droneCameraBtn.value.length > 0) {
|
if (droneCameraBtn.value.length > 0) {
|
||||||
|
|
||||||
let cameraIndex: number = 0;
|
let cameraIndex: number = 0;
|
||||||
@@ -673,6 +680,11 @@ const onloadCamera = async () => {
|
|||||||
getLiveData(droneCameraBtn.value[cameraIndex]);
|
getLiveData(droneCameraBtn.value[cameraIndex]);
|
||||||
}else{
|
}else{
|
||||||
let res = await liveStartUrl(props.sn);
|
let res = await liveStartUrl(props.sn);
|
||||||
|
console.info("[aixiaorui] dock liveStartUrl response", {
|
||||||
|
sn: props.sn,
|
||||||
|
res: res?.data,
|
||||||
|
streamUrl: res?.data?.flv
|
||||||
|
})
|
||||||
console.log('resUrl',res)
|
console.log('resUrl',res)
|
||||||
let resUrl = res.data.flv
|
let resUrl = res.data.flv
|
||||||
|
|
||||||
@@ -726,9 +738,9 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
const emit = defineEmits(['close','checkDrone',"updateAi","clsoeAi"]);
|
const emit = defineEmits(['close','checkDrone',"updateAi","clsoeAi"]);
|
||||||
const closeVideo = () => {
|
const closeVideo = () => {
|
||||||
if (!videoPalyer) return;
|
|
||||||
videoPalyer.close();
|
|
||||||
videoPalyer = null;
|
videoPalyer = null;
|
||||||
|
flvUrl.value = '';
|
||||||
|
jessibucaVideo.value?.destroy?.();
|
||||||
};
|
};
|
||||||
const closeMoadl = () => {
|
const closeMoadl = () => {
|
||||||
closeVideo();
|
closeVideo();
|
||||||
|
|||||||
@@ -79,7 +79,28 @@ const props = defineProps<Props>();
|
|||||||
|
|
||||||
const videoUrl = ref<string | undefined>();
|
const videoUrl = ref<string | undefined>();
|
||||||
|
|
||||||
|
const syncPlayingVideoDataUrl = (url?: string, retryCount = 10) => {
|
||||||
|
const currentUrl = String(url || videoUrl.value || '').trim();
|
||||||
|
const rootElement = container.value as HTMLElement | null;
|
||||||
|
if (!currentUrl || !rootElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoElement = rootElement.querySelector('video') as HTMLVideoElement | null;
|
||||||
|
if (videoElement) {
|
||||||
|
videoElement.setAttribute('data-url', currentUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryCount > 0) {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
syncPlayingVideoDataUrl(currentUrl, retryCount - 1);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const play = (url: string) => {
|
const play = (url: string) => {
|
||||||
|
videoUrl.value = url;
|
||||||
if (jessibuca) {
|
if (jessibuca) {
|
||||||
destroy();
|
destroy();
|
||||||
}
|
}
|
||||||
@@ -92,10 +113,12 @@ const play = (url: string) => {
|
|||||||
|
|
||||||
if (jessibuca.hasLoaded()) {
|
if (jessibuca.hasLoaded()) {
|
||||||
jessibuca.play(url);
|
jessibuca.play(url);
|
||||||
|
syncPlayingVideoDataUrl(url);
|
||||||
} else {
|
} else {
|
||||||
jessibuca.on('load', () => {
|
jessibuca.on('load', () => {
|
||||||
console.log('load 播放');
|
console.log('load 播放');
|
||||||
jessibuca.play(url);
|
jessibuca.play(url);
|
||||||
|
syncPlayingVideoDataUrl(url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.info("jessibuca.screenshot",jessibuca.screenshot)
|
console.info("jessibuca.screenshot",jessibuca.screenshot)
|
||||||
@@ -112,6 +135,7 @@ const init = (url) => {
|
|||||||
btnDom.value = document.getElementById('buttonsBox');
|
btnDom.value = document.getElementById('buttonsBox');
|
||||||
console.log('初始化时的地址为: ' + videoUrl.value);
|
console.log('初始化时的地址为: ' + videoUrl.value);
|
||||||
play(videoUrl.value);
|
play(videoUrl.value);
|
||||||
|
syncPlayingVideoDataUrl(videoUrl.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -137,7 +161,8 @@ defineExpose({
|
|||||||
init,
|
init,
|
||||||
resize,
|
resize,
|
||||||
play,
|
play,
|
||||||
destroy
|
destroy,
|
||||||
|
setDataUrl: syncPlayingVideoDataUrl
|
||||||
});
|
});
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
showQu.value = props.showQu
|
showQu.value = props.showQu
|
||||||
@@ -206,6 +231,7 @@ const create = () => {
|
|||||||
|
|
||||||
emits('onresize')
|
emits('onresize')
|
||||||
console.log('on load init');
|
console.log('on load init');
|
||||||
|
syncPlayingVideoDataUrl(videoUrl.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
jessibuca.on('log', function (msg: string) {
|
jessibuca.on('log', function (msg: string) {
|
||||||
@@ -219,6 +245,7 @@ const create = () => {
|
|||||||
});
|
});
|
||||||
jessibuca.on('play', function () {
|
jessibuca.on('play', function () {
|
||||||
playing.value = true;
|
playing.value = true;
|
||||||
|
syncPlayingVideoDataUrl(videoUrl.value);
|
||||||
});
|
});
|
||||||
jessibuca.on('fullscreen', function (msg: boolean) {
|
jessibuca.on('fullscreen', function (msg: boolean) {
|
||||||
console.log('on fullscreen', msg);
|
console.log('on fullscreen', msg);
|
||||||
|
|||||||
396
src/components/dockerCont/live/live-webrtc-bysn.vue
Normal file
396
src/components/dockerCont/live/live-webrtc-bysn.vue
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
<template>
|
||||||
|
<div class="live-webrtc-bysn">
|
||||||
|
<video
|
||||||
|
ref="videoRef"
|
||||||
|
:data-url="rtcUrl"
|
||||||
|
class="live-webrtc-video"
|
||||||
|
autoplay
|
||||||
|
playsinline
|
||||||
|
muted
|
||||||
|
></video>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from "vue"
|
||||||
|
import { getLiveCapacityBySn } from "@mars/api/manage"
|
||||||
|
import { startLiveStream } from "@mars/api/clusterScheduling"
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
sn: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const videoRef = ref<any>(null)
|
||||||
|
const pc = ref<any>(null)
|
||||||
|
const rtcUrl = ref("")
|
||||||
|
const currentPlayToken = ref(0)
|
||||||
|
const LIVE_WEBRTC_URL_CACHE_KEY = "live-webrtc-bysn-url-map"
|
||||||
|
const PLAY_RETRY_MAX_ATTEMPTS = 3
|
||||||
|
const PLAY_RETRY_DELAY_MS = 2000
|
||||||
|
|
||||||
|
const loadRtcUrlCacheMap = () => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawValue = window.localStorage.getItem(LIVE_WEBRTC_URL_CACHE_KEY)
|
||||||
|
if (!rawValue) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedValue = JSON.parse(rawValue)
|
||||||
|
if (!parsedValue || typeof parsedValue !== "object" || Array.isArray(parsedValue)) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedValue
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("load rtc url cache failed:", error)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCachedRtcUrlBySn = (sn: any) => {
|
||||||
|
const deviceSn = String(sn || "").trim()
|
||||||
|
if (!deviceSn) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheMap = loadRtcUrlCacheMap()
|
||||||
|
return String(cacheMap[deviceSn] || "").trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCachedRtcUrlBySn = (sn: any, url: any) => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceSn = String(sn || "").trim()
|
||||||
|
const rtcStreamUrl = String(url || "").trim()
|
||||||
|
if (!deviceSn || !rtcStreamUrl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheMap = loadRtcUrlCacheMap()
|
||||||
|
cacheMap[deviceSn] = rtcStreamUrl
|
||||||
|
window.localStorage.setItem(LIVE_WEBRTC_URL_CACHE_KEY, JSON.stringify(cacheMap))
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("save rtc url cache failed:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeCachedRtcUrlBySn = (sn: any) => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceSn = String(sn || "").trim()
|
||||||
|
if (!deviceSn) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheMap = loadRtcUrlCacheMap()
|
||||||
|
if (!cacheMap[deviceSn]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delete cacheMap[deviceSn]
|
||||||
|
window.localStorage.setItem(LIVE_WEBRTC_URL_CACHE_KEY, JSON.stringify(cacheMap))
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("remove rtc url cache failed:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLastCameraInfo = (sn: any, response: any) => {
|
||||||
|
const deviceSn = String(sn || "").trim()
|
||||||
|
const deviceList = Array.isArray(response?.data) ? response.data : []
|
||||||
|
const matchedDevice = deviceList.find((item: any) => item?.sn === deviceSn)
|
||||||
|
const cameraList = Array.isArray(matchedDevice?.cameras_list) ? matchedDevice.cameras_list : []
|
||||||
|
const cameraInfo = cameraList[cameraList.length - 1]
|
||||||
|
|
||||||
|
if (!matchedDevice || !cameraInfo) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
deviceSn: matchedDevice.sn || deviceSn,
|
||||||
|
cameraInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildStreamPayload = (cameraInfo: any, deviceSn: any) => {
|
||||||
|
const videoInfo = Array.isArray(cameraInfo?.videos_list) ? cameraInfo.videos_list[0] : null
|
||||||
|
if (!videoInfo) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: `channel=${videoInfo.room_id}&sn=${deviceSn}&token=${videoInfo.token}&uid=${videoInfo.id}`,
|
||||||
|
video_id: `${deviceSn}/${cameraInfo.index}/${videoInfo.index}`,
|
||||||
|
url_type: "1",
|
||||||
|
video_quality: "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeAnswerSdp = (value: any) => {
|
||||||
|
const rawSdp = String(value || "")
|
||||||
|
if (!rawSdp) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawSdp
|
||||||
|
.replace(/\r\n/g, "\n")
|
||||||
|
.replace(/\r/g, "\n")
|
||||||
|
.split("\n")
|
||||||
|
.map((line: any) => String(line || "").trimEnd())
|
||||||
|
.join("\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTcpCandidateLines = (value: any) => {
|
||||||
|
const normalizedSdp = normalizeAnswerSdp(value)
|
||||||
|
if (!normalizedSdp) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineList = normalizedSdp.split("\r\n")
|
||||||
|
const filteredLineList = lineList.filter((line: any) => {
|
||||||
|
const currentLine = String(line || "")
|
||||||
|
if (!currentLine.startsWith("a=candidate:")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return !(currentLine.includes(" tcp ") && currentLine.includes(" tcptype passive"))
|
||||||
|
})
|
||||||
|
|
||||||
|
return filteredLineList.join("\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
const setRemoteDescriptionSafely = async (answerSdp: any) => {
|
||||||
|
const normalizedSdp = normalizeAnswerSdp(answerSdp)
|
||||||
|
if (!normalizedSdp) {
|
||||||
|
throw new Error("answer sdp empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pc.value.setRemoteDescription(new RTCSessionDescription({
|
||||||
|
type: "answer",
|
||||||
|
sdp: normalizedSdp
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
const sanitizedSdp = removeTcpCandidateLines(normalizedSdp)
|
||||||
|
if (!sanitizedSdp || sanitizedSdp === normalizedSdp) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
await pc.value.setRemoteDescription(new RTCSessionDescription({
|
||||||
|
type: "answer",
|
||||||
|
sdp: sanitizedSdp
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopPlayback = () => {
|
||||||
|
if (pc.value) {
|
||||||
|
pc.value.close()
|
||||||
|
pc.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoRef.value) {
|
||||||
|
videoRef.value.srcObject = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wait = (duration: any) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
window.setTimeout(resolve, duration)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const startWebRtc = async (url: any, playToken: any) => {
|
||||||
|
const apiUrl = String(url || "").trim()
|
||||||
|
if (!apiUrl) {
|
||||||
|
throw new Error("webrtc url empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
stopPlayback()
|
||||||
|
|
||||||
|
pc.value = new RTCPeerConnection({
|
||||||
|
iceServers: []
|
||||||
|
})
|
||||||
|
|
||||||
|
pc.value.ontrack = (event: any) => {
|
||||||
|
if (playToken !== currentPlayToken.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoRef.value) {
|
||||||
|
videoRef.value.srcObject = event.streams[0]
|
||||||
|
const playPromise = videoRef.value.play?.()
|
||||||
|
if (playPromise && typeof playPromise.catch === "function") {
|
||||||
|
playPromise.catch((error: any) => {
|
||||||
|
console.warn("video play failed:", error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.value.addTransceiver("audio", { direction: "recvonly" })
|
||||||
|
pc.value.addTransceiver("video", { direction: "recvonly" })
|
||||||
|
|
||||||
|
const offer = await pc.value.createOffer()
|
||||||
|
await pc.value.setLocalDescription(offer)
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: offer.sdp
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`服务器响应错误: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
const answerSdp = result?.sdp || result?.sd || ""
|
||||||
|
|
||||||
|
if (result?.code !== 0 || !answerSdp) {
|
||||||
|
throw new Error(result?.msg || "获取Answer失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playToken !== currentPlayToken.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await setRemoteDescriptionSafely(answerSdp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const playBySn = async (sn: any, attempt = 1) => {
|
||||||
|
const deviceSn = String(sn || "").trim()
|
||||||
|
if (attempt === 1) {
|
||||||
|
currentPlayToken.value += 1
|
||||||
|
}
|
||||||
|
const playToken = currentPlayToken.value
|
||||||
|
|
||||||
|
if (attempt === 1) {
|
||||||
|
stopPlayback()
|
||||||
|
rtcUrl.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deviceSn) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const capacityResponse = await getLiveCapacityBySn(deviceSn, {})
|
||||||
|
if (playToken !== currentPlayToken.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveInfo = getLastCameraInfo(deviceSn, capacityResponse)
|
||||||
|
if (!liveInfo) {
|
||||||
|
throw new Error("未找到可用摄像头")
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamPayload = buildStreamPayload(liveInfo.cameraInfo, liveInfo.deviceSn)
|
||||||
|
if (!streamPayload) {
|
||||||
|
throw new Error("摄像头视频信息为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
const startResponse = await startLiveStream(streamPayload)
|
||||||
|
console.log("start live stream response:", startResponse)
|
||||||
|
if (playToken !== currentPlayToken.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextRtcUrl = startResponse.data?.urls?.HTTP_WEB_RTC
|
||||||
|
if (!nextRtcUrl) {
|
||||||
|
throw new Error("开流接口未返回有效WebRTC地址")
|
||||||
|
}
|
||||||
|
|
||||||
|
rtcUrl.value = nextRtcUrl
|
||||||
|
setCachedRtcUrlBySn(deviceSn, nextRtcUrl)
|
||||||
|
await startWebRtc(nextRtcUrl, playToken)
|
||||||
|
} catch (error: any) {
|
||||||
|
if (playToken !== currentPlayToken.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < PLAY_RETRY_MAX_ATTEMPTS) {
|
||||||
|
console.warn(`play by sn retry ${attempt + 1}/${PLAY_RETRY_MAX_ATTEMPTS}:`, error)
|
||||||
|
await wait(PLAY_RETRY_DELAY_MS)
|
||||||
|
if (playToken !== currentPlayToken.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await playBySn(deviceSn, attempt + 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stopPlayback()
|
||||||
|
console.error("play by sn failed:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const playCachedOrFetchBySn = async (sn: any) => {
|
||||||
|
const deviceSn = String(sn || "").trim()
|
||||||
|
if (!deviceSn) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedRtcUrl = getCachedRtcUrlBySn(deviceSn)
|
||||||
|
if (!cachedRtcUrl) {
|
||||||
|
await playBySn(deviceSn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPlayToken.value += 1
|
||||||
|
const playToken = currentPlayToken.value
|
||||||
|
stopPlayback()
|
||||||
|
rtcUrl.value = cachedRtcUrl
|
||||||
|
|
||||||
|
try {
|
||||||
|
await startWebRtc(cachedRtcUrl, playToken)
|
||||||
|
} catch (error) {
|
||||||
|
if (playToken !== currentPlayToken.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("cached rtc url play failed, fallback to playBySn:", error)
|
||||||
|
removeCachedRtcUrlBySn(deviceSn)
|
||||||
|
await playBySn(deviceSn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
playCachedOrFetchBySn(props.sn)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
currentPlayToken.value += 1
|
||||||
|
stopPlayback()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.live-webrtc-bysn {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-webrtc-video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
416
src/components/dockerCont/live/stream-player.vue
Normal file
416
src/components/dockerCont/live/stream-player.vue
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
<template>
|
||||||
|
<div class="stream-player">
|
||||||
|
<jessibuca
|
||||||
|
v-show="!isWebrtcMode"
|
||||||
|
ref="flvPlayerRef"
|
||||||
|
:currentVedioDatas="currentVedioDatas"
|
||||||
|
:videoUrl="videoUrl"
|
||||||
|
:error="error"
|
||||||
|
:hasAudio="hasAudio"
|
||||||
|
:height="height"
|
||||||
|
:showQu="showQu"
|
||||||
|
@jietu="forwardScreenshot"
|
||||||
|
@onresize="emit('onresize')"
|
||||||
|
/>
|
||||||
|
<div v-show="isWebrtcMode" ref="webrtcContainerRef" class="webrtc-container">
|
||||||
|
<video ref="webrtcVideoRef" :data-url="currentUrl" class="webrtc-video" autoplay playsinline muted></video>
|
||||||
|
<div class="buttons-box-right">
|
||||||
|
<b v-if="showQu" class="jessibuca-btn" @click="captureWebRtcFrame">实时取证</b>
|
||||||
|
<i class="iconfont icon-shuaxin11 jessibuca-btn" @click="replayCurrentUrl"></i>
|
||||||
|
<i v-if="!fullscreen" class="iconfont icon-weibiaoti10 jessibuca-btn" @click="toggleFullscreen"></i>
|
||||||
|
<i v-else class="iconfont icon-weibiaoti11 jessibuca-btn" @click="toggleFullscreen"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import jessibuca from './jessibuca.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
videoUrl: string
|
||||||
|
error?: Error | null
|
||||||
|
currentVedioDatas?: any
|
||||||
|
hasAudio?: boolean
|
||||||
|
height?: number
|
||||||
|
showQu?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
videoUrl: '',
|
||||||
|
error: null,
|
||||||
|
currentVedioDatas: () => ({}),
|
||||||
|
hasAudio: false,
|
||||||
|
height: 500,
|
||||||
|
showQu: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['onresize', 'jietu'])
|
||||||
|
|
||||||
|
const flvPlayerRef = ref<any>(null)
|
||||||
|
const webrtcContainerRef = ref<HTMLElement | null>(null)
|
||||||
|
const webrtcVideoRef = ref<HTMLVideoElement | null>(null)
|
||||||
|
const isWebrtcMode = ref(false)
|
||||||
|
const fullscreen = ref(false)
|
||||||
|
const currentUrl = ref('')
|
||||||
|
const playToken = ref(0)
|
||||||
|
|
||||||
|
let peerConnection: RTCPeerConnection | null = null
|
||||||
|
|
||||||
|
const isWebrtcUrl = (url: string) => /webrtc/i.test(String(url || ''))
|
||||||
|
|
||||||
|
const forwardScreenshot = (value: string) => {
|
||||||
|
emit('jietu', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopWebrtc = () => {
|
||||||
|
if (peerConnection) {
|
||||||
|
peerConnection.ontrack = null
|
||||||
|
peerConnection.onconnectionstatechange = null
|
||||||
|
peerConnection.close()
|
||||||
|
peerConnection = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoElement = webrtcVideoRef.value
|
||||||
|
if (!videoElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaStream = videoElement.srcObject as MediaStream | null
|
||||||
|
mediaStream?.getTracks().forEach((track) => track.stop())
|
||||||
|
videoElement.pause()
|
||||||
|
videoElement.srcObject = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeAnswerSdp = (value: any) => {
|
||||||
|
const rawSdp = String(value || '')
|
||||||
|
if (!rawSdp) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawSdp
|
||||||
|
.replace(/\r\n/g, '\n')
|
||||||
|
.replace(/\r/g, '\n')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => String(line || '').trimEnd())
|
||||||
|
.join('\r\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTcpCandidateLines = (value: any) => {
|
||||||
|
const normalizedSdp = normalizeAnswerSdp(value)
|
||||||
|
if (!normalizedSdp) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedSdp
|
||||||
|
.split('\r\n')
|
||||||
|
.filter((line) => {
|
||||||
|
if (!line.startsWith('a=candidate:')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return !(line.includes(' tcp ') && line.includes(' tcptype passive'))
|
||||||
|
})
|
||||||
|
.join('\r\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const setRemoteDescriptionSafely = async (pc: RTCPeerConnection, answerSdp: any) => {
|
||||||
|
const normalizedSdp = normalizeAnswerSdp(answerSdp)
|
||||||
|
if (!normalizedSdp) {
|
||||||
|
throw new Error('answer sdp empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pc.setRemoteDescription(
|
||||||
|
new RTCSessionDescription({
|
||||||
|
type: 'answer',
|
||||||
|
sdp: normalizedSdp
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
const sanitizedSdp = removeTcpCandidateLines(normalizedSdp)
|
||||||
|
if (!sanitizedSdp || sanitizedSdp === normalizedSdp) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
await pc.setRemoteDescription(
|
||||||
|
new RTCSessionDescription({
|
||||||
|
type: 'answer',
|
||||||
|
sdp: sanitizedSdp
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseWebrtcResponse = async (response: Response) => {
|
||||||
|
const text = await response.text()
|
||||||
|
if (!text) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch (error) {
|
||||||
|
return { sdp: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const playWebrtc = async (url: string, token: number) => {
|
||||||
|
stopWebrtc()
|
||||||
|
|
||||||
|
const videoElement = webrtcVideoRef.value
|
||||||
|
if (!videoElement) {
|
||||||
|
throw new Error('webrtc video element missing')
|
||||||
|
}
|
||||||
|
|
||||||
|
const pc = new RTCPeerConnection({
|
||||||
|
iceServers: []
|
||||||
|
})
|
||||||
|
peerConnection = pc
|
||||||
|
|
||||||
|
pc.ontrack = (event) => {
|
||||||
|
if (token !== playToken.value || !webrtcVideoRef.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
webrtcVideoRef.value.srcObject = event.streams[0]
|
||||||
|
const playPromise = webrtcVideoRef.value.play?.()
|
||||||
|
if (playPromise && typeof playPromise.catch === 'function') {
|
||||||
|
playPromise.catch((error: any) => {
|
||||||
|
console.warn('[stream-player] video play failed:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
emit('onresize')
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.onconnectionstatechange = () => {
|
||||||
|
if (token !== playToken.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = pc.connectionState
|
||||||
|
if (state === 'failed' || state === 'disconnected') {
|
||||||
|
console.warn('[stream-player] webrtc connection state:', state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.addTransceiver('audio', { direction: 'recvonly' })
|
||||||
|
pc.addTransceiver('video', { direction: 'recvonly' })
|
||||||
|
|
||||||
|
const offer = await pc.createOffer()
|
||||||
|
await pc.setLocalDescription(offer)
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: offer.sdp || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`webrtc request failed: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await parseWebrtcResponse(response)
|
||||||
|
const answerSdp = result?.sdp || result?.sd || result?.data?.sdp || result?.data?.sd || ''
|
||||||
|
if (!answerSdp) {
|
||||||
|
throw new Error(result?.msg || result?.message || 'webrtc answer sdp missing')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token !== playToken.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await setRemoteDescriptionSafely(pc, answerSdp)
|
||||||
|
}
|
||||||
|
|
||||||
|
const play = async (url: string) => {
|
||||||
|
const nextUrl = String(url || '').trim()
|
||||||
|
if (!nextUrl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUrl.value = nextUrl
|
||||||
|
const token = ++playToken.value
|
||||||
|
|
||||||
|
if (isWebrtcUrl(nextUrl)) {
|
||||||
|
flvPlayerRef.value?.destroy?.()
|
||||||
|
isWebrtcMode.value = true
|
||||||
|
await nextTick()
|
||||||
|
await playWebrtc(nextUrl, token)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stopWebrtc()
|
||||||
|
isWebrtcMode.value = false
|
||||||
|
await nextTick()
|
||||||
|
flvPlayerRef.value?.play?.(nextUrl)
|
||||||
|
flvPlayerRef.value?.setDataUrl?.(nextUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const destroy = () => {
|
||||||
|
playToken.value += 1
|
||||||
|
stopWebrtc()
|
||||||
|
flvPlayerRef.value?.destroy?.()
|
||||||
|
fullscreen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
if (!isWebrtcMode.value) {
|
||||||
|
flvPlayerRef.value?.resize?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const replayCurrentUrl = async () => {
|
||||||
|
if (!currentUrl.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await play(currentUrl.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestFullscreen = async (element: any) => {
|
||||||
|
if (element?.requestFullscreen) {
|
||||||
|
await element.requestFullscreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element?.webkitRequestFullscreen) {
|
||||||
|
await element.webkitRequestFullscreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element?.mozRequestFullScreen) {
|
||||||
|
await element.mozRequestFullScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitFullscreen = async () => {
|
||||||
|
const doc = document as Document & {
|
||||||
|
webkitExitFullscreen?: () => Promise<void> | void
|
||||||
|
mozCancelFullScreen?: () => Promise<void> | void
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.exitFullscreen) {
|
||||||
|
await doc.exitFullscreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.webkitExitFullscreen) {
|
||||||
|
await doc.webkitExitFullscreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.mozCancelFullScreen) {
|
||||||
|
await doc.mozCancelFullScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFullscreen = () => {
|
||||||
|
const doc = document as Document & {
|
||||||
|
msFullscreenElement?: Element | null
|
||||||
|
mozFullScreenElement?: Element | null
|
||||||
|
webkitFullscreenElement?: Element | null
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!(
|
||||||
|
doc.fullscreenElement ||
|
||||||
|
doc.msFullscreenElement ||
|
||||||
|
doc.mozFullScreenElement ||
|
||||||
|
doc.webkitFullscreenElement
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFullscreen = async () => {
|
||||||
|
const container = webrtcContainerRef.value
|
||||||
|
if (!container) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFullscreen()) {
|
||||||
|
await exitFullscreen()
|
||||||
|
} else {
|
||||||
|
await requestFullscreen(container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const captureWebRtcFrame = () => {
|
||||||
|
const videoElement = webrtcVideoRef.value
|
||||||
|
if (!videoElement || videoElement.readyState < 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = videoElement.videoWidth || videoElement.clientWidth
|
||||||
|
canvas.height = videoElement.videoHeight || videoElement.clientHeight
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
if (!context) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
context.drawImage(videoElement, 0, 0, canvas.width, canvas.height)
|
||||||
|
emit('jietu', canvas.toDataURL('image/png'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFullscreenChange = () => {
|
||||||
|
fullscreen.value = isFullscreen()
|
||||||
|
emit('onresize')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
|
destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
destroy,
|
||||||
|
play,
|
||||||
|
resize
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.stream-player {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.webrtc-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.webrtc-video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
background-color: #000;
|
||||||
|
object-fit: fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons-box-right {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jessibuca-btn {
|
||||||
|
width: 20px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 27px;
|
||||||
|
margin: 0 10px;
|
||||||
|
padding: 0 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user