网页之间打电话用webrtc

This commit is contained in:
2025-12-16 09:16:51 +08:00
parent b8ce478ad2
commit 46a4bf26cf
8 changed files with 437 additions and 78 deletions

1
.gitignore vendored
View File

@@ -23,3 +23,4 @@ dist-ssr
*.sln
*.sw?
/dist.zip

View File

@@ -14,7 +14,7 @@ export interface PullURLInfo {
// 参与者信息
export interface ParticipantInfo {
user_id: string
platform: 'h5' | 'app' | 'miniprogram'
platform: 'h5' | 'web' | 'app' | 'miniprogram' | 'wxapp'
has_audio: boolean
has_video: boolean
}
@@ -30,7 +30,7 @@ export interface ICEServerConfig {
export interface JoinCallRoomRequest {
room_id: string
user_id: string
platform: 'h5' | 'app' | 'miniprogram'
platform: 'h5' | 'web' | 'app' | 'miniprogram' | 'wxapp'
}
// 加入通话房间响应
@@ -39,6 +39,7 @@ export interface JoinCallRoomResponse {
platform: string
ice_servers?: ICEServerConfig[]
ws_push_url?: string // H5/App 用 RTMP 模式 WebSocket 推流地址
self_flv_url?: string // H5/App 用 - 自己的 HTTP-FLV 地址(供小程序拉流)
flv_pull_urls?: PullURLInfo[] // H5/App 拉取小程序流的 FLV 地址
push_url?: string // 小程序用 RTMP 推流地址
pull_urls?: PullURLInfo[] // 小程序用 RTMP 拉流地址

View File

@@ -400,13 +400,20 @@ watch(
onUnmounted(() => {
if (flvPlayer) {
try {
// 检查播放器状态,避免空指针错误
// @ts-ignore - flv.js Player 内部属性
if (flvPlayer._mediaElement) {
flvPlayer.pause()
}
flvPlayer.unload()
flvPlayer.detachMediaElement()
flvPlayer.destroy()
} catch (e) {
// 忽略已销毁的播放器错误
if (!(e instanceof TypeError && (e as Error).message?.includes("Cannot read properties of null"))) {
console.error('[CallWindow] 销毁 FLV 播放器失败:', e)
}
}
flvPlayer = null
}
})

View File

@@ -6,7 +6,7 @@ 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 { isRTMPMode, isAutoMode, shouldUseRTMP, parsePlatform, type Platform } from '@/config/call'
import type { ChatMessage, Contact, ICEServerConfig } from '@/types/api'
import type { CallStatus } from '@/types/message'
@@ -69,6 +69,10 @@ export function useWebRTC(
let currentReceiverUserId = ''
let currentRoomId = ''
// Auto 模式:跟踪对方平台
let remotePlatform: Platform = 'unknown'
const useRTMP = ref(false) // 当前通话是否使用 RTMP 模式
// RTMP 模式相关
let wsPushSocket: WebSocket | null = null
let mediaRecorder: MediaRecorder | null = null
@@ -85,12 +89,21 @@ export function useWebRTC(
// 【关键修复】如果是群聊信令,直接忽略!
// 通过判断是否存在 callRoomId 或 participantIds 来识别
if (content.callRoomId || content.participantIds) {
console.log('[WebRTC] 忽略群聊信令')
return
}
const signal = message.call_status as any
const extra = message.extra ? (typeof message.extra === 'string' ? JSON.parse(message.extra) : message.extra) : {}
console.log('[WebRTC] 📨 收到信令:', signal, {
from: message.sender_user_id,
room_id: message.room_id,
call_id: message.call_id,
content: content,
extra: extra
})
if (extra.type) call.type = extra.type
if (signal === 'sync_state') {
@@ -100,7 +113,23 @@ export function useWebRTC(
}
if (signal === 'invite') {
if (call.active) return
// Auto 模式:检测对方平台,决定使用 WebRTC 还是 RTMP
remotePlatform = parsePlatform(extra.platform)
useRTMP.value = shouldUseRTMP(remotePlatform)
console.log('[WebRTC] 📞 收到来电邀请:', {
from: message.sender_user_id,
room_id: message.room_id,
call_id: message.call_id,
type: extra.type,
platform: extra.platform,
remotePlatform,
useRTMP: useRTMP.value
})
if (call.active) {
console.log('[WebRTC] ⚠️ 当前有通话,忽略新来电')
return
}
isCaller.value = false
currentReceiverUserId = message.sender_user_id
if (message.room_id) currentRoomId = message.room_id
@@ -109,28 +138,49 @@ export function useWebRTC(
call.minimized = false
call.status = 'incoming'
call.statusText = `邀请你通话`
console.log('[WebRTC] ✅ 设置来电状态, roomId:', currentRoomId, 'mode:', useRTMP.value ? 'RTMP' : 'WebRTC')
playRingtone('incoming')
if (onIncomingCall) onIncomingCall(message.sender_user_id)
} else if (signal === 'accepted') {
// Auto 模式:从 accepted 信令中检测对方平台,决定最终使用的模式
const previousMode = useRTMP.value
if (extra.platform) {
remotePlatform = parsePlatform(extra.platform)
useRTMP.value = shouldUseRTMP(remotePlatform)
console.log('[WebRTC] 📱 对方平台:', extra.platform, '→', remotePlatform,
'| 模式:', useRTMP.value ? 'RTMP' : 'WebRTC',
previousMode !== useRTMP.value ? '(已切换)' : '')
} else {
// 如果没有 platform 信息,默认认为对方是 h5使用 WebRTC
remotePlatform = 'h5'
useRTMP.value = false
console.log('[WebRTC] ⚠️ 对方未提供平台信息,默认为 h5使用 WebRTC')
}
console.log('[WebRTC] ✅ 对方已接听, 最终模式:', useRTMP.value ? 'RTMP' : 'WebRTC')
stopRingtone()
call.status = 'connected'
call.statusText = '通话中'
startCallTimer()
// 加入房间获取小程序用户的 FLV 流地址RTMP 模式下也会启动 WebSocket 推流)
console.log('[WebRTC] 📡 准备加入房间, roomId:', currentRoomId, 'mode:', useRTMP.value ? 'RTMP' : 'WebRTC')
await joinCallRoomAndGetFlvUrls()
// WebRTC 模式:创建 Offer
if (!isRTMPMode()) {
// WebRTC 模式:创建 Offer(只有 useRTMP 为 false 时才使用 WebRTC
if (!useRTMP.value) {
console.log('[WebRTC] WebRTC 模式:创建 Offer')
if (!pc) await createPC()
const offer = await pc!.createOffer()
await pc!.setLocalDescription(offer)
sendSignal('offer', offer)
} else {
console.log('[WebRTC] RTMP 模式:等待 FLV 流')
}
} else if (signal === 'offer') {
// WebRTC 模式才处理 offer
if (!isRTMPMode()) {
// WebRTC 模式才处理 offerRTMP 模式不使用 WebRTC 信令)
if (!useRTMP.value) {
stopRingtone()
if (!pc) {
await initMedia(call.type === 'video')
@@ -147,19 +197,57 @@ export function useWebRTC(
}
} else if (signal === 'answer') {
// WebRTC 模式才处理 answer
if (!isRTMPMode() && pc) {
// WebRTC 模式才处理 answerRTMP 模式不使用 WebRTC 信令)
if (!useRTMP.value && pc) {
await pc.setRemoteDescription(content)
processPendingCandidates()
}
} else if (signal === 'candidate') {
// WebRTC 模式才处理 candidate
if (!isRTMPMode()) {
// WebRTC 模式才处理 candidateRTMP 模式不使用 WebRTC 信令)
if (!useRTMP.value) {
if (pc && pc.remoteDescription) await pc.addIceCandidate(content)
else pendingCandidates.push(content)
}
} else if (signal === 'participant_joined') {
// 新参与者加入房间(来自小程序端)
console.log('[WebRTC] 🆕 收到 participant_joined 信令:', {
user_id: content.user_id,
platform: content.platform,
flv_url: content.flv_url,
pull_url: content.pull_url
})
// 如果有 FLV 地址,添加或更新小程序流列表
if (content.flv_url && content.user_id) {
const existingIndex = call.miniprogramStreams.findIndex(s => s.userId === content.user_id)
if (existingIndex >= 0) {
// 更新现有流的地址(重要:当用户重新加入时使用新的流地址)
const oldFlvUrl = call.miniprogramStreams[existingIndex].flvUrl
call.miniprogramStreams[existingIndex].flvUrl = content.flv_url
console.log('[WebRTC] 🔄 更新小程序流地址:', content.user_id)
console.log('[WebRTC] 旧地址:', oldFlvUrl)
console.log('[WebRTC] 新地址:', content.flv_url)
} else {
// 添加新的小程序流
call.miniprogramStreams.push({
userId: content.user_id,
flvUrl: content.flv_url
})
console.log('[WebRTC] ✅ 添加小程序流:', content.user_id, content.flv_url)
}
console.log('[WebRTC] 📺 当前小程序流列表:', call.miniprogramStreams)
} else {
console.log('[WebRTC] ⚠️ participant_joined 没有 FLV 地址')
}
// 如果当前没有小程序流,尝试重新获取房间信息
if (call.miniprogramStreams.length === 0 && currentRoomId) {
console.log('[WebRTC] 📡 没有小程序流,尝试刷新房间信息...')
refreshRoomStreams()
}
} else if (['hangup', 'ended', 'answered_elsewhere'].includes(signal)) {
if (isCaller.value) {
if (call.status === 'outgoing') sendSummaryMessage('rejected')
@@ -342,10 +430,19 @@ export function useWebRTC(
}
function sendSignal(status: CallStatus, data?: any, receiverUserId?: string) {
if (!call.id) return
if (!call.id) {
console.warn('[WebRTC] ⚠️ sendSignal: call.id 为空,跳过')
return
}
const targetUserId = receiverUserId || currentReceiverUserId
const roomId = getSafeRoomId(targetUserId)
if (!roomId) return
if (!roomId) {
console.warn('[WebRTC] ⚠️ sendSignal: roomId 为空,跳过')
return
}
console.log('[WebRTC] 📤 发送信令:', status, { to: targetUserId, roomId, callId: call.id })
const payload = {
sender_client_id: wsManager.getClientId() || '',
receiver_user_id: targetUserId,
@@ -354,9 +451,12 @@ export function useWebRTC(
content: JSON.stringify(data || {}),
call_id: call.id,
call_status: status,
extra: JSON.stringify({ type: call.type }),
// 包含平台信息,让对方知道我们是 web 端
extra: JSON.stringify({ type: call.type, platform: 'web' }),
}
messageApi.sendMessage(payload).catch(console.error)
messageApi.sendMessage(payload).catch(err => {
console.error('[WebRTC] ❌ 发送信令失败:', err)
})
}
function sendSyncState(type: 'cam-toggle' | 'mic-toggle', value: boolean) {
@@ -377,6 +477,8 @@ export function useWebRTC(
}
async function startCall(type: 'audio' | 'video', receiverUserId: string, roomId?: string, contact?: Contact) {
console.log('[WebRTC] 📞 发起通话:', { type, receiverUserId, roomId, contactRoomId: contact?.room_id })
if (roomId) currentRoomId = roomId
else if (contact?.room_id) currentRoomId = contact.room_id
else if (getRoomId) {
@@ -385,12 +487,27 @@ export function useWebRTC(
}
if (!currentRoomId) {
console.error('[WebRTC] ❌ 缺少房间信息')
toastStore.error('无法建立通话连接:缺少房间信息')
return false
}
// Auto 模式:发起通话时,默认使用 WebRTC收到 accepted 后根据对方平台调整
// 如果对方是小程序,会在 accepted 处理时切换到 RTMP
// 固定模式:使用配置的模式
if (isAutoMode()) {
// Auto 模式下,发起方先假设使用 WebRTCWeb-to-Web 最优)
// 收到 accepted 后,如果对方是小程序才切换到 RTMP
useRTMP.value = false
console.log('[WebRTC] ✅ 房间ID:', currentRoomId, 'Auto 模式:发起通话,默认 WebRTC等待对方平台信息')
} else {
useRTMP.value = isRTMPMode()
console.log('[WebRTC] ✅ 房间ID:', currentRoomId, '固定模式:', useRTMP.value ? 'RTMP' : 'WebRTC')
}
isCaller.value = true
currentReceiverUserId = receiverUserId
remotePlatform = 'unknown' // 重置远程平台
call.type = type
call.id = Date.now().toString()
call.active = true
@@ -404,21 +521,25 @@ export function useWebRTC(
remoteStream.value = null
try {
console.log('[WebRTC] 🎥 初始化媒体设备...')
await initMedia(type === 'video')
console.log('[WebRTC] ✅ 媒体设备初始化成功')
// 根据模式选择不同的推流方式
if (isRTMPMode()) {
console.log('[WebRTC] 使用 RTMP 模式')
// 根据当前模式选择不同的推流方式
if (useRTMP.value) {
console.log('[WebRTC] 📡 使用 RTMP 模式auto 模式下可能在 accepted 后切换)')
// RTMP 模式:加入房间后会自动开始 WebSocket 推流
} else {
console.log('[WebRTC] 使用 WebRTC 模式')
console.log('[WebRTC] 📡 使用 WebRTC 模式')
await createPC()
}
playRingtone('dialing')
console.log('[WebRTC] 📤 发送 invite 信令给:', receiverUserId)
sendSignal('invite', undefined, receiverUserId)
return true
} catch (error: any) {
console.error('[WebRTC] ❌ 发起通话失败:', error)
toastStore.error(error.message || '无法启动通话')
closeCall()
return false
@@ -426,6 +547,9 @@ export function useWebRTC(
}
async function acceptCall(senderUserId?: string) {
// 接听时 useRTMP 已经在 invite 处理时设置好了
console.log('[WebRTC] 📞 接听来电:', { senderUserId, currentRoomId, mode: useRTMP.value ? 'RTMP' : 'WebRTC' })
isCaller.value = false
stopRingtone()
if (senderUserId) currentReceiverUserId = senderUserId
@@ -435,20 +559,24 @@ export function useWebRTC(
call.remoteCamOff = false
try {
console.log('[WebRTC] 🎥 初始化媒体设备...')
await initMedia(call.type === 'video')
console.log('[WebRTC] ✅ 媒体设备初始化成功')
// 根据模式选择不同的推流方式
if (isRTMPMode()) {
console.log('[WebRTC] 接听:使用 RTMP 模式')
// 根据模式选择不同的推流方式useRTMP 在收到 invite 时已确定)
if (useRTMP.value) {
console.log('[WebRTC] 📡 接听:使用 RTMP 模式,准备加入房间')
// RTMP 模式:加入房间后会自动开始 WebSocket 推流
await joinCallRoomAndGetFlvUrls()
} else {
console.log('[WebRTC] 接听:使用 WebRTC 模式')
console.log('[WebRTC] 📡 接听:使用 WebRTC 模式')
await createPC()
}
console.log('[WebRTC] 📤 发送 accepted 信令给:', currentReceiverUserId)
sendSignal('accepted', undefined, currentReceiverUserId)
} catch (error) {
console.error('[WebRTC] ❌ 接听失败:', error)
endCall()
}
}
@@ -500,37 +628,137 @@ export function useWebRTC(
* 加入通话房间并获取小程序用户的 FLV 流地址
*/
async function joinCallRoomAndGetFlvUrls(): Promise<void> {
console.log('[WebRTC] 🚪 准备加入房间:', { roomId: currentRoomId, userId })
if (!currentRoomId) {
console.error('[WebRTC] ❌ joinCallRoomAndGetFlvUrls: currentRoomId 为空,跳过')
return
}
try {
console.log('[WebRTC] 📤 发送加入房间请求...')
const response = await callApi.joinCallRoom({
room_id: currentRoomId,
user_id: userId,
platform: 'web'
})
console.log('[WebRTC] ✅ 加入房间响应:', JSON.stringify(response, null, 2))
// 检查是否有小程序用户的 FLV 流
console.log('[WebRTC] 📺 检查 FLV 拉流地址:', response.flv_pull_urls)
if (response.flv_pull_urls && response.flv_pull_urls.length > 0) {
console.log('[WebRTC] ✅ 检测到小程序用户流:', response.flv_pull_urls.length, '个')
call.miniprogramStreams = response.flv_pull_urls.map(p => ({
userId: p.user_id,
flvUrl: p.flv_url || ''
})).filter(s => s.flvUrl)
console.log('[WebRTC] 📺 已添加小程序流:', call.miniprogramStreams)
} else {
console.log('[WebRTC] ⚠️ 没有检测到小程序用户流')
}
// 检查参与者列表
console.log('[WebRTC] 👥 房间参与者:', response.participants)
// RTMP 模式:获取 WebSocket 推流地址并开始推流
if (useRTMP.value && response.ws_push_url) {
wsPushUrl.value = response.ws_push_url
console.log('[WebRTC] RTMP 模式WebSocket 推流地址:', response.ws_push_url)
// 确保 localStream 存在再推流,增加重试机制
const tryStartPush = (retryCount = 0) => {
if (localStream.value) {
console.log('[WebRTC] ✅ localStream 就绪,开始 WebSocket 推流')
startWebSocketPush(response.ws_push_url!)
} else if (retryCount < 10) {
console.warn(`[WebRTC] ⚠️ localStream 未就绪500ms 后重试 (${retryCount + 1}/10)`)
setTimeout(() => tryStartPush(retryCount + 1), 500)
} else {
console.error('[WebRTC] ❌ localStream 始终未就绪,放弃推流')
}
}
tryStartPush()
}
// 关键修复:通知房间内其他参与者"我已加入"
// 发送 participant_joined 信令,让小程序端知道可以拉取我的流
sendParticipantJoinedSignal(response)
} catch (error) {
console.error('[WebRTC] 加入房间失败:', error)
}
}
/**
* 发送 participant_joined 信令通知其他参与者
*/
function sendParticipantJoinedSignal(response: callApi.JoinCallRoomResponse) {
console.log('[WebRTC] 📤 准备发送 participant_joined 信令:', { callId: call.id, roomId: currentRoomId, to: currentReceiverUserId })
if (!call.id || !currentRoomId) {
console.warn('[WebRTC] ⚠️ sendParticipantJoinedSignal: call.id 或 roomId 为空,跳过')
return
}
// 构建参与者信息,包含自己的 FLV 地址供小程序拉流
const participantInfo = {
user_id: userId,
platform: 'web',
// Web 端的 FLV 地址(供小程序拉流)
pull_url: response.ws_push_url || '', // WebSocket 推流地址(仅供参考)
flv_url: response.self_flv_url || '', // HTTP-FLV 地址(供小程序 live-player 播放)
}
console.log('[WebRTC] 📤 发送 participant_joined 信令:', participantInfo, 'to:', currentReceiverUserId)
const payload = {
sender_client_id: wsManager.getClientId() || '',
receiver_user_id: currentReceiverUserId,
room_id: currentRoomId,
message_type: 6,
content: JSON.stringify(participantInfo),
call_id: call.id,
call_status: 'participant_joined' as any,
extra: JSON.stringify({ type: call.type, platform: 'web' }),
}
messageApi.sendMessage(payload).then(() => {
console.log('[WebRTC] ✅ participant_joined 信令发送成功')
}).catch(err => {
console.error('[WebRTC] ❌ participant_joined 信令发送失败:', err)
})
}
/**
* 刷新房间流信息(当收到 participant_joined 但没有流地址时)
*/
async function refreshRoomStreams() {
if (!currentRoomId) return
try {
const response = await callApi.joinCallRoom({
room_id: currentRoomId,
user_id: userId,
platform: 'h5'
platform: 'web'
})
console.log('[WebRTC] 加入房间响应:', response)
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 => ({
for (const p of response.flv_pull_urls) {
const exists = call.miniprogramStreams.some(s => s.userId === p.user_id)
if (!exists && p.flv_url) {
call.miniprogramStreams.push({
userId: p.user_id,
flvUrl: p.flv_url || ''
})).filter(s => s.flvUrl)
flvUrl: p.flv_url
})
console.log('[WebRTC] 刷新后添加小程序流:', p.user_id, p.flv_url)
}
// 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)
console.error('[WebRTC] 刷新房间流信息失败:', error)
}
}
@@ -575,6 +803,8 @@ export function useWebRTC(
/**
* 开始 MediaRecorder 录制
* 优先使用 H.264 编码,因为 H.264 可以直接封装到 FLV无需转码
* 如果浏览器不支持 H.264,则降级到 VP8/VP9需要服务器端 FFmpeg 转码)
*/
function startMediaRecorder(): void {
if (!localStream.value || !wsPushSocket) {
@@ -582,17 +812,29 @@ export function useWebRTC(
}
// 检查浏览器支持的 MIME 类型
// 优先级H.264 > VP8 > VP9 > 默认
// H.264 可以直接重封装为 FLVVP8/VP9 需要 FFmpeg 转码
const mimeTypes = [
'video/webm;codecs=vp8,opus',
'video/webm;codecs=vp9,opus',
'video/webm;codecs=h264,opus',
'video/webm',
'video/webm;codecs=h264,opus', // 优先H.264 可直接封装 FLV
'video/webm;codecs=avc1,opus', // 备选H.264 另一种写法
'video/webm;codecs=vp8,opus', // 降级:需要 FFmpeg 转码
'video/webm;codecs=vp9,opus', // 降级:需要 FFmpeg 转码
'video/webm', // 最后兜底
]
let selectedMimeType = ''
let selectedCodec = 'unknown'
for (const mimeType of mimeTypes) {
if (MediaRecorder.isTypeSupported(mimeType)) {
selectedMimeType = mimeType
// 解析出视频编解码器
if (mimeType.includes('h264') || mimeType.includes('avc1')) {
selectedCodec = 'h264'
} else if (mimeType.includes('vp9')) {
selectedCodec = 'vp9'
} else if (mimeType.includes('vp8')) {
selectedCodec = 'vp8'
}
break
}
}
@@ -603,7 +845,12 @@ export function useWebRTC(
return
}
console.log('[WebRTC] 使用编码格式:', selectedMimeType)
console.log('[WebRTC] 使用编码格式:', selectedMimeType, '视频编解码器:', selectedCodec)
// 如果不是 H.264,提示用户可能需要转码
if (selectedCodec !== 'h264') {
console.warn('[WebRTC] ⚠️ 浏览器不支持 H.264 编码,使用', selectedCodec, '(需要服务器转码)')
}
try {
mediaRecorder = new MediaRecorder(localStream.value, {
@@ -612,8 +859,23 @@ export function useWebRTC(
audioBitsPerSecond: 64000, // 64kbps
})
// 首次发送编解码器信息给服务器
let codecInfoSent = false
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0 && wsPushSocket?.readyState === WebSocket.OPEN) {
// 首次发送时先发送编解码器信息JSON 格式)
if (!codecInfoSent) {
const codecInfo = JSON.stringify({
type: 'codec_info',
video_codec: selectedCodec,
audio_codec: 'opus',
mime_type: selectedMimeType
})
wsPushSocket.send(codecInfo)
codecInfoSent = true
console.log('[WebRTC] 已发送编解码器信息:', codecInfo)
}
wsPushSocket.send(event.data)
}
}
@@ -623,7 +885,7 @@ export function useWebRTC(
}
mediaRecorder.onstart = () => {
console.log('[WebRTC] MediaRecorder 开始录制')
console.log('[WebRTC] MediaRecorder 开始录制, 编解码器:', selectedCodec)
}
mediaRecorder.onstop = () => {
@@ -691,24 +953,27 @@ export function useWebRTC(
* @param streamIndex 流索引
*/
function initFlvPlayer(videoElement: HTMLVideoElement, streamIndex: number = 0): flvjs.Player | null {
console.log('[WebRTC] 🎬 初始化 FLV 播放器, streamIndex:', streamIndex, '当前流列表:', call.miniprogramStreams)
if (!flvjs.isSupported()) {
console.error('[WebRTC] 浏览器不支持 flv.js')
console.error('[WebRTC] 浏览器不支持 flv.js')
toastStore.error('您的浏览器不支持 FLV 播放')
return null
}
const stream = call.miniprogramStreams[streamIndex]
if (!stream || !stream.flvUrl) {
console.error('[WebRTC] 没有可用的 FLV 流')
console.error('[WebRTC] 没有可用的 FLV 流, stream:', stream)
return null
}
// 如果已有播放器,先销毁
if (stream.player) {
console.log('[WebRTC] 🔄 销毁已有的 FLV 播放器')
stream.player.destroy()
}
console.log('[WebRTC] 初始化 FLV 播放器:', stream.flvUrl)
console.log('[WebRTC] 🎬 创建 FLV 播放器:', stream.flvUrl)
const player = flvjs.createPlayer({
type: 'flv',
@@ -728,10 +993,18 @@ export function useWebRTC(
player.play()
// 监听错误
player.on(flvjs.Events.ERROR, (errorType, errorDetail) => {
console.error('[WebRTC] FLV 播放错误:', errorType, errorDetail)
player.on(flvjs.Events.ERROR, (errorType, errorDetail, errorInfo) => {
console.error('[WebRTC] FLV 播放错误:', errorType, errorDetail, errorInfo)
if (errorType === flvjs.ErrorTypes.NETWORK_ERROR) {
toastStore.error('网络错误,无法播放小程序视频流')
} else if (errorType === flvjs.ErrorTypes.MEDIA_ERROR) {
// 处理媒体错误,包括编解码器不支持
if (errorDetail === 'CodecUnsupported') {
console.error('[WebRTC] 音视频编解码器不支持,小程序端需要配置 audio-codec="aac"')
toastStore.error('视频格式不兼容,请让小程序端更新后重试')
} else {
toastStore.error('视频播放失败: ' + errorDetail)
}
}
})

View File

@@ -1,12 +1,16 @@
/**
* 通话模式配置
*
* 支持种模式:
* 支持种模式:
* - webrtc: 原有 WebRTC P2P 模式Web 与 Web 之间通话,低延迟
* - rtmp: RTMP 服务器中转模式,支持 Web 与小程序端互通
* - auto: 自动模式根据对方平台自动选择Web-Web 用 WebRTCWeb-小程序 用 RTMP
*/
export type CallMode = 'webrtc' | 'rtmp'
export type CallMode = 'webrtc' | 'rtmp' | 'auto'
// 平台类型
export type Platform = 'h5' | 'app' | 'miniprogram' | 'unknown'
/**
* 获取当前通话模式
@@ -18,6 +22,7 @@ export function getCallMode(): CallMode {
/**
* 是否为 RTMP 模式
* 注意auto 模式下,此函数返回 false需要使用 shouldUseRTMP 判断
*/
export function isRTMPMode(): boolean {
return getCallMode() === 'rtmp'
@@ -25,11 +30,55 @@ export function isRTMPMode(): boolean {
/**
* 是否为 WebRTC 模式
* 注意auto 模式下,此函数返回 false需要使用 shouldUseWebRTC 判断
*/
export function isWebRTCMode(): boolean {
return getCallMode() === 'webrtc'
}
/**
* 是否为 auto 模式
*/
export function isAutoMode(): boolean {
return getCallMode() === 'auto'
}
/**
* 根据对方平台判断是否应该使用 RTMP 模式
* - 如果当前是 rtmp 模式,始终返回 true
* - 如果当前是 webrtc 模式,始终返回 false
* - 如果当前是 auto 模式:
* - 对方是小程序 -> 使用 RTMP
* - 对方是 H5/App -> 使用 WebRTC
*/
export function shouldUseRTMP(remotePlatform?: Platform): boolean {
const mode = getCallMode()
if (mode === 'rtmp') {
return true
}
if (mode === 'webrtc') {
return false
}
// auto 模式:根据对方平台决定
if (remotePlatform === 'miniprogram') {
console.log('[CallConfig] Auto mode: 对方是小程序,使用 RTMP 模式')
return true
}
console.log('[CallConfig] Auto mode: 对方是 Web/App使用 WebRTC 模式')
return false
}
/**
* 根据对方平台判断是否应该使用 WebRTC 模式
*/
export function shouldUseWebRTC(remotePlatform?: Platform): boolean {
return !shouldUseRTMP(remotePlatform)
}
/**
* 设置通话模式(运行时切换)
*/
@@ -37,3 +86,29 @@ export function setCallMode(mode: CallMode): void {
(window as any).__CALL_MODE__ = mode
}
/**
* 解析平台字符串
* 统一不同端发送的平台标识:
* - 'web' / 'h5' -> 'h5' (Web 端)
* - 'wxapp' / 'miniprogram' / 'mini' / 'wechat' -> 'miniprogram' (小程序端)
* - 'app' -> 'app' (原生 App)
*/
export function parsePlatform(platform?: string): Platform {
if (!platform) return 'unknown'
switch (platform.toLowerCase()) {
case 'h5':
case 'web':
return 'h5'
case 'app':
return 'app'
case 'miniprogram':
case 'mini':
case 'wechat':
case 'wxapp': // 新增UniApp 小程序端发送的标识
return 'miniprogram'
default:
return 'unknown'
}
}

View File

@@ -9,7 +9,8 @@ import '@fortawesome/fontawesome-free/css/all.css'
// ========== 通话模式配置 ==========
// - 'webrtc': 原有 WebRTC 模式Web 与 Web 之间 P2P 通话,低延迟)
// - 'rtmp': RTMP 模式(支持与小程序端互通,通过服务器中转)
;(window as any).__CALL_MODE__ = 'rtmp'
// - 'auto': 自动模式Web-Web 用 WebRTCWeb-小程序 用 RTMP
;(window as any).__CALL_MODE__ = 'auto'
const app = createApp(App)

View File

@@ -2307,10 +2307,10 @@ watch(currentTab, (newTab) => {
})
onMounted(async () => {
if (!authStore.isAuthenticated) {
// 每次页面加载/刷新时都验证 token 并获取最新用户信息
const isValid = await authStore.checkAuth()
if (!isValid) { router.push('/login'); return }
}
if (authStore.user) {
await wsManager.connect(authStore.user.id)

View File

@@ -25,7 +25,8 @@ export default defineConfig({
changeOrigin: true,
},
'/upload': {
target: 'http://localhost:12080',
// target: 'http://localhost:12080',
target: 'https://g-ws.nailaoyun.cn',
ws: true,
changeOrigin: true,
},