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

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

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