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">
|
2024-04-09 09:45:17 -04:00
|
|
|
<div class="bg-base-100 w-full h-full rounded shadow">
|
2024-04-08 11:07:03 -04:00
|
|
|
<div class="w-full h-full p-2">
|
|
|
|
<video v-if="streamExtension === 'flv'" ref="httpStreamFlv" controls />
|
|
|
|
<VideoPlayer
|
|
|
|
class="live-player"
|
|
|
|
v-else-if="configStore.configGui[configStore.configID]"
|
|
|
|
:key="configStore.configID"
|
|
|
|
reference="httpStream"
|
|
|
|
:options="{
|
|
|
|
liveui: true,
|
|
|
|
controls: true,
|
|
|
|
suppressNotSupportedError: true,
|
|
|
|
autoplay: false,
|
|
|
|
preload: 'auto',
|
|
|
|
sources: [
|
|
|
|
{
|
|
|
|
type: 'application/x-mpegURL',
|
|
|
|
src: configStore.configGui[configStore.configID].preview_url,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}"
|
|
|
|
/>
|
|
|
|
</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
|
2024-04-09 09:45:17 -04:00
|
|
|
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
|
2024-04-09 09:45:17 -04:00
|
|
|
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
|
|
|
>
|
|
|
|
{{ secToHMS(playlistStore.remainingSec >= 0 ? playlistStore.remainingSec : 0) }}
|
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">
|
2024-04-09 09:45:17 -04:00
|
|
|
<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">
|
|
|
|
<div v-if="playlistStore.ingestRuns" class="h-1/3 font-bold truncate" title="Live Ingest">
|
|
|
|
Live 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"
|
|
|
|
:title="filename(playlistStore.currentClip)"
|
|
|
|
>
|
|
|
|
{{ filename(playlistStore.currentClip) }}
|
|
|
|
</div>
|
|
|
|
<div class="grow">
|
|
|
|
<strong>Duration:</strong> {{ secToHMS(playlistStore.currentClipDuration) }} |
|
|
|
|
<strong>In:</strong> {{ secToHMS(playlistStore.currentClipIn) }} | <strong>Out:</strong>
|
|
|
|
{{ secToHMS(playlistStore.currentClipOut) }}
|
|
|
|
</div>
|
|
|
|
<div class="h-1/3">
|
2024-04-08 07:53:04 -04:00
|
|
|
<progress
|
|
|
|
class="progress progress-accent w-full"
|
|
|
|
:value="playlistStore.progressValue"
|
|
|
|
max="100"
|
|
|
|
></progress>
|
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">
|
2024-04-09 09:45:17 -04:00
|
|
|
<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="Start Playout Service"
|
|
|
|
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="Jump to last Clip"
|
|
|
|
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="Stop Playout Service"
|
|
|
|
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="Reset Playout State"
|
2024-04-09 09:45:17 -04:00
|
|
|
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="Restart Playout Service"
|
2024-04-09 09:45:17 -04:00
|
|
|
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="Jump to next Clip"
|
|
|
|
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">
|
2023-07-14 07:45:48 -04:00
|
|
|
import { storeToRefs } from 'pinia'
|
2023-01-11 04:54:25 -05:00
|
|
|
import mpegts from 'mpegts.js'
|
|
|
|
|
|
|
|
const { $dayjs } = useNuxtApp()
|
|
|
|
const authStore = useAuth()
|
|
|
|
const configStore = useConfig()
|
|
|
|
const playlistStore = usePlaylist()
|
|
|
|
const { filename, secToHMS, timeToSeconds } = stringFormatter()
|
2023-07-14 07:45:48 -04:00
|
|
|
const { configID } = storeToRefs(useConfig())
|
2023-01-11 04:54:25 -05:00
|
|
|
const contentType = { 'content-type': 'application/json;charset=UTF-8' }
|
|
|
|
|
|
|
|
const breakStatusCheck = ref(false)
|
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()
|
|
|
|
const streamExtension = ref(configStore.configGui[configStore.configID].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,
|
2023-07-13 01:36:27 -04:00
|
|
|
url: configStore.configGui[configStore.configID].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
|
|
|
})
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
breakStatusCheck.value = false
|
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()
|
|
|
|
}
|
|
|
|
|
|
|
|
status()
|
|
|
|
})
|
|
|
|
|
2023-07-14 07:45:48 -04:00
|
|
|
watch([configID], () => {
|
|
|
|
breakStatusCheck.value = false
|
|
|
|
timeStr.value = '00:00:00'
|
|
|
|
playlistStore.remainingSec = -1
|
|
|
|
playlistStore.currentClip = ''
|
|
|
|
playlistStore.ingestRuns = false
|
|
|
|
playlistStore.currentClipDuration = 0
|
|
|
|
playlistStore.currentClipIn = 0
|
|
|
|
playlistStore.currentClipOut = 0
|
|
|
|
|
|
|
|
if (timer.value) {
|
|
|
|
clearTimeout(timer.value)
|
|
|
|
}
|
|
|
|
status()
|
|
|
|
})
|
|
|
|
|
2023-01-11 04:54:25 -05:00
|
|
|
onBeforeUnmount(() => {
|
|
|
|
breakStatusCheck.value = true
|
|
|
|
|
|
|
|
if (timer.value) {
|
|
|
|
clearTimeout(timer.value)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
async function status() {
|
|
|
|
/*
|
|
|
|
Get playout state and information's from current clip.
|
|
|
|
- animate timers
|
|
|
|
- when clip end is reached call API again and set new values
|
|
|
|
*/
|
2023-10-20 02:28:09 -04:00
|
|
|
await playlistStore.playoutStat()
|
2023-01-11 04:54:25 -05:00
|
|
|
|
|
|
|
async function setStatus(resolve: any) {
|
|
|
|
/*
|
|
|
|
recursive function as a endless loop
|
|
|
|
*/
|
2023-01-11 09:51:08 -05:00
|
|
|
timeStr.value = $dayjs().utcOffset(configStore.utcOffset).format('HH:mm:ss')
|
|
|
|
const timeInSec = timeToSeconds(timeStr.value)
|
2023-01-11 04:54:25 -05:00
|
|
|
playlistStore.remainingSec = playlistStore.currentClipStart + playlistStore.currentClipOut - timeInSec
|
|
|
|
const playedSec = playlistStore.currentClipOut - playlistStore.remainingSec
|
2024-04-08 15:33:28 -04:00
|
|
|
|
|
|
|
if (playlistStore.currentClipOut === 0) {
|
|
|
|
playlistStore.progressValue = 0
|
|
|
|
} else {
|
|
|
|
playlistStore.progressValue = (playedSec * 100) / playlistStore.currentClipOut
|
|
|
|
}
|
2023-01-11 04:54:25 -05:00
|
|
|
|
|
|
|
if (breakStatusCheck.value) {
|
|
|
|
return
|
|
|
|
} else if ((playlistStore.playoutIsRunning && playlistStore.remainingSec < 0) || timeInSec % 30 === 0) {
|
|
|
|
// When 30 seconds a passed, get new status.
|
2023-10-20 02:28:09 -04:00
|
|
|
await playlistStore.playoutStat()
|
2023-01-11 04:54:25 -05:00
|
|
|
} else if (!playlistStore.playoutIsRunning) {
|
|
|
|
playlistStore.remainingSec = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
timer.value = setTimeout(() => setStatus(resolve), 1000)
|
|
|
|
}
|
|
|
|
return new Promise((resolve) => setStatus(resolve))
|
|
|
|
}
|
|
|
|
|
|
|
|
async function controlProcess(state: string) {
|
|
|
|
/*
|
|
|
|
Control playout systemd service (start, stop, restart)
|
|
|
|
*/
|
|
|
|
const channel = configStore.configGui[configStore.configID].id
|
|
|
|
|
2023-01-11 07:41:22 -05:00
|
|
|
await $fetch(`/api/control/${channel}/process/`, {
|
2023-01-11 04:54:25 -05:00
|
|
|
method: 'POST',
|
|
|
|
headers: { ...contentType, ...authStore.authHeader },
|
2023-07-13 01:36:27 -04:00
|
|
|
body: JSON.stringify({ command: state }),
|
2023-01-11 04:54:25 -05:00
|
|
|
})
|
|
|
|
|
2023-10-20 02:28:09 -04:00
|
|
|
setTimeout(async () => {
|
|
|
|
await playlistStore.playoutStat()
|
2023-01-11 04:54:25 -05:00
|
|
|
}, 1000)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function controlPlayout(state: string) {
|
|
|
|
/*
|
|
|
|
Control playout:
|
|
|
|
- jump to next clip
|
|
|
|
- jump to last clip
|
|
|
|
- reset playout state
|
|
|
|
*/
|
|
|
|
const channel = configStore.configGui[configStore.configID].id
|
|
|
|
|
2023-01-11 07:41:22 -05:00
|
|
|
await $fetch(`/api/control/${channel}/playout/`, {
|
2023-01-11 04:54:25 -05:00
|
|
|
method: 'POST',
|
|
|
|
headers: { ...contentType, ...authStore.authHeader },
|
2023-07-13 01:36:27 -04:00
|
|
|
body: JSON.stringify({ control: state }),
|
2023-01-11 04:54:25 -05:00
|
|
|
})
|
|
|
|
|
2023-10-20 02:28:09 -04:00
|
|
|
setTimeout(async () => {
|
|
|
|
await playlistStore.playoutStat()
|
2023-01-11 04:54:25 -05:00
|
|
|
}, 1000)
|
|
|
|
}
|
|
|
|
</script>
|