ffplayout/frontend/components/PlayerControl.vue

345 lines
13 KiB
Vue
Raw Normal View History

2023-01-11 04:54:25 -05:00
<template>
2024-04-08 07:00:03 -04:00
<div class="w-full">
2024-04-08 07:53:04 -04:00
<div class="grid grid-cols-1 md:grid-cols-[auto_512px] xl:grid-cols-[512px_auto_450px]">
2024-04-08 11:07:03 -04:00
<div class="order-1 p-1">
<div class="bg-base-100 w-full h-full rounded shadow flex items-center p-2">
<div class="w-full aspect-video">
2024-04-08 11:07:03 -04:00
<video v-if="streamExtension === 'flv'" ref="httpStreamFlv" controls />
<VideoPlayer
v-else-if="configStore.showPlayer && configStore.channels[configStore.id]"
:key="configStore.id"
2024-04-16 12:13:28 -04:00
class="live-player"
2024-04-08 11:07:03 -04:00
reference="httpStream"
:options="{
liveui: true,
controls: true,
suppressNotSupportedError: true,
autoplay: false,
preload: 'auto',
sources: [
{
type: 'application/x-mpegURL',
src: configStore.channels[configStore.id].preview_url,
2024-04-08 11:07:03 -04:00
},
],
}"
/>
</div>
2024-04-08 07:00:03 -04:00
</div>
</div>
2024-04-08 07:53:04 -04:00
<div
class="order-3 xl:order-2 col-span-1 md:col-span-2 xl:col-span-1 bg-base-200 h-full grid grid-cols-1 xs:grid-cols-2"
>
2024-04-08 07:00:03 -04:00
<div class="col-span-1 p-1">
<div
class="w-full h-full bg-base-100 rounded font-['DigitalNumbers'] p-6 text-3xl md:text-2xl 2xl:text-5xl 4xl:text-7xl tracking-tighter flex justify-center items-center shadow"
2024-04-08 07:00:03 -04:00
>
{{ timeStr }}
</div>
</div>
<div class="col-span-1 p-1 min-h-[50%]">
<div
class="w-full h-full bg-base-100 rounded font-['DigitalNumbers'] p-6 text-3xl md:text-2xl 2xl:text-5xl 4xl:text-7xl tracking-tighter flex justify-center items-center shadow"
2024-04-08 07:00:03 -04:00
>
2024-04-29 03:35:26 -04:00
{{ secToHMS(timeRemaining()) }}
2023-01-11 04:54:25 -05:00
</div>
</div>
2024-04-08 07:00:03 -04:00
2024-04-08 07:53:04 -04:00
<div class="col-span-1 xs:col-span-2 p-1">
<div class="w-full h-full bg-base-100 rounded flex items-center p-3 shadow">
2024-04-08 07:00:03 -04:00
<div class="w-full h-full flex flex-col">
2024-04-13 18:01:45 -04:00
<div v-if="playlistStore.ingestRuns" class="h-1/3 font-bold truncate">
{{ t('control.ingest') }}
2023-01-11 04:54:25 -05:00
</div>
2024-04-08 07:00:03 -04:00
<div
v-else
class="h-1/3 font-bold text truncate"
:class="{ 'text-base-content/60': playlistStore.current.category === 'advertisement' }"
2024-09-11 03:51:16 -04:00
:title="playlistStore.current.title || filename(playlistStore.current.source)"
2024-04-08 07:00:03 -04:00
>
{{
2024-09-11 03:51:16 -04:00
playlistStore.current.title ||
filename(playlistStore.current.source) ||
t('control.noClip')
}}
2024-04-08 07:00:03 -04:00
</div>
<div class="grow">
<strong>{{ t('player.duration') }}:</strong>
{{ secToHMS(playlistStore.current.duration) }} | <strong>{{ t('player.in') }}:</strong>
{{ secToHMS(playlistStore.current.in) }} |
<strong>{{ t('player.out') }}:</strong>
2024-09-11 03:51:16 -04:00
{{ secToHMS(playlistStore.current.out) }}
<template v-if="playlistStore.shift !== 0">
| <strong>{{ t('player.shift') }}:</strong>
{{ secToHMS(playlistStore.shift) }}
</template>
2024-04-08 07:00:03 -04:00
</div>
<div class="h-1/3">
2024-04-08 07:53:04 -04:00
<progress
class="progress progress-accent w-full"
:value="
playlistStore.progressValue && playlistStore.progressValue <= 100
? playlistStore.progressValue
: 0
"
2024-04-08 07:53:04 -04:00
max="100"
2024-04-16 12:13:28 -04:00
/>
2023-01-11 04:54:25 -05:00
</div>
</div>
</div>
</div>
</div>
2024-04-08 07:00:03 -04:00
<div class="order-2 xl:order-3 p-1">
<div class="bg-base-100 h-full flex justify-center rounded shadow">
2024-04-08 07:53:04 -04:00
<div class="w-full h-full grid grid-cols-3">
<div class="text-center">
<div class="w-full h-1/2 aspect-square p-2">
<button
:title="t('control.start')"
2024-04-08 07:53:04 -04:00
class="btn btn-primary h-full w-full text-7xl text-lime-600"
:class="playlistStore.playoutIsRunning && 'shadow-glow shadow-lime-600'"
@click="controlProcess('start')"
>
<i class="bi-play" />
</button>
</div>
<div class="w-full h-1/2 aspect-square p-2">
<button
:title="t('control.last')"
2024-04-08 07:53:04 -04:00
class="btn btn-primary h-full w-full text-7xl text-cyan-600"
@click="controlPlayout('back')"
>
<i class="bi-skip-start" />
</button>
</div>
</div>
<div class="text-center">
<div class="w-full h-1/2 aspect-square p-2">
<button
:title="t('control.stop')"
2024-04-08 07:53:04 -04:00
class="btn btn-primary h-full w-full text-7xl text-red-600"
@click="controlProcess('stop')"
>
<i class="bi-stop" />
</button>
</div>
<div class="w-full h-1/2 aspect-square p-2">
<button
:title="t('control.reset')"
class="btn btn-primary h-full w-full text-6xl text-cyan-600"
2024-04-08 07:53:04 -04:00
@click="controlPlayout('reset')"
>
<i class="bi-arrow-repeat" />
</button>
</div>
</div>
<div class="text-center">
<div class="w-full h-1/2 aspect-square p-2">
<button
:title="t('control.restart')"
class="btn btn-primary h-full w-full text-6xl text-yellow-500"
2024-04-08 07:53:04 -04:00
@click="controlProcess('restart')"
>
<i class="bi-arrow-clockwise" />
</button>
</div>
2024-04-08 07:00:03 -04:00
2024-04-08 07:53:04 -04:00
<div class="w-full h-1/2 aspect-square p-2">
<button
:title="t('control.next')"
2024-04-08 07:53:04 -04:00
class="btn btn-primary h-full w-full text-7xl text-cyan-600"
@click="controlPlayout('next')"
>
<i class="bi-skip-end" />
</button>
</div>
</div>
2024-04-08 07:00:03 -04:00
</div>
</div>
</div>
2023-01-11 04:54:25 -05:00
</div>
</div>
</template>
<script setup lang="ts">
import { throttle } from 'lodash-es'
import { storeToRefs } from 'pinia'
2023-01-11 04:54:25 -05:00
import mpegts from 'mpegts.js'
const { t } = useI18n()
2023-01-11 04:54:25 -05:00
const { $dayjs } = useNuxtApp()
const authStore = useAuth()
const configStore = useConfig()
2024-04-29 03:35:26 -04:00
const indexStore = useIndex()
2023-01-11 04:54:25 -05:00
const playlistStore = usePlaylist()
2024-04-29 03:35:26 -04:00
const { filename, secToHMS } = stringFormatter()
const { id } = storeToRefs(useConfig())
2023-01-11 04:54:25 -05:00
2024-09-11 03:51:16 -04:00
const currentDefault = {
uid: '',
title: t('control.noClip'),
source: t('control.noClip'),
duration: 0,
in: 0,
out: 0,
} as PlaylistItem
playlistStore.current = currentDefault
2023-01-11 09:51:08 -05:00
const timeStr = ref('00:00:00')
2023-01-11 04:54:25 -05:00
const timer = ref()
2024-04-29 03:35:26 -04:00
const errorCounter = ref(0)
const streamExtension = ref(configStore.channels[configStore.id].preview_url.split('.').pop())
2023-07-12 02:59:52 -04:00
const httpStreamFlv = ref(null)
2023-01-11 04:54:25 -05:00
const httpFlvSource = ref({
type: 'flv',
isLive: true,
url: configStore.channels[configStore.id].preview_url,
2023-01-11 04:54:25 -05:00
})
const mpegtsOptions = ref({
lazyLoadMaxDuration: 3 * 60,
2023-07-13 01:36:27 -04:00
liveBufferLatencyChasing: true,
2023-01-11 04:54:25 -05:00
})
const streamUrl = ref(`/data/event/${configStore.channels[configStore.id].id}?endpoint=playout&uuid=${authStore.uuid}`)
2024-04-29 03:35:26 -04:00
2024-04-30 10:57:33 -04:00
// 'http://127.0.0.1:8787/data/event/1?endpoint=playout&uuid=f2f8c29b-712a-48c5-8919-b535d3a05a3a'
2024-04-29 03:35:26 -04:00
const { status, data, error, close } = useEventSource(streamUrl, [], {
autoReconnect: {
retries: -1,
delay: 1000,
onFailed() {
indexStore.sseConnected = false
},
},
})
2023-01-11 04:54:25 -05:00
onMounted(() => {
2023-07-12 02:59:52 -04:00
let player: any = null
2023-01-11 04:54:25 -05:00
2023-07-12 02:59:52 -04:00
if (streamExtension.value === 'flv' && mpegts.getFeatureList().mseLivePlayback) {
if (typeof player !== 'undefined' && player != null) {
player.unload()
player.detachMediaElement()
player.destroy()
player = null
2023-01-11 04:54:25 -05:00
}
player = mpegts.createPlayer(httpFlvSource.value, mpegtsOptions.value)
2023-07-12 02:59:52 -04:00
player.attachMediaElement(httpStreamFlv.value)
2023-01-11 04:54:25 -05:00
player.load()
}
2024-04-29 03:35:26 -04:00
clock()
2023-01-11 04:54:25 -05:00
})
2024-04-29 03:35:26 -04:00
onBeforeUnmount(() => {
indexStore.sseConnected = false
close()
if (timer.value) {
clearTimeout(timer.value)
}
})
2024-04-29 03:35:26 -04:00
watch([status, error], async () => {
if (status.value === 'OPEN') {
indexStore.sseConnected = true
errorCounter.value = 0
} else {
indexStore.sseConnected = false
errorCounter.value += 1
2024-06-24 01:51:50 -04:00
if (errorCounter.value > 11) {
2024-04-29 03:35:26 -04:00
await authStore.obtainUuid()
streamUrl.value = `/data/event/${configStore.channels[configStore.id].id}?endpoint=playout&uuid=${
authStore.uuid
}`
2024-04-29 03:35:26 -04:00
errorCounter.value = 0
}
}
})
watch([data], () => {
if (data.value) {
try {
const playout_status = JSON.parse(data.value)
playlistStore.setStatus(playout_status)
} catch {
2024-04-29 03:35:26 -04:00
indexStore.sseConnected = true
playlistStore.playoutIsRunning = false
2024-04-29 03:35:26 -04:00
resetStatus()
}
}
})
watch([id], () => {
2024-04-29 03:35:26 -04:00
resetStatus()
streamUrl.value = `/data/event/${configStore.channels[configStore.id].id}?endpoint=playout&uuid=${authStore.uuid}`
2023-01-11 04:54:25 -05:00
if (timer.value) {
clearTimeout(timer.value)
}
})
2024-04-29 03:35:26 -04:00
function timeRemaining() {
2024-09-11 03:51:16 -04:00
let remaining = playlistStore.current.out - playlistStore.elapsedSec
2023-01-11 04:54:25 -05:00
2024-04-29 03:35:26 -04:00
if (remaining < 0) {
remaining = 0
}
2023-01-11 04:54:25 -05:00
2024-04-29 03:35:26 -04:00
return remaining
}
2023-01-11 04:54:25 -05:00
2024-04-29 03:35:26 -04:00
async function clock() {
async function setTime(resolve: any) {
timeStr.value = $dayjs().utcOffset(configStore.utcOffset).format('HH:mm:ss')
timer.value = setTimeout(() => setTime(resolve), 1000)
2023-01-11 04:54:25 -05:00
}
2024-04-29 03:35:26 -04:00
return new Promise((resolve) => setTime(resolve))
}
function resetStatus() {
playlistStore.elapsedSec = 0
playlistStore.shift = 0
2024-09-11 03:51:16 -04:00
playlistStore.current = currentDefault
2023-01-11 04:54:25 -05:00
}
const controlProcess = throttle(async (state: string) => {
2023-01-11 04:54:25 -05:00
/*
2024-07-04 10:41:32 -04:00
Control playout (start, stop, restart)
2023-01-11 04:54:25 -05:00
*/
const channel = configStore.channels[configStore.id].id
await $fetch(`/api/control/${channel}/process/`, {
2023-01-11 04:54:25 -05:00
method: 'POST',
2024-04-16 08:07:50 -04:00
headers: { ...configStore.contentType, ...authStore.authHeader },
2023-07-13 01:36:27 -04:00
body: JSON.stringify({ command: state }),
2023-01-11 04:54:25 -05:00
})
}, 800)
2023-01-11 04:54:25 -05:00
const controlPlayout = throttle(async (state: string) => {
2023-01-11 04:54:25 -05:00
/*
Control playout:
- jump to next clip
- jump to last clip
- reset playout state
*/
const channel = configStore.channels[configStore.id].id
2023-01-11 04:54:25 -05:00
await $fetch(`/api/control/${channel}/playout/`, {
2023-01-11 04:54:25 -05:00
method: 'POST',
2024-04-16 08:07:50 -04:00
headers: { ...configStore.contentType, ...authStore.authHeader },
2023-07-13 01:36:27 -04:00
body: JSON.stringify({ control: state }),
2023-01-11 04:54:25 -05:00
})
}, 800)
2023-01-11 04:54:25 -05:00
</script>