bug修复

This commit is contained in:
2025-12-15 14:13:56 +08:00
parent 2ac44c2fa3
commit b8ce478ad2
8 changed files with 583 additions and 24 deletions

View File

@@ -11,6 +11,7 @@
"@ant-design/icons-vue": "^7.0.0",
"@fortawesome/fontawesome-free": "^7.1.0",
"axios": "^1.6.0",
"flv.js": "^1.6.2",
"pinia": "^2.1.0",
"vue": "^3.4.0",
"vue-router": "^4.2.0"

21
pnpm-lock.yaml generated
View File

@@ -17,6 +17,9 @@ importers:
axios:
specifier: ^1.6.0
version: 1.13.2
flv.js:
specifier: ^1.6.2
version: 1.6.2
pinia:
specifier: ^2.1.0
version: 2.3.1(typescript@5.3.3)(vue@3.5.25(typescript@5.3.3))
@@ -673,6 +676,9 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
es6-promise@4.2.8:
resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'}
@@ -705,6 +711,9 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
flv.js@1.6.2:
resolution: {integrity: sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
@@ -1172,6 +1181,9 @@ packages:
typescript:
optional: true
webworkify-webpack@2.1.5:
resolution: {integrity: sha512-2akF8FIyUvbiBBdD+RoHpoTbHMQF2HwjcxfDvgztAX5YwbZNyrtfUMgvfgFVsgDhDPVTlkbb5vyasqDHfIDPQw==}
snapshots:
'@alloc/quick-lru@5.2.0': {}
@@ -1669,6 +1681,8 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.2
es6-promise@4.2.8: {}
esbuild@0.21.5:
optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5
@@ -1719,6 +1733,11 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
flv.js@1.6.2:
dependencies:
es6-promise: 4.2.8
webworkify-webpack: 2.1.5
follow-redirects@1.15.11: {}
form-data@4.0.5:
@@ -2132,3 +2151,5 @@ snapshots:
'@vue/shared': 3.5.25
optionalDependencies:
typescript: 5.3.3
webworkify-webpack@2.1.5: {}

View File

@@ -11,6 +11,8 @@
:is-mobile="isMobile"
:local-stream="localStream"
:remote-stream="remoteStream"
:init-flv-player="webrtcStore.webrtc?.initFlvPlayer"
:has-miniprogram-streams="webrtcStore.webrtc?.hasMiniprogramStreams"
@end-call="webrtcStore.webrtc.endCall"
@accept-call="webrtcStore.webrtc.acceptCall"
@toggle-mute="webrtcStore.webrtc.toggleMute"

81
src/api/modules/call.ts Normal file
View File

@@ -0,0 +1,81 @@
import request from '../request'
/**
* 通话相关 API
*/
// 拉流地址信息
export interface PullURLInfo {
user_id: string
url: string
flv_url?: string
}
// 参与者信息
export interface ParticipantInfo {
user_id: string
platform: 'h5' | 'app' | 'miniprogram'
has_audio: boolean
has_video: boolean
}
// ICE 服务器配置
export interface ICEServerConfig {
urls: string[]
username?: string
credential?: string
}
// 加入通话房间请求
export interface JoinCallRoomRequest {
room_id: string
user_id: string
platform: 'h5' | 'app' | 'miniprogram'
}
// 加入通话房间响应
export interface JoinCallRoomResponse {
room_id: string
platform: string
ice_servers?: ICEServerConfig[]
ws_push_url?: string // H5/App 用 RTMP 模式 WebSocket 推流地址
flv_pull_urls?: PullURLInfo[] // H5/App 拉取小程序流的 FLV 地址
push_url?: string // 小程序用 RTMP 推流地址
pull_urls?: PullURLInfo[] // 小程序用 RTMP 拉流地址
participants: ParticipantInfo[]
}
// 离开通话房间请求
export interface LeaveCallRoomRequest {
room_id: string
user_id: string
}
/**
* 加入通话房间
* H5/App 加入时会返回房间内小程序用户的 FLV 拉流地址
*/
export function joinCallRoom(data: JoinCallRoomRequest) {
return request.post<JoinCallRoomResponse>('/call/join', data)
}
/**
* 离开通话房间
*/
export function leaveCallRoom(data: LeaveCallRoomRequest) {
return request.post<{ left: boolean }>('/call/leave', data)
}
/**
* 获取通话房间信息
*/
export function getCallRoom(roomId: string) {
return request.get<{
room_id: string
call_type: string
is_group_call: boolean
participant_count: number
participants: ParticipantInfo[]
}>(`/call/room/${roomId}`)
}

View File

@@ -66,8 +66,9 @@
</div>
</div>
<!-- B. 远程视频流 -->
<!-- B. 远程视频流 (WebRTC) -->
<video
v-show="!hasFlvStream"
ref="remoteVideoRef"
class="w-full h-full transition-opacity duration-500 absolute inset-0 z-0 bg-black"
:class="[
@@ -78,6 +79,28 @@
playsinline
></video>
<!-- B2. 远程视频流 (FLV - 小程序流) -->
<video
v-show="hasFlvStream"
ref="flvVideoRef"
class="w-full h-full transition-opacity duration-500 absolute inset-0 z-0 bg-black"
:class="[
{ 'opacity-0': call.type === 'audio' || call.status !== 'connected' },
call.minimized ? 'object-contain' : 'object-cover'
]"
autoplay
playsinline
></video>
<!-- 小程序流标识 -->
<div
v-if="hasFlvStream && call.status === 'connected' && !call.minimized"
class="absolute top-20 left-4 z-30 bg-green-500/80 text-white text-xs px-2 py-1 rounded flex items-center gap-1"
>
<i class="fas fa-mobile-alt"></i>
<span>小程序</span>
</div>
<!-- C. 本地视频流 -->
<div
v-show="shouldShowLocalVideo"
@@ -148,9 +171,11 @@
</template>
<script setup lang="ts">
import { ref, watchEffect, computed, onMounted, reactive, nextTick } from 'vue'
import { ref, watchEffect, computed, onMounted, onUnmounted, reactive, nextTick, watch } from 'vue'
import type flvjs from 'flv.js'
import Avatar from '@/components/common/Avatar.vue' // 引入 Avatar
import type { Contact } from '@/types/api'
import type { FlvStreamInfo } from '@/composables/useWebRTC'
interface Props {
call: {
@@ -163,11 +188,15 @@ interface Props {
camOff: boolean
remoteCamOff: boolean
duration: number
miniprogramStreams?: FlvStreamInfo[]
}
target: Contact | null
isMobile: boolean
localStream: MediaStream | null
remoteStream: MediaStream | null
// FLV 播放相关(用于播放小程序流)
initFlvPlayer?: (videoElement: HTMLVideoElement, streamIndex?: number) => flvjs.Player | null
hasMiniprogramStreams?: () => boolean
}
const props = defineProps<Props>()
@@ -182,8 +211,15 @@ const emit = defineEmits<{
const localVideoRef = ref<HTMLVideoElement | null>(null)
const remoteVideoRef = ref<HTMLVideoElement | null>(null)
const flvVideoRef = ref<HTMLVideoElement | null>(null) // FLV 播放器视频元素
const windowRef = ref<HTMLElement | null>(null)
// 是否有小程序流需要播放
const hasFlvStream = computed(() => {
return props.hasMiniprogramStreams?.() ||
(props.call.miniprogramStreams && props.call.miniprogramStreams.length > 0)
})
const shouldShowLocalVideo = computed(() => {
return props.call.type === 'video' &&
!props.call.minimized &&
@@ -335,4 +371,43 @@ watchEffect(() => {
videoEl.srcObject = null
}
})
// 监听小程序流,初始化 FLV 播放器
let flvPlayer: flvjs.Player | null = null
watch(
() => [props.call.miniprogramStreams, props.call.status, flvVideoRef.value],
() => {
// 当有小程序流且通话已连接时,初始化 FLV 播放器
if (
props.call.status === 'connected' &&
props.call.miniprogramStreams &&
props.call.miniprogramStreams.length > 0 &&
flvVideoRef.value &&
props.initFlvPlayer
) {
// 避免重复初始化
if (!flvPlayer) {
console.log('[CallWindow] 初始化 FLV 播放器')
flvPlayer = props.initFlvPlayer(flvVideoRef.value, 0)
}
}
},
{ immediate: true }
)
// 组件卸载时清理 FLV 播放器
onUnmounted(() => {
if (flvPlayer) {
try {
flvPlayer.pause()
flvPlayer.unload()
flvPlayer.detachMediaElement()
flvPlayer.destroy()
} catch (e) {
console.error('[CallWindow] 销毁 FLV 播放器失败:', e)
}
flvPlayer = null
}
})
</script>

View File

@@ -1,9 +1,12 @@
import { reactive, shallowRef, ref } from 'vue'
import flvjs from 'flv.js'
import * as messageApi from '@/api/modules/message'
import * as systemApi from '@/api/modules/system'
import * as callApi from '@/api/modules/call'
import { wsManager } from '@/api/websocket'
import { useToastStore } from '@/stores/toast'
import { useChatStore } from '@/stores/chat'
import { isRTMPMode } from '@/config/call'
import type { ChatMessage, Contact, ICEServerConfig } from '@/types/api'
import type { CallStatus } from '@/types/message'
@@ -20,6 +23,15 @@ export interface CallState {
remoteCamOff: boolean
duration: number
startTime: number | null
// 小程序流信息
miniprogramStreams: FlvStreamInfo[]
}
// FLV 流信息(来自小程序端)
export interface FlvStreamInfo {
userId: string
flvUrl: string
player?: flvjs.Player
}
const RINGTONE_INCOMING_BASE64 = 'data:audio/mp3;base64,//uQxAAAAAAAAAAAAEluZm8AAAAPAAAAHgAABOYADQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NAAAAAAJDTEFtZTMuMTAwBK8AAAAAAAAAABQJAAHIAAAAHgAABObK82LdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uQxAAAAAAABAAAAAAAAAAASbXAzLm9yZwAAAP8AAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uQxAEAMzI2hAAAAANIAAAAQAAAEAQGBAIAQBAEAQCBwAAAAe3/8w9///3v//+57//3///8w9///3v//+57//3///8AAKAAX/4EFwR//uQxBEAM8I2fAAAAANIAAAAQAAAEAAAAAAB///+BACAAAAAAH/9R//qD//6g////+oAAAD/4IHgAAAAAA//6gAAAAAT//uQxBIAOQI2fAAAAANIAAAAQAAAE//////////////////////////////////////////////////////////////////uQxBsAOYI2fAAAAANIAAAAQAAAE//////////////////////////////////////////////////////////////////uQxCsAOYI2fAAAAANIAAAAQAAAE//////////////////////////////////////////////////////////////////uQxDQAOYI2fAAAAANIAAAAQAAAE//////////////////////////////////////////////////////////////////uQxE4AOYI2fAAAAANIAAAAQAAAE//////////////////////////////////////////////////////////////////uQxE4AOYI2fAAAAANIAAAAQAAAE//////////////////////////////////////////////////////////////////uQxF4AOYI2fAAAAANIAAAAQAAAE//////////////////////////////////////////////////////////////////uQxGwAOYI2fAAAAANIAAAAQAAAE//////////////////////////////////////////////////////////////////uQxHgAOYI2fAAAAANIAAAAQAAAE//////////////////////////////////////////////////////////////////uQxIUAAANIAAAAAQAIAAAB//////////////////////////////////////////////////////////////////////////////////uQxIkAAANIAAAAAQAIAAAB////////////////////////////////////////////////////////////////////////////////'
@@ -45,6 +57,7 @@ export function useWebRTC(
remoteCamOff: false,
duration: 0,
startTime: null,
miniprogramStreams: [],
})
const isCaller = ref(false)
@@ -56,6 +69,11 @@ export function useWebRTC(
let currentReceiverUserId = ''
let currentRoomId = ''
// RTMP 模式相关
let wsPushSocket: WebSocket | null = null
let mediaRecorder: MediaRecorder | null = null
const wsPushUrl = ref<string>('')
const audioIncoming = new Audio(RINGTONE_INCOMING_BASE64)
audioIncoming.loop = true
@@ -99,12 +117,20 @@ export function useWebRTC(
call.status = 'connected'
call.statusText = '通话中'
startCallTimer()
// 加入房间获取小程序用户的 FLV 流地址RTMP 模式下也会启动 WebSocket 推流)
await joinCallRoomAndGetFlvUrls()
// WebRTC 模式:创建 Offer
if (!isRTMPMode()) {
if (!pc) await createPC()
const offer = await pc!.createOffer()
await pc!.setLocalDescription(offer)
sendSignal('offer', offer)
}
} else if (signal === 'offer') {
// WebRTC 模式才处理 offer
if (!isRTMPMode()) {
stopRingtone()
if (!pc) {
await initMedia(call.type === 'video')
@@ -118,16 +144,21 @@ export function useWebRTC(
call.status = 'connected'
call.statusText = '通话中'
startCallTimer()
}
} else if (signal === 'answer') {
if (pc) {
// WebRTC 模式才处理 answer
if (!isRTMPMode() && pc) {
await pc.setRemoteDescription(content)
processPendingCandidates()
}
} else if (signal === 'candidate') {
// WebRTC 模式才处理 candidate
if (!isRTMPMode()) {
if (pc && pc.remoteDescription) await pc.addIceCandidate(content)
else pendingCandidates.push(content)
}
} else if (['hangup', 'ended', 'answered_elsewhere'].includes(signal)) {
if (isCaller.value) {
@@ -374,7 +405,16 @@ export function useWebRTC(
try {
await initMedia(type === 'video')
// 根据模式选择不同的推流方式
if (isRTMPMode()) {
console.log('[WebRTC] 使用 RTMP 模式')
// RTMP 模式:加入房间后会自动开始 WebSocket 推流
} else {
console.log('[WebRTC] 使用 WebRTC 模式')
await createPC()
}
playRingtone('dialing')
sendSignal('invite', undefined, receiverUserId)
return true
@@ -396,7 +436,17 @@ export function useWebRTC(
try {
await initMedia(call.type === 'video')
// 根据模式选择不同的推流方式
if (isRTMPMode()) {
console.log('[WebRTC] 接听:使用 RTMP 模式')
// RTMP 模式:加入房间后会自动开始 WebSocket 推流
await joinCallRoomAndGetFlvUrls()
} else {
console.log('[WebRTC] 接听:使用 WebRTC 模式')
await createPC()
}
sendSignal('accepted', undefined, currentReceiverUserId)
} catch (error) {
endCall()
@@ -415,6 +465,8 @@ export function useWebRTC(
}
// 被叫方不发送消息,由发起方统一发送
sendSignal('hangup')
// 离开通话房间
leaveCallRoom()
closeCall()
}
@@ -426,6 +478,10 @@ export function useWebRTC(
call.statusText = ''
call.id = null
stopCallTimer()
// 停止 WebSocket 推流 (RTMP 模式)
stopWebSocketPush()
if (pc) {
pc.close()
pc = null
@@ -435,6 +491,280 @@ export function useWebRTC(
localStream.value = null
}
remoteStream.value = null
// 清理 FLV 播放器
destroyFlvPlayers()
call.miniprogramStreams = []
}
/**
* 加入通话房间并获取小程序用户的 FLV 流地址
*/
async function joinCallRoomAndGetFlvUrls(): Promise<void> {
if (!currentRoomId) return
try {
const response = await callApi.joinCallRoom({
room_id: currentRoomId,
user_id: userId,
platform: 'h5'
})
console.log('[WebRTC] 加入房间响应:', response)
// 检查是否有小程序用户的 FLV 流
if (response.flv_pull_urls && response.flv_pull_urls.length > 0) {
console.log('[WebRTC] 检测到小程序用户流:', response.flv_pull_urls)
call.miniprogramStreams = response.flv_pull_urls.map(p => ({
userId: p.user_id,
flvUrl: p.flv_url || ''
})).filter(s => s.flvUrl)
}
// RTMP 模式:获取 WebSocket 推流地址并开始推流
if (isRTMPMode() && response.ws_push_url) {
wsPushUrl.value = response.ws_push_url
console.log('[WebRTC] RTMP模式WebSocket推流地址:', response.ws_push_url)
// 开始 WebSocket 推流
if (localStream.value) {
startWebSocketPush(response.ws_push_url)
}
}
} catch (error) {
console.error('[WebRTC] 加入房间失败:', error)
}
}
/**
* 开始 WebSocket 推流 (RTMP 模式)
* 使用 MediaRecorder 编码本地流,通过 WebSocket 发送到服务器
*/
function startWebSocketPush(wsUrl: string): void {
if (!localStream.value) {
console.error('[WebRTC] 无法推流:本地流不存在')
return
}
// 关闭已有连接
stopWebSocketPush()
console.log('[WebRTC] 开始 WebSocket 推流:', wsUrl)
// 创建 WebSocket 连接
wsPushSocket = new WebSocket(wsUrl)
wsPushSocket.onopen = () => {
console.log('[WebRTC] WebSocket 连接已建立')
// 连接成功后开始录制
startMediaRecorder()
}
wsPushSocket.onclose = (event) => {
console.log('[WebRTC] WebSocket 连接已关闭:', event.code, event.reason)
}
wsPushSocket.onerror = (error) => {
console.error('[WebRTC] WebSocket 错误:', error)
toastStore.error('推流连接失败')
}
wsPushSocket.onmessage = (event) => {
// 处理服务器消息(如有需要)
console.log('[WebRTC] WebSocket 收到消息:', event.data)
}
}
/**
* 开始 MediaRecorder 录制
*/
function startMediaRecorder(): void {
if (!localStream.value || !wsPushSocket) {
return
}
// 检查浏览器支持的 MIME 类型
const mimeTypes = [
'video/webm;codecs=vp8,opus',
'video/webm;codecs=vp9,opus',
'video/webm;codecs=h264,opus',
'video/webm',
]
let selectedMimeType = ''
for (const mimeType of mimeTypes) {
if (MediaRecorder.isTypeSupported(mimeType)) {
selectedMimeType = mimeType
break
}
}
if (!selectedMimeType) {
console.error('[WebRTC] 浏览器不支持 WebM 录制')
toastStore.error('您的浏览器不支持视频推流')
return
}
console.log('[WebRTC] 使用编码格式:', selectedMimeType)
try {
mediaRecorder = new MediaRecorder(localStream.value, {
mimeType: selectedMimeType,
videoBitsPerSecond: 500000, // 500kbps
audioBitsPerSecond: 64000, // 64kbps
})
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0 && wsPushSocket?.readyState === WebSocket.OPEN) {
wsPushSocket.send(event.data)
}
}
mediaRecorder.onerror = (error) => {
console.error('[WebRTC] MediaRecorder 错误:', error)
}
mediaRecorder.onstart = () => {
console.log('[WebRTC] MediaRecorder 开始录制')
}
mediaRecorder.onstop = () => {
console.log('[WebRTC] MediaRecorder 停止录制')
}
// 每 100ms 发送一次数据
mediaRecorder.start(100)
} catch (error) {
console.error('[WebRTC] 创建 MediaRecorder 失败:', error)
toastStore.error('初始化推流失败')
}
}
/**
* 停止 WebSocket 推流
*/
function stopWebSocketPush(): void {
// 停止 MediaRecorder
if (mediaRecorder) {
try {
if (mediaRecorder.state !== 'inactive') {
mediaRecorder.stop()
}
} catch (e) {
// 忽略错误
}
mediaRecorder = null
}
// 关闭 WebSocket
if (wsPushSocket) {
try {
wsPushSocket.close()
} catch (e) {
// 忽略错误
}
wsPushSocket = null
}
wsPushUrl.value = ''
console.log('[WebRTC] WebSocket 推流已停止')
}
/**
* 离开通话房间
*/
async function leaveCallRoom(): Promise<void> {
if (!currentRoomId) return
try {
await callApi.leaveCallRoom({
room_id: currentRoomId,
user_id: userId
})
} catch (error) {
console.error('[WebRTC] 离开房间失败:', error)
}
}
/**
* 初始化 FLV 播放器
* @param videoElement 视频元素
* @param streamIndex 流索引
*/
function initFlvPlayer(videoElement: HTMLVideoElement, streamIndex: number = 0): flvjs.Player | null {
if (!flvjs.isSupported()) {
console.error('[WebRTC] 浏览器不支持 flv.js')
toastStore.error('您的浏览器不支持 FLV 播放')
return null
}
const stream = call.miniprogramStreams[streamIndex]
if (!stream || !stream.flvUrl) {
console.error('[WebRTC] 没有可用的 FLV 流')
return null
}
// 如果已有播放器,先销毁
if (stream.player) {
stream.player.destroy()
}
console.log('[WebRTC] 初始化 FLV 播放器:', stream.flvUrl)
const player = flvjs.createPlayer({
type: 'flv',
url: stream.flvUrl,
isLive: true,
hasAudio: true,
hasVideo: true,
}, {
enableWorker: false,
enableStashBuffer: false,
stashInitialSize: 128,
lazyLoad: false,
})
player.attachMediaElement(videoElement)
player.load()
player.play()
// 监听错误
player.on(flvjs.Events.ERROR, (errorType, errorDetail) => {
console.error('[WebRTC] FLV 播放错误:', errorType, errorDetail)
if (errorType === flvjs.ErrorTypes.NETWORK_ERROR) {
toastStore.error('网络错误,无法播放小程序视频流')
}
})
// 保存播放器引用
stream.player = player
return player
}
/**
* 销毁所有 FLV 播放器
*/
function destroyFlvPlayers(): void {
for (const stream of call.miniprogramStreams) {
if (stream.player) {
try {
stream.player.pause()
stream.player.unload()
stream.player.detachMediaElement()
stream.player.destroy()
} catch (e) {
console.error('[WebRTC] 销毁 FLV 播放器失败:', e)
}
stream.player = undefined
}
}
}
/**
* 检查是否有小程序流需要播放
*/
function hasMiniprogramStreams(): boolean {
return call.miniprogramStreams.length > 0
}
async function processPendingCandidates() {
@@ -487,6 +817,10 @@ export function useWebRTC(
handleSignaling,
toggleMute,
toggleCamera,
formatDuration
formatDuration,
// FLV 播放相关(用于小程序流)
initFlvPlayer,
destroyFlvPlayers,
hasMiniprogramStreams,
}
}

39
src/config/call.ts Normal file
View File

@@ -0,0 +1,39 @@
/**
* 通话模式配置
*
* 支持两种模式:
* - webrtc: 原有 WebRTC P2P 模式Web 与 Web 之间通话,低延迟
* - rtmp: RTMP 服务器中转模式,支持 Web 与小程序端互通
*/
export type CallMode = 'webrtc' | 'rtmp'
/**
* 获取当前通话模式
* 从 window.__CALL_MODE__ 读取,默认为 'webrtc'
*/
export function getCallMode(): CallMode {
return (window as any).__CALL_MODE__ || 'webrtc'
}
/**
* 是否为 RTMP 模式
*/
export function isRTMPMode(): boolean {
return getCallMode() === 'rtmp'
}
/**
* 是否为 WebRTC 模式
*/
export function isWebRTCMode(): boolean {
return getCallMode() === 'webrtc'
}
/**
* 设置通话模式(运行时切换)
*/
export function setCallMode(mode: CallMode): void {
(window as any).__CALL_MODE__ = mode
}

View File

@@ -5,6 +5,12 @@ import router from './router'
import './assets/styles/main.scss'
// 引入 FontAwesome 主 CSS
import '@fortawesome/fontawesome-free/css/all.css'
// ========== 通话模式配置 ==========
// - 'webrtc': 原有 WebRTC 模式Web 与 Web 之间 P2P 通话,低延迟)
// - 'rtmp': RTMP 模式(支持与小程序端互通,通过服务器中转)
;(window as any).__CALL_MODE__ = 'rtmp'
const app = createApp(App)
app.use(createPinia())