适配微信小程序
This commit is contained in:
@@ -36,7 +36,15 @@
|
||||
</view>
|
||||
|
||||
<!-- 本地推流 -->
|
||||
<!-- autopush: URL 设置后自动推流,组件会自动判断何时准备好 -->
|
||||
<!--
|
||||
关键配置说明:
|
||||
- video-codec: 优先使用硬件编码,兼容性更好
|
||||
- min-bitrate/max-bitrate: 设置合理的码率范围,避免超出设备能力
|
||||
- video-width/video-height: 明确指定分辨率,避免自动选择不支持的分辨率
|
||||
-->
|
||||
<live-pusher
|
||||
v-if="pushUrl"
|
||||
id="local-pusher"
|
||||
class="local-pusher"
|
||||
:url="pushUrl"
|
||||
@@ -45,9 +53,18 @@
|
||||
:enable-camera="!call.camOff"
|
||||
:enable-mic="!call.muted"
|
||||
aspect="9:16"
|
||||
beauty="5"
|
||||
whiteness="3"
|
||||
:beauty="5"
|
||||
:whiteness="3"
|
||||
orientation="vertical"
|
||||
device-position="front"
|
||||
video-codec="hardware"
|
||||
:min-bitrate="200"
|
||||
:max-bitrate="1000"
|
||||
:video-width="360"
|
||||
:video-height="640"
|
||||
audio-quality="high"
|
||||
@statechange="onPusherStateChange"
|
||||
@netstatus="onPusherNetStatus"
|
||||
@error="onPusherError"
|
||||
/>
|
||||
</view>
|
||||
@@ -175,24 +192,42 @@ const statusDotClass = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 监听通话激活,初始化 Context
|
||||
watch(() => call.active, (newVal) => {
|
||||
if (newVal) {
|
||||
// 监听推流地址变化,在 live-pusher 渲染完成后初始化 Context
|
||||
// 关键修复:之前监听 call.active,但此时 pushUrl 还是空的,live-pusher 未渲染
|
||||
// 现在改为监听 pushUrl,确保 live-pusher 渲染后再初始化 Context
|
||||
watch(() => pushUrl.value, (newVal, oldVal) => {
|
||||
if (newVal && !oldVal) {
|
||||
// pushUrl 从空变为有值,live-pusher 即将渲染
|
||||
console.log('[MiniProgramCall] pushUrl 已设置,准备初始化 Context')
|
||||
nextTick(() => {
|
||||
// 传入当前组件实例,确保 live-pusher 能被找到
|
||||
// 在同层渲染模式下,这点尤为重要
|
||||
initPusherContext(instance)
|
||||
// 等待 live-pusher 完全渲染后再初始化
|
||||
setTimeout(() => {
|
||||
console.log('[MiniProgramCall] 延迟后初始化 Pusher Context')
|
||||
initPusherContext(instance)
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const onPusherError = (e: any) => {
|
||||
console.error('Pusher Error:', e)
|
||||
uni.showToast({ title: '推流失败,请检查摄像头权限', icon: 'none' })
|
||||
console.error('[MiniProgramCall] Pusher Error:', e)
|
||||
const detail = e.detail || e
|
||||
if (detail.errCode === 10001) {
|
||||
uni.showToast({ title: '请授权摄像头和麦克风权限', icon: 'none' })
|
||||
} else {
|
||||
uni.showToast({ title: '推流失败,请检查权限设置', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const onPusherNetStatus = (e: any) => {
|
||||
const info = e.detail?.info || e.detail
|
||||
if (info) {
|
||||
console.log('[MiniProgramCall] 推流网络状态:', info)
|
||||
}
|
||||
}
|
||||
|
||||
const onPlayerError = (e: any, userId: string) => {
|
||||
console.error(`Player Error [${userId}]:`, e)
|
||||
console.error(`[MiniProgramCall] Player Error [${userId}]:`, e)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -60,6 +60,10 @@ let currentReceiverUserId = ''
|
||||
// live-pusher 组件上下文
|
||||
let pusherContext: UniApp.LivePusherContext | null = null
|
||||
|
||||
// 推流状态标记
|
||||
let isPushingSucceeded = false // autopush 是否已成功(收到 1009)
|
||||
let isStartingPush = false // 是否正在启动推流(防止并发调用)
|
||||
|
||||
export function useMiniProgramCall() {
|
||||
const authStore = useAuthStore()
|
||||
const chatStore = useChatStore()
|
||||
@@ -273,7 +277,7 @@ export function useMiniProgramCall() {
|
||||
uni.showToast({ title: '服务器未返回推流地址', icon: 'none' })
|
||||
}
|
||||
|
||||
// 设置拉流地址
|
||||
// 设置拉流地址 (包括 Web/H5 用户的 RTMP 流)
|
||||
if (response.pull_urls && response.pull_urls.length > 0) {
|
||||
remoteStreams.value = response.pull_urls.map(p => ({
|
||||
userId: p.user_id,
|
||||
@@ -281,6 +285,12 @@ export function useMiniProgramCall() {
|
||||
flvUrl: p.flv_url,
|
||||
}))
|
||||
console.log('[MiniProgramCall] 拉流地址:', remoteStreams.value)
|
||||
|
||||
// 检测 Web 用户的流并提示
|
||||
const webStreams = remoteStreams.value.filter(s => s.pullUrl && !s.pullUrl.includes('miniprogram'))
|
||||
if (webStreams.length > 0) {
|
||||
console.log('[MiniProgramCall] 检测到 Web 用户流:', webStreams.length)
|
||||
}
|
||||
}
|
||||
|
||||
// 参与者信息
|
||||
@@ -288,11 +298,41 @@ export function useMiniProgramCall() {
|
||||
console.log('[MiniProgramCall] 房间参与者:', response.participants)
|
||||
}
|
||||
|
||||
// 关键修复:使用 nextTick 确保 DOM 更新,live-pusher 的 url 属性生效后再启动
|
||||
// 重置推流状态标记
|
||||
isPushingSucceeded = false
|
||||
isStartingPush = false
|
||||
|
||||
// autopush=true 模式下,live-pusher 会在 URL 设置后自动推流
|
||||
// 时序说明:
|
||||
// 1. pushUrl 设置后,MiniProgramCallWindow.vue 中的 watch 会触发
|
||||
// 2. watch 中会延迟 500ms 后调用 initPusherContext()
|
||||
// 3. autopush 会自动开始推流
|
||||
// 4. 当收到状态码 1009 (Send first video frame) 时,标记推流成功
|
||||
// 5. 备用推流只在 autopush 未成功时触发
|
||||
nextTick(() => {
|
||||
checkAndLogPushStatus()
|
||||
|
||||
// 延迟后检查是否需要手动启动推流(仅作为 autopush 失败的备用方案)
|
||||
setTimeout(() => {
|
||||
startPushing()
|
||||
}, 500) // 延迟500ms确保组件渲染完成
|
||||
// 关键检查:如果 autopush 已经成功,不再调用手动推流
|
||||
if (isPushingSucceeded) {
|
||||
console.log('[MiniProgramCall] ✅ autopush 已成功,跳过备用推流')
|
||||
return
|
||||
}
|
||||
|
||||
if (call.active && pushUrl.value && pusherContext) {
|
||||
console.log('[MiniProgramCall] 🔄 autopush 未成功,尝试手动启动推流')
|
||||
startPushing()
|
||||
} else if (call.active && pushUrl.value && !pusherContext) {
|
||||
console.warn('[MiniProgramCall] ⚠️ pusherContext 未初始化,等待后重试')
|
||||
setTimeout(() => {
|
||||
if (!isPushingSucceeded && call.active && pushUrl.value && pusherContext) {
|
||||
console.log('[MiniProgramCall] 🔄 重试手动启动推流')
|
||||
startPushing()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}, 3000) // 延长到 3 秒,给 autopush 更多时间
|
||||
})
|
||||
|
||||
// 开始计时
|
||||
@@ -383,64 +423,233 @@ export function useMiniProgramCall() {
|
||||
pushUrl.value = ''
|
||||
remoteStreams.value = []
|
||||
currentReceiverUserId = ''
|
||||
|
||||
// 重置推流状态标记
|
||||
isPushingSucceeded = false
|
||||
isStartingPush = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始推流
|
||||
* 检查并请求摄像头和麦克风权限
|
||||
*/
|
||||
function startPushing() {
|
||||
async function checkAndRequestPermissions(): Promise<boolean> {
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
// 检查摄像头权限
|
||||
const cameraRes = await uni.getSetting({})
|
||||
const cameraAuth = (cameraRes as any).authSetting?.['scope.camera']
|
||||
const recordAuth = (cameraRes as any).authSetting?.['scope.record']
|
||||
|
||||
if (cameraAuth === false || recordAuth === false) {
|
||||
// 权限被拒绝,引导用户到设置页
|
||||
uni.showModal({
|
||||
title: '权限提示',
|
||||
content: '需要摄像头和麦克风权限才能进行视频通话,请在设置中开启',
|
||||
confirmText: '去设置',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.openSetting({})
|
||||
}
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (cameraAuth === undefined) {
|
||||
// 未授权,请求授权
|
||||
await uni.authorize({ scope: 'scope.camera' })
|
||||
}
|
||||
if (recordAuth === undefined) {
|
||||
await uni.authorize({ scope: 'scope.record' })
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('[MiniProgramCall] 权限请求失败:', err)
|
||||
return false
|
||||
}
|
||||
// #endif
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查推流状态并记录日志(autopush 模式下使用)
|
||||
*/
|
||||
async function checkAndLogPushStatus() {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 检测是否在开发者工具中运行
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
const isDevtools = systemInfo.platform === 'devtools'
|
||||
|
||||
if (isDevtools) {
|
||||
console.warn('[MiniProgramCall] ⚠️ 开发者工具不支持 live-pusher,请在真机上测试')
|
||||
uni.showToast({ title: '开发者工具不支持推流,请真机测试', icon: 'none', duration: 3000 })
|
||||
console.warn('[MiniProgramCall] ⚠️ 开发者工具不支持 live-pusher')
|
||||
console.warn('[MiniProgramCall] 请使用手机扫码在「真机调试」或「预览」模式测试')
|
||||
uni.showToast({
|
||||
title: '开发者工具不支持视频通话,请真机测试',
|
||||
icon: 'none',
|
||||
duration: 4000
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
const hasPermission = await checkAndRequestPermissions()
|
||||
if (!hasPermission) {
|
||||
console.error('[MiniProgramCall] ❌ 未获得必要权限')
|
||||
uni.showToast({
|
||||
title: '请授权摄像头和麦克风权限',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[MiniProgramCall] 🎬 autopush 模式,推流地址已设置')
|
||||
console.log('[MiniProgramCall] 推流地址:', pushUrl.value)
|
||||
console.log('[MiniProgramCall] 设备信息:', {
|
||||
platform: systemInfo.platform,
|
||||
model: systemInfo.model,
|
||||
system: systemInfo.system,
|
||||
brand: systemInfo.brand,
|
||||
SDKVersion: systemInfo.SDKVersion
|
||||
})
|
||||
|
||||
// 检查基础库版本是否支持 live-pusher
|
||||
const sdkVersion = systemInfo.SDKVersion || ''
|
||||
const versionParts = sdkVersion.split('.').map(Number)
|
||||
if (versionParts[0] < 2 || (versionParts[0] === 2 && versionParts[1] < 9)) {
|
||||
console.warn('[MiniProgramCall] ⚠️ 基础库版本过低,建议升级到 2.9.0+')
|
||||
uni.showToast({
|
||||
title: '微信版本过低,请更新微信',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动开始推流
|
||||
* 关键:在 RTC 模式下,需要先调用 enterRoom() 再调用 start()
|
||||
* 参考网友方案:enterRoom() 后调用 start(),设置 setTimeout 延迟
|
||||
*/
|
||||
async function startPushing() {
|
||||
// #ifdef MP-WEIXIN
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
const isDevtools = systemInfo.platform === 'devtools'
|
||||
|
||||
if (isDevtools) {
|
||||
console.warn('[MiniProgramCall] ⚠️ 开发者工具不支持 live-pusher')
|
||||
return
|
||||
}
|
||||
|
||||
// 防止并发调用
|
||||
if (isStartingPush) {
|
||||
console.log('[MiniProgramCall] ⏳ 推流正在启动中,跳过重复调用')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已经推流成功,不再重复启动
|
||||
if (isPushingSucceeded) {
|
||||
console.log('[MiniProgramCall] ✅ 推流已成功,跳过重复启动')
|
||||
return
|
||||
}
|
||||
|
||||
if (!pusherContext) {
|
||||
console.error('[MiniProgramCall] ❌ Pusher Context 未初始化')
|
||||
console.error('[MiniProgramCall] 请确保:')
|
||||
console.error(' 1. 组件中有 <live-pusher id="local-pusher" ...>')
|
||||
console.error(' 2. 调用了 initPusherContext(getCurrentInstance())')
|
||||
uni.showToast({ title: 'live-pusher 组件未初始化', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 确保 URL 存在
|
||||
if (!pushUrl.value) {
|
||||
console.error('[MiniProgramCall] ❌ 推流地址为空,无法推流')
|
||||
console.error('[MiniProgramCall] ❌ 推流地址为空')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[MiniProgramCall] 开始推流,URL:', pushUrl.value)
|
||||
isStartingPush = true
|
||||
console.log('[MiniProgramCall] 🎬 手动启动推流 - 使用 enterRoom + start 模式')
|
||||
|
||||
// 关键修复:先调用 enterRoom() 进入 RTC 房间
|
||||
// 这是解决 operateXWebLivePusher:fail:internal error 的关键步骤
|
||||
try {
|
||||
// @ts-ignore - enterRoom 是较新的 API,类型定义可能缺失
|
||||
const ctx = pusherContext as any
|
||||
if (typeof ctx.enterRoom === 'function') {
|
||||
ctx.enterRoom({
|
||||
success: () => {
|
||||
console.log('[MiniProgramCall] ✅ enterRoom 成功')
|
||||
// 延迟后调用 start(),确保房间准备就绪
|
||||
setTimeout(() => {
|
||||
doStartPushing()
|
||||
}, 500)
|
||||
},
|
||||
fail: (err: any) => {
|
||||
console.warn('[MiniProgramCall] ⚠️ enterRoom 失败,直接尝试 start:', err)
|
||||
// enterRoom 失败时直接尝试 start
|
||||
doStartPushing()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 如果 enterRoom 不可用,直接调用 start
|
||||
console.log('[MiniProgramCall] enterRoom 不可用,直接调用 start')
|
||||
doStartPushing()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[MiniProgramCall] enterRoom 调用异常,直接尝试 start:', e)
|
||||
doStartPushing()
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际执行推流启动
|
||||
*/
|
||||
function doStartPushing() {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (!pusherContext) {
|
||||
isStartingPush = false
|
||||
return
|
||||
}
|
||||
|
||||
pusherContext.start({
|
||||
success: () => {
|
||||
console.log('[MiniProgramCall] ✅ 推流启动成功')
|
||||
console.log('[MiniProgramCall] ✅ 推流启动成功(start回调)')
|
||||
isStartingPush = false
|
||||
// 注意:真正的推流成功标志是收到状态码 1009
|
||||
},
|
||||
fail: (err: any) => {
|
||||
console.error('[MiniProgramCall] ❌ 推流启动失败:', err)
|
||||
console.error('[MiniProgramCall] 错误信息:', JSON.stringify(err))
|
||||
|
||||
// 详细的错误提示
|
||||
isStartingPush = false
|
||||
|
||||
let errorMsg = '推流失败'
|
||||
if (err.errMsg) {
|
||||
if (err.errMsg.includes('internal error')) {
|
||||
errorMsg = '推流内部错误,请检查RTMP服务器'
|
||||
} else if (err.errMsg.includes('operateLivePusher:fail')) {
|
||||
errorMsg = '推流操作失败'
|
||||
} else if (err.errMsg.includes('permission')) {
|
||||
errorMsg = '缺少摄像头/麦克风权限'
|
||||
const errMsg = err.errMsg || ''
|
||||
const errno = err.errno
|
||||
|
||||
if (errMsg.includes('internal error') || errno === 4) {
|
||||
// internal error 通常是因为推流器已经在运行,或者状态冲突
|
||||
// 如果 autopush 已经在工作,这个错误可以忽略
|
||||
if (isPushingSucceeded) {
|
||||
console.log('[MiniProgramCall] ℹ️ internal error,但 autopush 已成功,忽略')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
uni.showToast({ title: errorMsg, icon: 'none', duration: 3000 })
|
||||
|
||||
// 在开发者工具中,提示用户
|
||||
if (isDevtools) {
|
||||
console.warn('[MiniProgramCall] 💡 提示:开发者工具不支持 live-pusher')
|
||||
console.warn('[MiniProgramCall] 请使用手机扫码在真机预览中测试')
|
||||
errorMsg = '推流服务异常'
|
||||
console.log('[MiniProgramCall] 🔄 internal error,2秒后重试...')
|
||||
setTimeout(() => {
|
||||
if (!isPushingSucceeded && call.active && pusherContext) {
|
||||
isStartingPush = true
|
||||
pusherContext.start({
|
||||
success: () => {
|
||||
console.log('[MiniProgramCall] ✅ 重试推流成功')
|
||||
isStartingPush = false
|
||||
},
|
||||
fail: (e: any) => {
|
||||
console.error('[MiniProgramCall] ❌ 重试推流仍失败:', e)
|
||||
isStartingPush = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 2000)
|
||||
} else if (errMsg.includes('permission') || errno === 10001) {
|
||||
errorMsg = '请授权摄像头和麦克风权限'
|
||||
uni.showToast({ title: errorMsg, icon: 'none', duration: 3000 })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -605,20 +814,45 @@ export function useMiniProgramCall() {
|
||||
break
|
||||
case 1004: // 自动调整分辨率
|
||||
break
|
||||
case 1005: // 成功打开麦克风
|
||||
case 1005: // 推流动态调整分辨率
|
||||
break
|
||||
case 1006: // 推流动态调整码率
|
||||
break
|
||||
case 1007: // 首帧画面采集完成
|
||||
console.log('[MiniProgramCall] ✅ 首帧画面采集完成')
|
||||
break
|
||||
case 1008: // 编码器启动
|
||||
console.log('[MiniProgramCall] ✅ 编码器启动')
|
||||
break
|
||||
case 1009: // 已发送首帧视频 - 这是推流真正成功的标志!
|
||||
console.log('[MiniProgramCall] ✅✅ 推流成功!已发送首帧视频')
|
||||
isPushingSucceeded = true // 关键:标记推流成功
|
||||
isStartingPush = false
|
||||
call.statusText = '通话中'
|
||||
break
|
||||
|
||||
// 警告/网络状态
|
||||
case 1101: // 网络状况不佳:上行带宽不足
|
||||
console.warn('[MiniProgramCall] ⚠️ 网络不佳,上行带宽不足')
|
||||
call.statusText = '网络不佳...'
|
||||
break
|
||||
case 1102: // 网络断连,已启动自动重连(不是编码器失败!)
|
||||
console.log('[MiniProgramCall] ℹ️ 网络断连,正在自动重连...')
|
||||
call.statusText = '重连中...'
|
||||
// 重要:1102 是自动重连,不需要手动干预!
|
||||
// 微信会自动处理重连,我们只需要等待
|
||||
break
|
||||
case 1103: // 摄像头被占用
|
||||
console.error('[MiniProgramCall] ❌ 摄像头被占用')
|
||||
uni.showToast({ title: '摄像头被占用', icon: 'none' })
|
||||
break
|
||||
|
||||
// 麦克风状态
|
||||
case 2027: // 麦克风启动成功
|
||||
console.log('[MiniProgramCall] ✅ 麦克风已打开')
|
||||
break
|
||||
|
||||
// 警告状态
|
||||
case 1101: // 网络状况不佳:上行带宽不足
|
||||
call.statusText = '网络不佳...'
|
||||
break
|
||||
case 1102: // 视频编码器启动失败
|
||||
console.warn('[MiniProgramCall] ⚠️ 视频编码器启动失败')
|
||||
break
|
||||
case 1103: // 摄像头被占用
|
||||
uni.showToast({ title: '摄像头被占用', icon: 'none' })
|
||||
break
|
||||
// RTMP 错误
|
||||
case 3001: // RTMP 服务器建立连接失败
|
||||
console.error('[MiniProgramCall] ❌ RTMP 服务器连接失败')
|
||||
call.statusText = '服务器连接失败'
|
||||
@@ -639,6 +873,8 @@ export function useMiniProgramCall() {
|
||||
case 3005: // RTMP 服务器连接异常断开
|
||||
console.error('[MiniProgramCall] ❌ RTMP 连接异常断开')
|
||||
call.statusText = '连接异常断开'
|
||||
// 连接异常断开时,重置推流状态,允许重试
|
||||
isPushingSucceeded = false
|
||||
break
|
||||
|
||||
// 错误状态
|
||||
@@ -659,6 +895,7 @@ export function useMiniProgramCall() {
|
||||
case -1307: // 推流连接断开
|
||||
console.error('[MiniProgramCall] ❌ 推流连接断开')
|
||||
call.statusText = '连接已断开'
|
||||
isPushingSucceeded = false
|
||||
break
|
||||
|
||||
default:
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"urlCheck" : false,
|
||||
"preloadBackgroundData" : false
|
||||
},
|
||||
"libVersion" : "2.7.0",
|
||||
"libVersion" : "2.10.0",
|
||||
"usingComponents" : true,
|
||||
"darkmode" : true,
|
||||
"themeLocation" : "theme.json",
|
||||
@@ -66,7 +66,7 @@
|
||||
"desc" : "用于获取您的位置信息"
|
||||
},
|
||||
"scope.record" : {
|
||||
"desc" : "用于语音消息录制"
|
||||
"desc" : "用于语音消息录制和视频通话"
|
||||
},
|
||||
"scope.camera" : {
|
||||
"desc" : "用于拍摄照片和视频通话"
|
||||
@@ -75,7 +75,8 @@
|
||||
"desc" : "用于保存图片到相册"
|
||||
}
|
||||
},
|
||||
"requiredPrivateInfos" : [ "chooseImage", "chooseVideo", "chooseLocation", "getLocation" ]
|
||||
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ],
|
||||
"requiredBackgroundModes" : [ "audio", "voip" ]
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
|
||||
Reference in New Issue
Block a user