up
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user