机场视频换webrtc

This commit is contained in:
2026-05-23 10:56:59 +08:00
parent 225dd2bc20
commit 1f37d154d2
4 changed files with 876 additions and 25 deletions

View File

@@ -17,14 +17,14 @@
<a-spin size="large" v-if="loading" />
<!-- <div id="player" v-else></div> -->
<div class="player" :id="'player_' + sn" v-else>
<jessibuca
<stream-player
:currentVedioDatas="currentVedioDatas"
@jietu="jietu"
ref="jessibucaVideo"
style="width: 100%; height: 100%"
:videoUrl="flvUrl"
:showQu="true"
></jessibuca>
></stream-player>
</div>
<slot name="dingwei"></slot>
</div>
@@ -68,24 +68,15 @@
</div>
</template>
<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 { getLiveCapacityBySn, startLivestream,liveStartUrl } from '/@/api/manage';
import { message } from 'ant-design-vue';
import jswebrtc from '/@/vendors/jswebrtc.js';
import ailabel from './ailabel.vue'
import FlvExtend from 'flv-extend';
import { useUserAuthor } from '/@/hooks/use-g-author';
import jessibuca from './live/jessibuca.vue';
import { debounce } from "lodash-es"
import streamPlayer from './live/stream-player.vue';
import { getAiLive,getAddress,getAiLiveData,saveClueData } from '/@/api/daping';
import { useRoute } from "vue-router";
const route = useRoute()
const { getAiAuthor } = useUserAuthor();
@@ -121,6 +112,7 @@ const videoTypeByKey = {
const deviceSN = ref('');
const flvUrl = ref('');
const jessibucaVideo = ref();
const isShowData = ref(false);
const droneCameraBtn = ref<any>([]);
const xiansuoRef = ref(null)
@@ -381,8 +373,6 @@ const getLiveCapacityDatas = async (sn: string) => {
}
return [];
};
const store:any = inject("storeDataGovernanceDashboard");
const aiRtmpVideo = ref('');
/**
* 打开AI rtsp视频
*/
@@ -394,7 +384,6 @@ const openRtspVideo = async () => {
const rtmpLiveUrl = live.data.aiPullRtmp;
// 将rtmpLiveUrl中的wss替换为https
const fixedRtmpLiveUrl = typeof rtmpLiveUrl === 'string' ? rtmpLiveUrl.replace('wss', 'https') : rtmpLiveUrl;
aiRtmpVideo.value = fixedRtmpLiveUrl;
try {
createRtmpVideo(fixedRtmpLiveUrl);
} catch (error) {}
@@ -404,7 +393,6 @@ const openRtspVideo = async () => {
}
// }
};
const currDroneItem = ref("")
// 获取视频
const getLiveData = async (info: any) => {
console.log(`output->info`,info)
@@ -469,6 +457,12 @@ const getLiveVedioById = async (vl: any) => {
video_quality: 3,
};
currentVedioDatas.value = videoDataJson
console.info("[aixiaorui] dock getLiveVedioById", {
sn: videoDataJson.sn,
videoId: videoDataJson.videoId,
roomID: videoDataJson.roomID,
url: videoDataJson.url
})
// setCurrentVedioDatas(videoDataJson)
// await onStop(videoDataJson.videoId);
@@ -482,10 +476,16 @@ const openLive = async (options: DomByData) => {
loading.value = true
// 开启视频
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)
// let resUrl = res.data.urls.HTTPS_FLV;
let resUrl = res.data.url;
let resUrl = streamUrl;
if (resUrl) {
await createRtmpVideo(resUrl);
}
@@ -510,7 +510,6 @@ const liveStart = async (data: DomByData, url_type: string = '0') => {
const createRtmpVideo = async (url: string) => {
loading.value = false;
// 根据 roomID 创建 获取对应的dom
let remotePlay;
/* remotePlay = document.getElementById('player') as Element;
if (remotePlay) {
deleteChilds(remotePlay);
@@ -526,7 +525,8 @@ const createRtmpVideo = async (url: string) => {
// 将video加入到 dom中
// remotePlay?.appendChild(link);
await nextTick(async () => {
await rtmpVideoData(url, 'player_'+deviceSN.value);
flvUrl.value = url;
await jessibucaVideo.value?.play?.(url);
});
};
let videoPalyer = null;
@@ -657,10 +657,17 @@ function deleteChilds(ele: Element) {
}
}
}
void rtmpVideoData;
void deleteChilds;
const onloadCamera = async () => {
await getDroneCameras(props.sn);
console.info("[aixiaorui] dock onloadCamera", {
sn: props.sn,
liveType: props.liveType,
droneCameraBtn: droneCameraBtn.value
})
if (droneCameraBtn.value.length > 0) {
let cameraIndex: number = 0;
@@ -673,6 +680,11 @@ const onloadCamera = async () => {
getLiveData(droneCameraBtn.value[cameraIndex]);
}else{
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)
let resUrl = res.data.flv
@@ -726,9 +738,9 @@ onMounted(() => {
});
const emit = defineEmits(['close','checkDrone',"updateAi","clsoeAi"]);
const closeVideo = () => {
if (!videoPalyer) return;
videoPalyer.close();
videoPalyer = null;
flvUrl.value = '';
jessibucaVideo.value?.destroy?.();
};
const closeMoadl = () => {
closeVideo();

View File

@@ -79,7 +79,28 @@ const props = defineProps<Props>();
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) => {
videoUrl.value = url;
if (jessibuca) {
destroy();
}
@@ -92,10 +113,12 @@ const play = (url: string) => {
if (jessibuca.hasLoaded()) {
jessibuca.play(url);
syncPlayingVideoDataUrl(url);
} else {
jessibuca.on('load', () => {
console.log('load 播放');
jessibuca.play(url);
syncPlayingVideoDataUrl(url);
});
}
console.info("jessibuca.screenshot",jessibuca.screenshot)
@@ -112,6 +135,7 @@ const init = (url) => {
btnDom.value = document.getElementById('buttonsBox');
console.log('初始化时的地址为: ' + videoUrl.value);
play(videoUrl.value);
syncPlayingVideoDataUrl(videoUrl.value);
}
});
};
@@ -137,7 +161,8 @@ defineExpose({
init,
resize,
play,
destroy
destroy,
setDataUrl: syncPlayingVideoDataUrl
});
onMounted(() => {
showQu.value = props.showQu
@@ -206,6 +231,7 @@ const create = () => {
emits('onresize')
console.log('on load init');
syncPlayingVideoDataUrl(videoUrl.value);
});
jessibuca.on('log', function (msg: string) {
@@ -219,6 +245,7 @@ const create = () => {
});
jessibuca.on('play', function () {
playing.value = true;
syncPlayingVideoDataUrl(videoUrl.value);
});
jessibuca.on('fullscreen', function (msg: boolean) {
console.log('on fullscreen', msg);

View 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>

View 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>