This commit is contained in:
2026-10-17 19:29:49 +07:00
parent dc0d82d88a
commit a386f3d9e5
14 changed files with 1852 additions and 421 deletions
+23 -97
View File
@@ -1,117 +1,43 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import type { Command, MediaKind, State } from '../types/screen'
import type { State } from '../types/screen'
import { MediaPlayer } from '../utils/media-player'
const audio = ref<HTMLAudioElement | null>(null)
const video = ref<HTMLVideoElement | null>(null)
const kind = ref<MediaKind>('audio')
const musicA = ref<HTMLAudioElement | null>(null)
const musicB = ref<HTMLAudioElement | null>(null)
const showVideo = ref(false)
const bridge = window.screenBridge!
let player: MediaPlayer | undefined
let unsubscribe: (() => void) | undefined
let generation = 0
let lastRevision = -1
let commandId = ''
let lastCommand: Command | null = null
async function metadata(media: HTMLMediaElement) {
if (media.readyState >= 1) return
await new Promise<void>((resolve, reject) => {
const finish = (error?: Error) => {
clearTimeout(timeout)
media.removeEventListener('loadedmetadata', ready)
media.removeEventListener('error', failed)
if (error) reject(error)
else resolve()
}
const ready = () => finish()
const failed = () => finish(new Error('Не удалось прочитать тестовое медиа'))
const timeout = setTimeout(() => finish(new Error('Медиа не подготовлено за 10 секунд')), 10000)
media.addEventListener('loadedmetadata', ready, { once: true })
media.addEventListener('error', failed, { once: true })
if (media.error) failed()
})
}
async function apply(state: State) {
if (state.revision <= lastRevision) return
lastRevision = state.revision
const command = state.command
if (state.status !== 'pending' || !command || command.id === commandId) return
commandId = command.id
lastCommand = command
const current = ++generation
const media = command.media === 'audio' ? audio.value : video.value
const other = command.media === 'audio' ? video.value : audio.value
other?.pause()
kind.value = command.media
if (!media) return
try {
if (command.action === 'pause' || command.action === 'stop') media.pause()
await metadata(media)
if (current !== generation) return
if (command.action === 'play' || command.action === 'stop') media.currentTime = 0
if (command.action === 'seek') media.currentTime = Math.min(command.position, media.duration)
if (command.action === 'play') {
await media.play()
if (current !== generation) return
}
const status = command.action === 'stop' ? 'stopped' : media.paused ? 'paused' : 'playing'
bridge.report({ id: command.id, status })
} catch (error) {
if (current === generation)
bridge.report({
id: command.id,
status: 'error',
error: error instanceof Error ? error.message : 'Ошибка воспроизведения',
})
}
}
function activeMedia() {
return lastCommand?.media === 'audio' ? audio.value : video.value
}
function ended(event: Event) {
const media = activeMedia()
if (lastCommand && event.target === media && media?.ended)
bridge.report({ id: lastCommand.id, status: 'ended' })
}
function failed(event: Event) {
if (lastCommand && event.target === activeMedia())
bridge.report({
id: lastCommand.id,
status: 'error',
error: 'Ошибка локального медиа; проверьте наличие fixtures',
})
let revision = -1
function apply(state: State) {
if (state.revision <= revision) return
revision = state.revision
showVideo.value = state.channels.main.source?.kind === 'video'
player?.apply(state)
}
onMounted(async () => {
for (const media of [audio.value, video.value]) if (media) media.volume = 0.35
unsubscribe = bridge.subscribe((state) => {
void apply(state)
})
await apply(await bridge.state())
player = new MediaPlayer(
{ audio: audio.value!, video: video.value!, music: [musicA.value!, musicB.value!] },
(report) => bridge.report(report),
)
unsubscribe = bridge.subscribe(apply)
apply(await bridge.state())
})
onUnmounted(() => {
generation++
unsubscribe?.()
audio.value?.pause()
video.value?.pause()
player?.destroy()
})
</script>
<template>
<main class="output">
<audio
ref="audio"
:src="'screen-app://app/media/audio'"
preload="auto"
@ended="ended"
@error="failed"
/>
<video
v-show="kind === 'video'"
ref="video"
:src="'screen-app://app/media/video'"
preload="auto"
@ended="ended"
@error="failed"
/>
<audio ref="audio" data-channel="main-audio" preload="auto" />
<video v-show="showVideo" ref="video" data-channel="main-video" preload="auto" playsinline />
<audio ref="musicA" data-channel="music-a" preload="auto" />
<audio ref="musicB" data-channel="music-b" preload="auto" />
</main>
</template>