diff --git a/src/components/call/MiniProgramCallWindow.vue b/src/components/call/MiniProgramCallWindow.vue
index 63fac86..2196beb 100644
--- a/src/components/call/MiniProgramCallWindow.vue
+++ b/src/components/call/MiniProgramCallWindow.vue
@@ -36,7 +36,15 @@
+
+
@@ -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)
}
diff --git a/src/composables/useMiniProgramCall.ts b/src/composables/useMiniProgramCall.ts
index fd3745e..5756c6e 100644
--- a/src/composables/useMiniProgramCall.ts
+++ b/src/composables/useMiniProgramCall.ts
@@ -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 {
+ // #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. 组件中有 ')
- 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:
diff --git a/src/manifest.json b/src/manifest.json
index 3b765f3..42c7254 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -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