<template> <div> <div class="container-fluid"> <div class="row control-row"> <div class="col-3 player-col"> <div> <video v-if="configStore.configGui[configStore.configID].preview_url.split('.').pop() === 'flv'" id="httpStream" ref="httpStream" width="100%" controls /> <VideoPlayer class="live-player" v-else-if="videoOptions.sources.length > 0" :key="configStore.configID" reference="httpStream" :options="videoOptions" /> </div> </div> <div class="col"> <div class="row control-col"> <div class="col-8 status-col"> <div class="row status-row"> <div class="col time-col clock-col"> <div class="time-str"> {{ playlistStore.timeStr }} </div> </div> <div class="col time-col counter-col"> <div class="time-str"> {{ secToHMS(playlistStore.remainingSec) }} </div> </div> <div class="col current-clip"> <div class="current-clip-text" :title="filename(playlistStore.currentClip)"> {{ filename(playlistStore.currentClip) }} </div> <div class="current-clip-meta"> <strong>Duration:</strong> {{ secToHMS(playlistStore.currentClipDuration) }} | <strong>In:</strong> {{ secToHMS(playlistStore.currentClipIn) }} | <strong>Out:</strong> {{ secToHMS(playlistStore.currentClipOut) }} </div> <div class="current-clip-progress progress"> <div class="progress-bar bg-warning" :aria-valuenow="playlistStore.progressValue" :style="`width: ${playlistStore.progressValue}%;`" /> </div> </div> </div> </div> <div class="col-4 control-unit-col"> <div class="row control-unit-row"> <div class="col"> <div> <button title="Start Playout Service" class="btn btn-primary control-button control-button-play" :class="isPlaying" @click="controlProcess('start')" > <i class="bi-play" /> </button> </div> </div> <div class="col"> <div> <button title="Stop Playout Service" class="btn btn-primary control-button control-button-stop" @click="controlProcess('stop')" > <i class="bi-stop" /> </button> </div> </div> <div class="col"> <div> <button title="Restart Playout Service" class="btn btn-primary control-button control-button-restart" @click="controlProcess('restart')" > <i class="bi-arrow-clockwise" /> </button> </div> </div> <div class="w-100" /> <div class="col"> <div> <button title="Jump to last Clip" class="btn btn-primary control-button control-button-control" @click="controlPlayout('back')" > <i class="bi-skip-start" /> </button> </div> </div> <div class="col"> <div> <button title="Reset Playout State" class="btn btn-primary control-button control-button-control" @click="controlPlayout('reset')" > <i class="bi-arrow-repeat" /> </button> </div> </div> <div class="col"> <div> <button title="Jump to next Clip" class="btn btn-primary control-button control-button-control" @click="controlPlayout('next')" > <i class="bi-skip-end" /> </button> </div> </div> </div> </div> </div> </div> </div> </div> </div> </template> <script setup lang="ts"> import mpegts from 'mpegts.js' import { useAuth } from '~/stores/auth' import { useConfig } from '~/stores/config' import { usePlaylist } from '~/stores/playlist' const { $dayjs } = useNuxtApp() const authStore = useAuth() const configStore = useConfig() const playlistStore = usePlaylist() const { filename, secToHMS, timeToSeconds } = stringFormatter() const contentType = { 'content-type': 'application/json;charset=UTF-8' } const isPlaying = ref('') const breakStatusCheck = ref(false) const timer = ref() const streamExtension = ref(configStore.configGui[configStore.configID].preview_url.split('.').pop()) const httpStream = ref() const videoOptions = ref({ liveui: true, controls: true, suppressNotSupportedError: true, autoplay: false, preload: 'auto', sources: [] as SourceObject[], }) const httpFlvSource = ref({ type: 'flv', isLive: true, url: '', }) const mpegtsOptions = ref({ enableWorker: true, lazyLoadMaxDuration: 3 * 60, liveBufferLatencyChasing: true, }) onMounted(() => { breakStatusCheck.value = false videoOptions.value.sources = [ { type: 'application/x-mpegURL', src: configStore.configGui[configStore.configID].preview_url, }, ] let player: any if (streamExtension.value === 'flv') { httpFlvSource.value.url = configStore.configGui[configStore.configID].preview_url if (typeof player !== 'undefined') { if (player != null) { player.unload() player.detachMediaElement() player.destroy() player = null } } player = mpegts.createPlayer(httpFlvSource.value, mpegtsOptions.value) player.attachMediaElement(httpStream.value) player.load() } status() }) 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 */ playoutStatus() playlistStore.playoutStat() async function setStatus(resolve: any) { /* recursive function as a endless loop */ playlistStore.timeStr = $dayjs().utcOffset(configStore.utcOffset).format('HH:mm:ss') const timeInSec = timeToSeconds(playlistStore.timeStr) playlistStore.remainingSec = playlistStore.currentClipStart + playlistStore.currentClipOut - timeInSec const playedSec = playlistStore.currentClipOut - playlistStore.remainingSec playlistStore.progressValue = (playedSec * 100) / playlistStore.currentClipOut if (breakStatusCheck.value) { return } else if ((playlistStore.playoutIsRunning && playlistStore.remainingSec < 0) || timeInSec % 30 === 0) { // When 30 seconds a passed, get new status. playoutStatus() playlistStore.playoutStat() } else if (!playlistStore.playoutIsRunning) { playlistStore.remainingSec = 0 } timer.value = setTimeout(() => setStatus(resolve), 1000) } return new Promise((resolve) => setStatus(resolve)) } async function playoutStatus() { /* Check if playout is running, when yes set css class. */ const channel = configStore.configGui[configStore.configID].id await $fetch(`/api/control/${channel}/process/`, { method: 'POST', headers: { ...contentType, ...authStore.authHeader }, body: JSON.stringify({ command: 'status' }), }) .then((response: any) => { if (response === 'active') { isPlaying.value = 'is-playing' } else { playlistStore.playoutIsRunning = false isPlaying.value = '' } }) .catch(() => { isPlaying.value = '' }) } async function controlProcess(state: string) { /* Control playout systemd service (start, stop, restart) */ const channel = configStore.configGui[configStore.configID].id await $fetch(`/api/control/${channel}/process/`, { method: 'POST', headers: { ...contentType, ...authStore.authHeader }, body: JSON.stringify({ command: state }), }) setTimeout(() => { playoutStatus() playlistStore.playoutStat() }, 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 await $fetch(`/api/control/${channel}/playout/`, { method: 'POST', headers: { ...contentType, ...authStore.authHeader }, body: JSON.stringify({ command: state }), }) setTimeout(() => { playoutStatus() playlistStore.playoutStat() }, 1000) } </script> <style lang="scss" scoped> .control-row { min-height: 254px; } .player-col { max-width: 542px; min-width: 380px; padding-top: 2px; padding-bottom: 2px; } .player-col > div { background-color: black; width: 100%; height: 100%; } .live-player { position: relative; top: 50%; transform: translateY(-50%); } .control-col { height: 100%; min-height: 254px; } .status-col { padding-right: 30px; } .control-unit-col { min-width: 250px; padding: 2px 17px 2px 2px; } .control-unit-row { background: $gray-900; height: 100%; margin-right: 0; border-radius: 0.25rem; text-align: center; } .control-unit-row .col { height: 50%; min-height: 90px; } .control-unit-row .col div { height: 80%; margin: 0.6em 0; } .control-button { font-size: 4em; line-height: 0; width: 80%; height: 100%; } .status-row { height: 100%; min-width: 325px; } .status-row .col { margin: 2px; } .time-col { position: relative; background: $gray-900; padding: 0.5em; text-align: center; border-radius: 0.25rem; } .time-str { position: relative; top: 50%; -webkit-transform: translateY(-50%); -ms-transform: translateY(-50%); transform: translateY(-50%); font-family: 'DigitalNumbers-Regular'; font-size: 4.5em; letter-spacing: -0.18em; padding-right: 14px; } .current-clip { background: $gray-900; padding: 10px; border-radius: 0.25rem; min-width: 700px; } .current-clip-text { height: 40%; padding-top: 0.5em; text-align: left; font-weight: bold; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .current-clip-meta { margin-bottom: 0.7em; } .current-clip-progress { top: 80%; margin-top: 0.2em; } .control-button-play { color: $control-button-play; &:hover { color: $control-button-play-hover; } } .is-playing { box-shadow: 0 0 15px $control-button-play; } .control-button-stop { color: $control-button-stop; &:hover { color: $control-button-stop-hover; } } .control-button-restart { color: $control-button-restart; &:hover { color: $control-button-restart-hover; } } .control-button-control { color: $control-button-control; &:hover { color: $control-button-control-hover; } } .clip-progress { height: 5px; padding-top: 3px; } @media (max-width: 1555px) { .control-row { min-height: 200px; } .control-col { height: 100%; min-height: inherit; } .status-col { padding-right: 0; height: 100%; flex: 0 0 60%; max-width: 60%; } .current-clip { min-width: 300px; } .time-str { font-size: 3.5em; } .control-unit-row { margin-right: -30px; } .control-unit-col { flex: 0 0 35%; max-width: 35%; margin: 0 0 0 30px; } } @media (max-width: 1337px) { .status-col { flex: 0 0 47%; max-width: 47%; height: 68%; } .control-unit-col { flex: 0 0 47%; max-width: 47%; } } @media (max-width: 1102px) { .control-unit-row .col { min-height: 70px; padding-right: 0; padding-left: 0; } .control-button { font-size: 2em; } } @media (max-width: 889px) { .control-row { min-height: 540px; } .status-col { flex: 0 0 94%; max-width: 94%; height: 68%; } .control-unit-col { flex: 0 0 94%; max-width: 94%; margin: 0; padding-left: 17px; } } @media (max-width: 689px) { .player-col { flex: 0 0 98%; max-width: 98%; padding-top: 30px; } .control-row { min-height: 830px; } .control-col { margin: 0; } .control-unit-col, .status-col { flex: 0 0 96%; max-width: 96%; } } </style>