网页之间打电话用webrtc
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ dist-ssr
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
/dist.zip
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface PullURLInfo {
|
|||||||
// 参与者信息
|
// 参与者信息
|
||||||
export interface ParticipantInfo {
|
export interface ParticipantInfo {
|
||||||
user_id: string
|
user_id: string
|
||||||
platform: 'h5' | 'app' | 'miniprogram'
|
platform: 'h5' | 'web' | 'app' | 'miniprogram' | 'wxapp'
|
||||||
has_audio: boolean
|
has_audio: boolean
|
||||||
has_video: boolean
|
has_video: boolean
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ export interface ICEServerConfig {
|
|||||||
export interface JoinCallRoomRequest {
|
export interface JoinCallRoomRequest {
|
||||||
room_id: string
|
room_id: string
|
||||||
user_id: string
|
user_id: string
|
||||||
platform: 'h5' | 'app' | 'miniprogram'
|
platform: 'h5' | 'web' | 'app' | 'miniprogram' | 'wxapp'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加入通话房间响应
|
// 加入通话房间响应
|
||||||
@@ -39,6 +39,7 @@ export interface JoinCallRoomResponse {
|
|||||||
platform: string
|
platform: string
|
||||||
ice_servers?: ICEServerConfig[]
|
ice_servers?: ICEServerConfig[]
|
||||||
ws_push_url?: string // H5/App 用 RTMP 模式 WebSocket 推流地址
|
ws_push_url?: string // H5/App 用 RTMP 模式 WebSocket 推流地址
|
||||||
|
self_flv_url?: string // H5/App 用 - 自己的 HTTP-FLV 地址(供小程序拉流)
|
||||||
flv_pull_urls?: PullURLInfo[] // H5/App 拉取小程序流的 FLV 地址
|
flv_pull_urls?: PullURLInfo[] // H5/App 拉取小程序流的 FLV 地址
|
||||||
push_url?: string // 小程序用 RTMP 推流地址
|
push_url?: string // 小程序用 RTMP 推流地址
|
||||||
pull_urls?: PullURLInfo[] // 小程序用 RTMP 拉流地址
|
pull_urls?: PullURLInfo[] // 小程序用 RTMP 拉流地址
|
||||||
|
|||||||
@@ -400,12 +400,19 @@ watch(
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (flvPlayer) {
|
if (flvPlayer) {
|
||||||
try {
|
try {
|
||||||
flvPlayer.pause()
|
// 检查播放器状态,避免空指针错误
|
||||||
|
// @ts-ignore - flv.js Player 内部属性
|
||||||
|
if (flvPlayer._mediaElement) {
|
||||||
|
flvPlayer.pause()
|
||||||
|
}
|
||||||
flvPlayer.unload()
|
flvPlayer.unload()
|
||||||
flvPlayer.detachMediaElement()
|
flvPlayer.detachMediaElement()
|
||||||
flvPlayer.destroy()
|
flvPlayer.destroy()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[CallWindow] 销毁 FLV 播放器失败:', e)
|
// 忽略已销毁的播放器错误
|
||||||
|
if (!(e instanceof TypeError && (e as Error).message?.includes("Cannot read properties of null"))) {
|
||||||
|
console.error('[CallWindow] 销毁 FLV 播放器失败:', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
flvPlayer = null
|
flvPlayer = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import * as callApi from '@/api/modules/call'
|
|||||||
import { wsManager } from '@/api/websocket'
|
import { wsManager } from '@/api/websocket'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
import { useChatStore } from '@/stores/chat'
|
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 { ChatMessage, Contact, ICEServerConfig } from '@/types/api'
|
||||||
import type { CallStatus } from '@/types/message'
|
import type { CallStatus } from '@/types/message'
|
||||||
|
|
||||||
@@ -68,6 +68,10 @@ export function useWebRTC(
|
|||||||
const pendingCandidates: RTCIceCandidate[] = []
|
const pendingCandidates: RTCIceCandidate[] = []
|
||||||
let currentReceiverUserId = ''
|
let currentReceiverUserId = ''
|
||||||
let currentRoomId = ''
|
let currentRoomId = ''
|
||||||
|
|
||||||
|
// Auto 模式:跟踪对方平台
|
||||||
|
let remotePlatform: Platform = 'unknown'
|
||||||
|
const useRTMP = ref(false) // 当前通话是否使用 RTMP 模式
|
||||||
|
|
||||||
// RTMP 模式相关
|
// RTMP 模式相关
|
||||||
let wsPushSocket: WebSocket | null = null
|
let wsPushSocket: WebSocket | null = null
|
||||||
@@ -85,12 +89,21 @@ export function useWebRTC(
|
|||||||
// 【关键修复】如果是群聊信令,直接忽略!
|
// 【关键修复】如果是群聊信令,直接忽略!
|
||||||
// 通过判断是否存在 callRoomId 或 participantIds 来识别
|
// 通过判断是否存在 callRoomId 或 participantIds 来识别
|
||||||
if (content.callRoomId || content.participantIds) {
|
if (content.callRoomId || content.participantIds) {
|
||||||
|
console.log('[WebRTC] 忽略群聊信令')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const signal = message.call_status as any
|
const signal = message.call_status as any
|
||||||
const extra = message.extra ? (typeof message.extra === 'string' ? JSON.parse(message.extra) : message.extra) : {}
|
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 (extra.type) call.type = extra.type
|
||||||
|
|
||||||
if (signal === 'sync_state') {
|
if (signal === 'sync_state') {
|
||||||
@@ -100,7 +113,23 @@ export function useWebRTC(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (signal === 'invite') {
|
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
|
isCaller.value = false
|
||||||
currentReceiverUserId = message.sender_user_id
|
currentReceiverUserId = message.sender_user_id
|
||||||
if (message.room_id) currentRoomId = message.room_id
|
if (message.room_id) currentRoomId = message.room_id
|
||||||
@@ -109,55 +138,114 @@ export function useWebRTC(
|
|||||||
call.minimized = false
|
call.minimized = false
|
||||||
call.status = 'incoming'
|
call.status = 'incoming'
|
||||||
call.statusText = `邀请你通话`
|
call.statusText = `邀请你通话`
|
||||||
|
console.log('[WebRTC] ✅ 设置来电状态, roomId:', currentRoomId, 'mode:', useRTMP.value ? 'RTMP' : 'WebRTC')
|
||||||
playRingtone('incoming')
|
playRingtone('incoming')
|
||||||
if (onIncomingCall) onIncomingCall(message.sender_user_id)
|
if (onIncomingCall) onIncomingCall(message.sender_user_id)
|
||||||
|
|
||||||
} else if (signal === 'accepted') {
|
} 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()
|
stopRingtone()
|
||||||
call.status = 'connected'
|
call.status = 'connected'
|
||||||
call.statusText = '通话中'
|
call.statusText = '通话中'
|
||||||
startCallTimer()
|
startCallTimer()
|
||||||
// 加入房间获取小程序用户的 FLV 流地址(RTMP 模式下也会启动 WebSocket 推流)
|
// 加入房间获取小程序用户的 FLV 流地址(RTMP 模式下也会启动 WebSocket 推流)
|
||||||
|
console.log('[WebRTC] 📡 准备加入房间, roomId:', currentRoomId, 'mode:', useRTMP.value ? 'RTMP' : 'WebRTC')
|
||||||
await joinCallRoomAndGetFlvUrls()
|
await joinCallRoomAndGetFlvUrls()
|
||||||
|
|
||||||
// WebRTC 模式:创建 Offer
|
// WebRTC 模式:创建 Offer(只有 useRTMP 为 false 时才使用 WebRTC)
|
||||||
if (!isRTMPMode()) {
|
if (!useRTMP.value) {
|
||||||
|
console.log('[WebRTC] WebRTC 模式:创建 Offer')
|
||||||
if (!pc) await createPC()
|
if (!pc) await createPC()
|
||||||
const offer = await pc!.createOffer()
|
const offer = await pc!.createOffer()
|
||||||
await pc!.setLocalDescription(offer)
|
await pc!.setLocalDescription(offer)
|
||||||
sendSignal('offer', offer)
|
sendSignal('offer', offer)
|
||||||
|
} else {
|
||||||
|
console.log('[WebRTC] RTMP 模式:等待 FLV 流')
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (signal === 'offer') {
|
} else if (signal === 'offer') {
|
||||||
// WebRTC 模式才处理 offer
|
// WebRTC 模式才处理 offer(RTMP 模式不使用 WebRTC 信令)
|
||||||
if (!isRTMPMode()) {
|
if (!useRTMP.value) {
|
||||||
stopRingtone()
|
stopRingtone()
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
await initMedia(call.type === 'video')
|
await initMedia(call.type === 'video')
|
||||||
await createPC()
|
await createPC()
|
||||||
}
|
}
|
||||||
await pc!.setRemoteDescription(content)
|
await pc!.setRemoteDescription(content)
|
||||||
processPendingCandidates()
|
processPendingCandidates()
|
||||||
const answer = await pc!.createAnswer()
|
const answer = await pc!.createAnswer()
|
||||||
await pc!.setLocalDescription(answer)
|
await pc!.setLocalDescription(answer)
|
||||||
sendSignal('answer', answer, message.sender_user_id)
|
sendSignal('answer', answer, message.sender_user_id)
|
||||||
call.status = 'connected'
|
call.status = 'connected'
|
||||||
call.statusText = '通话中'
|
call.statusText = '通话中'
|
||||||
startCallTimer()
|
startCallTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (signal === 'answer') {
|
} else if (signal === 'answer') {
|
||||||
// WebRTC 模式才处理 answer
|
// WebRTC 模式才处理 answer(RTMP 模式不使用 WebRTC 信令)
|
||||||
if (!isRTMPMode() && pc) {
|
if (!useRTMP.value && pc) {
|
||||||
await pc.setRemoteDescription(content)
|
await pc.setRemoteDescription(content)
|
||||||
processPendingCandidates()
|
processPendingCandidates()
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (signal === 'candidate') {
|
} else if (signal === 'candidate') {
|
||||||
// WebRTC 模式才处理 candidate
|
// WebRTC 模式才处理 candidate(RTMP 模式不使用 WebRTC 信令)
|
||||||
if (!isRTMPMode()) {
|
if (!useRTMP.value) {
|
||||||
if (pc && pc.remoteDescription) await pc.addIceCandidate(content)
|
if (pc && pc.remoteDescription) await pc.addIceCandidate(content)
|
||||||
else pendingCandidates.push(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)) {
|
} else if (['hangup', 'ended', 'answered_elsewhere'].includes(signal)) {
|
||||||
@@ -342,10 +430,19 @@ export function useWebRTC(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendSignal(status: CallStatus, data?: any, receiverUserId?: string) {
|
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 targetUserId = receiverUserId || currentReceiverUserId
|
||||||
const roomId = getSafeRoomId(targetUserId)
|
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 = {
|
const payload = {
|
||||||
sender_client_id: wsManager.getClientId() || '',
|
sender_client_id: wsManager.getClientId() || '',
|
||||||
receiver_user_id: targetUserId,
|
receiver_user_id: targetUserId,
|
||||||
@@ -354,9 +451,12 @@ export function useWebRTC(
|
|||||||
content: JSON.stringify(data || {}),
|
content: JSON.stringify(data || {}),
|
||||||
call_id: call.id,
|
call_id: call.id,
|
||||||
call_status: status,
|
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) {
|
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) {
|
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
|
if (roomId) currentRoomId = roomId
|
||||||
else if (contact?.room_id) currentRoomId = contact.room_id
|
else if (contact?.room_id) currentRoomId = contact.room_id
|
||||||
else if (getRoomId) {
|
else if (getRoomId) {
|
||||||
@@ -385,12 +487,27 @@ export function useWebRTC(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!currentRoomId) {
|
if (!currentRoomId) {
|
||||||
|
console.error('[WebRTC] ❌ 缺少房间信息')
|
||||||
toastStore.error('无法建立通话连接:缺少房间信息')
|
toastStore.error('无法建立通话连接:缺少房间信息')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto 模式:发起通话时,默认使用 WebRTC,收到 accepted 后根据对方平台调整
|
||||||
|
// 如果对方是小程序,会在 accepted 处理时切换到 RTMP
|
||||||
|
// 固定模式:使用配置的模式
|
||||||
|
if (isAutoMode()) {
|
||||||
|
// Auto 模式下,发起方先假设使用 WebRTC(Web-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
|
isCaller.value = true
|
||||||
currentReceiverUserId = receiverUserId
|
currentReceiverUserId = receiverUserId
|
||||||
|
remotePlatform = 'unknown' // 重置远程平台
|
||||||
call.type = type
|
call.type = type
|
||||||
call.id = Date.now().toString()
|
call.id = Date.now().toString()
|
||||||
call.active = true
|
call.active = true
|
||||||
@@ -404,21 +521,25 @@ export function useWebRTC(
|
|||||||
remoteStream.value = null
|
remoteStream.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('[WebRTC] 🎥 初始化媒体设备...')
|
||||||
await initMedia(type === 'video')
|
await initMedia(type === 'video')
|
||||||
|
console.log('[WebRTC] ✅ 媒体设备初始化成功')
|
||||||
|
|
||||||
// 根据模式选择不同的推流方式
|
// 根据当前模式选择不同的推流方式
|
||||||
if (isRTMPMode()) {
|
if (useRTMP.value) {
|
||||||
console.log('[WebRTC] 使用 RTMP 模式')
|
console.log('[WebRTC] 📡 使用 RTMP 模式(auto 模式下可能在 accepted 后切换)')
|
||||||
// RTMP 模式:加入房间后会自动开始 WebSocket 推流
|
// RTMP 模式:加入房间后会自动开始 WebSocket 推流
|
||||||
} else {
|
} else {
|
||||||
console.log('[WebRTC] 使用 WebRTC 模式')
|
console.log('[WebRTC] 📡 使用 WebRTC 模式')
|
||||||
await createPC()
|
await createPC()
|
||||||
}
|
}
|
||||||
|
|
||||||
playRingtone('dialing')
|
playRingtone('dialing')
|
||||||
|
console.log('[WebRTC] 📤 发送 invite 信令给:', receiverUserId)
|
||||||
sendSignal('invite', undefined, receiverUserId)
|
sendSignal('invite', undefined, receiverUserId)
|
||||||
return true
|
return true
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error('[WebRTC] ❌ 发起通话失败:', error)
|
||||||
toastStore.error(error.message || '无法启动通话')
|
toastStore.error(error.message || '无法启动通话')
|
||||||
closeCall()
|
closeCall()
|
||||||
return false
|
return false
|
||||||
@@ -426,6 +547,9 @@ export function useWebRTC(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function acceptCall(senderUserId?: string) {
|
async function acceptCall(senderUserId?: string) {
|
||||||
|
// 接听时 useRTMP 已经在 invite 处理时设置好了
|
||||||
|
console.log('[WebRTC] 📞 接听来电:', { senderUserId, currentRoomId, mode: useRTMP.value ? 'RTMP' : 'WebRTC' })
|
||||||
|
|
||||||
isCaller.value = false
|
isCaller.value = false
|
||||||
stopRingtone()
|
stopRingtone()
|
||||||
if (senderUserId) currentReceiverUserId = senderUserId
|
if (senderUserId) currentReceiverUserId = senderUserId
|
||||||
@@ -435,20 +559,24 @@ export function useWebRTC(
|
|||||||
call.remoteCamOff = false
|
call.remoteCamOff = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('[WebRTC] 🎥 初始化媒体设备...')
|
||||||
await initMedia(call.type === 'video')
|
await initMedia(call.type === 'video')
|
||||||
|
console.log('[WebRTC] ✅ 媒体设备初始化成功')
|
||||||
|
|
||||||
// 根据模式选择不同的推流方式
|
// 根据模式选择不同的推流方式(useRTMP 在收到 invite 时已确定)
|
||||||
if (isRTMPMode()) {
|
if (useRTMP.value) {
|
||||||
console.log('[WebRTC] 接听:使用 RTMP 模式')
|
console.log('[WebRTC] 📡 接听:使用 RTMP 模式,准备加入房间')
|
||||||
// RTMP 模式:加入房间后会自动开始 WebSocket 推流
|
// RTMP 模式:加入房间后会自动开始 WebSocket 推流
|
||||||
await joinCallRoomAndGetFlvUrls()
|
await joinCallRoomAndGetFlvUrls()
|
||||||
} else {
|
} else {
|
||||||
console.log('[WebRTC] 接听:使用 WebRTC 模式')
|
console.log('[WebRTC] 📡 接听:使用 WebRTC 模式')
|
||||||
await createPC()
|
await createPC()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[WebRTC] 📤 发送 accepted 信令给:', currentReceiverUserId)
|
||||||
sendSignal('accepted', undefined, currentReceiverUserId)
|
sendSignal('accepted', undefined, currentReceiverUserId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[WebRTC] ❌ 接听失败:', error)
|
||||||
endCall()
|
endCall()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,37 +628,137 @@ export function useWebRTC(
|
|||||||
* 加入通话房间并获取小程序用户的 FLV 流地址
|
* 加入通话房间并获取小程序用户的 FLV 流地址
|
||||||
*/
|
*/
|
||||||
async function joinCallRoomAndGetFlvUrls(): Promise<void> {
|
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
|
if (!currentRoomId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await callApi.joinCallRoom({
|
const response = await callApi.joinCallRoom({
|
||||||
room_id: currentRoomId,
|
room_id: currentRoomId,
|
||||||
user_id: userId,
|
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) {
|
if (response.flv_pull_urls && response.flv_pull_urls.length > 0) {
|
||||||
console.log('[WebRTC] 检测到小程序用户流:', response.flv_pull_urls)
|
for (const p of response.flv_pull_urls) {
|
||||||
call.miniprogramStreams = response.flv_pull_urls.map(p => ({
|
const exists = call.miniprogramStreams.some(s => s.userId === p.user_id)
|
||||||
userId: p.user_id,
|
if (!exists && p.flv_url) {
|
||||||
flvUrl: p.flv_url || ''
|
call.miniprogramStreams.push({
|
||||||
})).filter(s => s.flvUrl)
|
userId: p.user_id,
|
||||||
}
|
flvUrl: p.flv_url
|
||||||
|
})
|
||||||
// RTMP 模式:获取 WebSocket 推流地址并开始推流
|
console.log('[WebRTC] 刷新后添加小程序流:', p.user_id, p.flv_url)
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('[WebRTC] 加入房间失败:', error)
|
console.error('[WebRTC] 刷新房间流信息失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,6 +803,8 @@ export function useWebRTC(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 开始 MediaRecorder 录制
|
* 开始 MediaRecorder 录制
|
||||||
|
* 优先使用 H.264 编码,因为 H.264 可以直接封装到 FLV,无需转码
|
||||||
|
* 如果浏览器不支持 H.264,则降级到 VP8/VP9(需要服务器端 FFmpeg 转码)
|
||||||
*/
|
*/
|
||||||
function startMediaRecorder(): void {
|
function startMediaRecorder(): void {
|
||||||
if (!localStream.value || !wsPushSocket) {
|
if (!localStream.value || !wsPushSocket) {
|
||||||
@@ -582,17 +812,29 @@ export function useWebRTC(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查浏览器支持的 MIME 类型
|
// 检查浏览器支持的 MIME 类型
|
||||||
|
// 优先级:H.264 > VP8 > VP9 > 默认
|
||||||
|
// H.264 可以直接重封装为 FLV,VP8/VP9 需要 FFmpeg 转码
|
||||||
const mimeTypes = [
|
const mimeTypes = [
|
||||||
'video/webm;codecs=vp8,opus',
|
'video/webm;codecs=h264,opus', // 优先:H.264 可直接封装 FLV
|
||||||
'video/webm;codecs=vp9,opus',
|
'video/webm;codecs=avc1,opus', // 备选:H.264 另一种写法
|
||||||
'video/webm;codecs=h264,opus',
|
'video/webm;codecs=vp8,opus', // 降级:需要 FFmpeg 转码
|
||||||
'video/webm',
|
'video/webm;codecs=vp9,opus', // 降级:需要 FFmpeg 转码
|
||||||
|
'video/webm', // 最后兜底
|
||||||
]
|
]
|
||||||
|
|
||||||
let selectedMimeType = ''
|
let selectedMimeType = ''
|
||||||
|
let selectedCodec = 'unknown'
|
||||||
for (const mimeType of mimeTypes) {
|
for (const mimeType of mimeTypes) {
|
||||||
if (MediaRecorder.isTypeSupported(mimeType)) {
|
if (MediaRecorder.isTypeSupported(mimeType)) {
|
||||||
selectedMimeType = 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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -603,7 +845,12 @@ export function useWebRTC(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[WebRTC] 使用编码格式:', selectedMimeType)
|
console.log('[WebRTC] 使用编码格式:', selectedMimeType, '视频编解码器:', selectedCodec)
|
||||||
|
|
||||||
|
// 如果不是 H.264,提示用户可能需要转码
|
||||||
|
if (selectedCodec !== 'h264') {
|
||||||
|
console.warn('[WebRTC] ⚠️ 浏览器不支持 H.264 编码,使用', selectedCodec, '(需要服务器转码)')
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mediaRecorder = new MediaRecorder(localStream.value, {
|
mediaRecorder = new MediaRecorder(localStream.value, {
|
||||||
@@ -612,8 +859,23 @@ export function useWebRTC(
|
|||||||
audioBitsPerSecond: 64000, // 64kbps
|
audioBitsPerSecond: 64000, // 64kbps
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 首次发送编解码器信息给服务器
|
||||||
|
let codecInfoSent = false
|
||||||
|
|
||||||
mediaRecorder.ondataavailable = (event) => {
|
mediaRecorder.ondataavailable = (event) => {
|
||||||
if (event.data.size > 0 && wsPushSocket?.readyState === WebSocket.OPEN) {
|
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)
|
wsPushSocket.send(event.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -623,7 +885,7 @@ export function useWebRTC(
|
|||||||
}
|
}
|
||||||
|
|
||||||
mediaRecorder.onstart = () => {
|
mediaRecorder.onstart = () => {
|
||||||
console.log('[WebRTC] MediaRecorder 开始录制')
|
console.log('[WebRTC] MediaRecorder 开始录制, 编解码器:', selectedCodec)
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaRecorder.onstop = () => {
|
mediaRecorder.onstop = () => {
|
||||||
@@ -691,24 +953,27 @@ export function useWebRTC(
|
|||||||
* @param streamIndex 流索引
|
* @param streamIndex 流索引
|
||||||
*/
|
*/
|
||||||
function initFlvPlayer(videoElement: HTMLVideoElement, streamIndex: number = 0): flvjs.Player | null {
|
function initFlvPlayer(videoElement: HTMLVideoElement, streamIndex: number = 0): flvjs.Player | null {
|
||||||
|
console.log('[WebRTC] 🎬 初始化 FLV 播放器, streamIndex:', streamIndex, '当前流列表:', call.miniprogramStreams)
|
||||||
|
|
||||||
if (!flvjs.isSupported()) {
|
if (!flvjs.isSupported()) {
|
||||||
console.error('[WebRTC] 浏览器不支持 flv.js')
|
console.error('[WebRTC] ❌ 浏览器不支持 flv.js')
|
||||||
toastStore.error('您的浏览器不支持 FLV 播放')
|
toastStore.error('您的浏览器不支持 FLV 播放')
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const stream = call.miniprogramStreams[streamIndex]
|
const stream = call.miniprogramStreams[streamIndex]
|
||||||
if (!stream || !stream.flvUrl) {
|
if (!stream || !stream.flvUrl) {
|
||||||
console.error('[WebRTC] 没有可用的 FLV 流')
|
console.error('[WebRTC] ❌ 没有可用的 FLV 流, stream:', stream)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果已有播放器,先销毁
|
// 如果已有播放器,先销毁
|
||||||
if (stream.player) {
|
if (stream.player) {
|
||||||
|
console.log('[WebRTC] 🔄 销毁已有的 FLV 播放器')
|
||||||
stream.player.destroy()
|
stream.player.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[WebRTC] 初始化 FLV 播放器:', stream.flvUrl)
|
console.log('[WebRTC] 🎬 创建 FLV 播放器:', stream.flvUrl)
|
||||||
|
|
||||||
const player = flvjs.createPlayer({
|
const player = flvjs.createPlayer({
|
||||||
type: 'flv',
|
type: 'flv',
|
||||||
@@ -728,10 +993,18 @@ export function useWebRTC(
|
|||||||
player.play()
|
player.play()
|
||||||
|
|
||||||
// 监听错误
|
// 监听错误
|
||||||
player.on(flvjs.Events.ERROR, (errorType, errorDetail) => {
|
player.on(flvjs.Events.ERROR, (errorType, errorDetail, errorInfo) => {
|
||||||
console.error('[WebRTC] FLV 播放错误:', errorType, errorDetail)
|
console.error('[WebRTC] FLV 播放错误:', errorType, errorDetail, errorInfo)
|
||||||
if (errorType === flvjs.ErrorTypes.NETWORK_ERROR) {
|
if (errorType === flvjs.ErrorTypes.NETWORK_ERROR) {
|
||||||
toastStore.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* 通话模式配置
|
* 通话模式配置
|
||||||
*
|
*
|
||||||
* 支持两种模式:
|
* 支持三种模式:
|
||||||
* - webrtc: 原有 WebRTC P2P 模式,Web 与 Web 之间通话,低延迟
|
* - webrtc: 原有 WebRTC P2P 模式,Web 与 Web 之间通话,低延迟
|
||||||
* - rtmp: RTMP 服务器中转模式,支持 Web 与小程序端互通
|
* - rtmp: RTMP 服务器中转模式,支持 Web 与小程序端互通
|
||||||
|
* - auto: 自动模式,根据对方平台自动选择(Web-Web 用 WebRTC,Web-小程序 用 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 模式
|
* 是否为 RTMP 模式
|
||||||
|
* 注意:auto 模式下,此函数返回 false,需要使用 shouldUseRTMP 判断
|
||||||
*/
|
*/
|
||||||
export function isRTMPMode(): boolean {
|
export function isRTMPMode(): boolean {
|
||||||
return getCallMode() === 'rtmp'
|
return getCallMode() === 'rtmp'
|
||||||
@@ -25,11 +30,55 @@ export function isRTMPMode(): boolean {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否为 WebRTC 模式
|
* 是否为 WebRTC 模式
|
||||||
|
* 注意:auto 模式下,此函数返回 false,需要使用 shouldUseWebRTC 判断
|
||||||
*/
|
*/
|
||||||
export function isWebRTCMode(): boolean {
|
export function isWebRTCMode(): boolean {
|
||||||
return getCallMode() === 'webrtc'
|
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
|
(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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import '@fortawesome/fontawesome-free/css/all.css'
|
|||||||
// ========== 通话模式配置 ==========
|
// ========== 通话模式配置 ==========
|
||||||
// - 'webrtc': 原有 WebRTC 模式(Web 与 Web 之间 P2P 通话,低延迟)
|
// - 'webrtc': 原有 WebRTC 模式(Web 与 Web 之间 P2P 通话,低延迟)
|
||||||
// - 'rtmp': RTMP 模式(支持与小程序端互通,通过服务器中转)
|
// - 'rtmp': RTMP 模式(支持与小程序端互通,通过服务器中转)
|
||||||
;(window as any).__CALL_MODE__ = 'rtmp'
|
// - 'auto': 自动模式(Web-Web 用 WebRTC,Web-小程序 用 RTMP)
|
||||||
|
;(window as any).__CALL_MODE__ = 'auto'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
|||||||
@@ -2307,10 +2307,10 @@ watch(currentTab, (newTab) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!authStore.isAuthenticated) {
|
// 每次页面加载/刷新时都验证 token 并获取最新用户信息
|
||||||
const isValid = await authStore.checkAuth()
|
const isValid = await authStore.checkAuth()
|
||||||
if (!isValid) { router.push('/login'); return }
|
if (!isValid) { router.push('/login'); return }
|
||||||
}
|
|
||||||
if (authStore.user) {
|
if (authStore.user) {
|
||||||
await wsManager.connect(authStore.user.id)
|
await wsManager.connect(authStore.user.id)
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export default defineConfig({
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/upload': {
|
'/upload': {
|
||||||
target: 'http://localhost:12080',
|
// target: 'http://localhost:12080',
|
||||||
|
target: 'https://g-ws.nailaoyun.cn',
|
||||||
ws: true,
|
ws: true,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user