提交 53e29aa7 authored 作者: 王鹏飞's avatar 王鹏飞

feat: 优化直播功能,支持上传会话管理和流控制

上级 28f4122c
......@@ -70,7 +70,10 @@ const { start: startSpeechTranscriber, stop: stopSpeechTranscriber } = useSpeech
})
const isOSSUpload = true
let ossResult = null
let uploadSession = {
result: null,
queue: Promise.resolve(),
}
let totalSize = 0
const {
......@@ -78,6 +81,7 @@ const {
start: startLive,
stop: stopLive,
restartStream,
stopStream,
startTime,
endTime,
duration,
......@@ -85,9 +89,15 @@ const {
switchCamera,
isMobile,
} = useLive({
enabledUserMedia: false,
keepChunks: props.isLocalUpload,
onStart: () => {
console.log('开始直播')
uploadSession = {
result: null,
queue: Promise.resolve(),
}
totalSize = 0
props.onStart && props.onStart()
startChat()
if (!props.isLocalUpload) {
......@@ -98,11 +108,20 @@ const {
totalSize += blob.size
if (props.isLocalUpload) return
if (isOSSUpload) {
ossResult = await appendUpload(blob, {
name: `live/experiment_${experimentId.value}/user_${ssoId}/${fileName.value}.mp4`,
position: ossResult?.nextAppendPosition,
})
fileUrl.value = ossResult.url
const session = uploadSession
const name = `live/experiment_${experimentId.value}/user_${ssoId}/${fileName.value}.mp4`
session.queue = session.queue
.then(async () => {
session.result = await appendUpload(blob, {
name,
position: session.result?.nextAppendPosition,
})
if (uploadSession === session) fileUrl.value = session.result.url
})
.catch((error) => {
console.error('OSS append upload failed:', error)
})
await session.queue
} else {
const base64Data = await readBlobAsBase64(blob)
const jsonData = JSON.stringify({
......@@ -200,31 +219,67 @@ watchEffect(() => {
})
const enabled = ref(false)
const toggleLive = () => {
enabled.value ? startLive() : stopLive()
}
watch(enabled, toggleLive)
const starting = ref(false)
let operationId = 0
let disposed = false
async function start() {
if (!video.value) return
if (window.parent !== window) {
window.parent.postMessage({ action: 'monitor-pause' }, '*')
// 等父页面释放摄像头后再重新获取
await new Promise((r) => setTimeout(r, 500))
if (!video.value || enabled.value || starting.value) return
const currentOperationId = ++operationId
starting.value = true
try {
if (window.parent !== window) {
await new Promise((resolve) => {
const timeout = window.setTimeout(done, 1000)
function done() {
window.clearTimeout(timeout)
window.removeEventListener('message', handleMonitorPaused)
resolve()
}
function handleMonitorPaused(event) {
if (event.source === window.parent && event.data?.action === 'monitor-paused') done()
}
window.addEventListener('message', handleMonitorPaused)
window.parent.postMessage({ action: 'monitor-pause' }, '*')
})
}
if (disposed || currentOperationId !== operationId) return
await restartStream()
if (disposed || currentOperationId !== operationId) {
stopStream()
return
}
startLive()
enabled.value = true
} catch (error) {
stopStream()
console.error('Failed to start live stream:', error)
ElMessage.error('摄像头启动失败,请检查摄像头权限后重试')
if (window.parent !== window) window.parent.postMessage({ action: 'monitor-resume' }, '*')
} finally {
if (currentOperationId === operationId) starting.value = false
}
enabled.value = true
}
function stop() {
if (!video.value) return
operationId += 1
starting.value = false
if (enabled.value) stopLive()
enabled.value = false
stopStream()
if (window.parent !== window) {
// 等录制完全停止后通知父页面恢复监控
setTimeout(() => {
window.parent.postMessage({ action: 'monitor-resume' }, '*')
}, 500)
window.parent.postMessage({ action: 'monitor-resume' }, '*')
}
}
onUnmounted(() => {
disposed = true
stop()
})
defineExpose({ enabled, start, stop })
</script>
......@@ -266,6 +321,8 @@ defineExpose({ enabled, start, stop })
size="large"
round
class="live-ft-btn"
:loading="starting"
:disabled="starting"
@click="enabled ? stop() : start()"
v-if="!recordId">
{{ enabled ? '结束直播' : '开始直播' }}
......
......@@ -33,7 +33,7 @@ export function useLive({ enabledUserMedia = true, keepChunks = true, onStart, o
const currentTime = ref(0)
// 获取设备列表并设置默认设备
const { videoInputs: cameras, audioInputs: microphones } = useDevicesList({ requestPermissions: true })
const { videoInputs: cameras, audioInputs: microphones } = useDevicesList({ requestPermissions: enabledUserMedia })
const currentCamera = computed(() => cameras.value[0]?.deviceId)
const currentMicrophone = computed(() => microphones.value[0]?.deviceId)
......@@ -53,7 +53,7 @@ export function useLive({ enabledUserMedia = true, keepChunks = true, onStart, o
})
// 创建媒体流
const { stream, restart: restartStream } = useUserMedia({
const { stream, restart: restartStream, stop: stopStream } = useUserMedia({
enabled: enabledUserMedia,
constraints: constraints as any,
})
......@@ -110,12 +110,13 @@ export function useLive({ enabledUserMedia = true, keepChunks = true, onStart, o
const start = () => {
if (!mediaRecorder) initializeMediaRecorder()
recordedChunks.value = []
mediaRecorder?.start(2000) // 每100ms触发一次dataavailable事件
mediaRecorder?.start(3000)
}
// 停止录制
const stop = () => {
if (mediaRecorder) mediaRecorder.stop()
mediaRecorder = null
}
// 切换摄像头(移动端前置/后置)
......@@ -131,6 +132,7 @@ export function useLive({ enabledUserMedia = true, keepChunks = true, onStart, o
start,
stop,
restartStream,
stopStream,
startTime,
endTime,
duration,
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论