feat(display): countdown, fallback su immagine e dissolvenza nell'overlay time out
L'overlay ora riproduce il video scelto dal controller mostrando un countdown 00:30→00:00 calcolato dallo stato condiviso. Se il video finisce prima dei 30s subentra un'immagine "TIME OUT + nome squadra" fino allo scadere; se dura più di 30s si applica una dissolvenza CSS negli ultimi istanti. In caso di riconnessione a metà time out il video viene riallineato (currentTime) al punto corretto.
This commit is contained in:
@@ -113,10 +113,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overlay time out: proietta i video spot a tutto schermo -->
|
||||
<!-- Overlay time out: proietta il video spot a tutto schermo -->
|
||||
<div v-if="state.timeout" class="timeout-overlay">
|
||||
<video v-if="spotList.length" ref="spotVideo" class="timeout-video" playsinline></video>
|
||||
<div v-else class="timeout-placeholder">TIME OUT</div>
|
||||
<div class="timeout-countdown">{{ timeoutCountdown }}</div>
|
||||
<video v-if="state.timeoutVideo && !mostraPlaceholder" ref="spotVideo" class="timeout-video"
|
||||
:class="{ 'timeout-video--fading': timeoutFading }" playsinline></video>
|
||||
<div v-else class="timeout-placeholder">
|
||||
TIME OUT<br />{{ state.sp.nomi[state.timeoutTeam] }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -129,16 +133,38 @@ export default {
|
||||
mixins: [createWsMixin('display')],
|
||||
data() {
|
||||
return {
|
||||
spotList: [], // filename dei video spot da riprodurre
|
||||
spotIndex: 0, // video corrente nella sequenza
|
||||
mostraPlaceholder: false, // video terminato prima dei 30s: mostra l'immagine "TIME OUT"
|
||||
spotVideoDuration: null, // durata reale del video in riproduzione, nota dopo il loadedmetadata
|
||||
nowTick: Date.now(),
|
||||
timeoutTicker: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.isMobile()) {
|
||||
try { document.documentElement.requestFullscreen() } catch (e) {}
|
||||
}
|
||||
if (this.state.timeout) this.avviaTimeout()
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.fermaTimeoutTicker()
|
||||
},
|
||||
computed: {
|
||||
timeoutRemaining() {
|
||||
if (!this.state.timeout || !this.state.timeoutStartedAt) return 30
|
||||
const trascorsi = (this.nowTick - this.state.timeoutStartedAt) / 1000
|
||||
return Math.max(0, Math.ceil(30 - trascorsi))
|
||||
},
|
||||
timeoutCountdown() {
|
||||
const r = this.timeoutRemaining
|
||||
const m = String(Math.floor(r / 60)).padStart(2, '0')
|
||||
const s = String(r % 60).padStart(2, '0')
|
||||
return `${m}:${s}`
|
||||
},
|
||||
timeoutFading() {
|
||||
// Video più lungo di 30s: dissolvenza negli ultimi istanti prima dello stop del server.
|
||||
if (!this.spotVideoDuration || this.spotVideoDuration <= 30) return false
|
||||
return this.timeoutRemaining <= 1
|
||||
},
|
||||
stricciaStrip() {
|
||||
const currentSet = this.state.sp.striscia.at(-1)
|
||||
if (!currentSet) return { home: [], guest: [] }
|
||||
@@ -168,32 +194,41 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
avviaTimeoutTicker() {
|
||||
this.fermaTimeoutTicker()
|
||||
this.nowTick = Date.now()
|
||||
this.timeoutTicker = setInterval(() => { this.nowTick = Date.now() }, 200)
|
||||
},
|
||||
fermaTimeoutTicker() {
|
||||
if (this.timeoutTicker) { clearInterval(this.timeoutTicker); this.timeoutTicker = null }
|
||||
},
|
||||
async avviaTimeout() {
|
||||
this.spotIndex = 0
|
||||
try {
|
||||
const res = await fetch('/spot/list')
|
||||
this.spotList = await res.json()
|
||||
} catch {
|
||||
this.spotList = []
|
||||
}
|
||||
if (!this.spotList.length) return
|
||||
this.mostraPlaceholder = !this.state.timeoutVideo
|
||||
this.spotVideoDuration = null
|
||||
this.avviaTimeoutTicker()
|
||||
if (!this.state.timeoutVideo) return
|
||||
await this.$nextTick()
|
||||
this.caricaSpotCorrente()
|
||||
},
|
||||
fermaTimeout() {
|
||||
this.fermaTimeoutTicker()
|
||||
const video = this.$refs.spotVideo
|
||||
if (video) { video.pause(); video.removeAttribute('src'); video.load() }
|
||||
this.spotList = []
|
||||
this.mostraPlaceholder = false
|
||||
this.spotVideoDuration = null
|
||||
},
|
||||
caricaSpotCorrente() {
|
||||
const video = this.$refs.spotVideo
|
||||
if (!video || !this.spotList.length) return
|
||||
video.src = '/spot/' + encodeURIComponent(this.spotList[this.spotIndex])
|
||||
// Avanza al prossimo video alla fine (loop: con un solo video torna su sé stesso)
|
||||
video.onended = () => {
|
||||
this.spotIndex = (this.spotIndex + 1) % this.spotList.length
|
||||
this.caricaSpotCorrente()
|
||||
if (!video || !this.state.timeoutVideo) return
|
||||
video.src = '/spot/' + encodeURIComponent(this.state.timeoutVideo)
|
||||
video.onloadedmetadata = () => {
|
||||
this.spotVideoDuration = video.duration
|
||||
// Riconnessione a metà time out: riallinea il video al punto corretto.
|
||||
const trascorsi = this.state.timeoutStartedAt ? (Date.now() - this.state.timeoutStartedAt) / 1000 : 0
|
||||
if (trascorsi > 0.5 && trascorsi < video.duration) video.currentTime = trascorsi
|
||||
}
|
||||
// Video più corto dei 30s: a fine riproduzione mostra l'immagine "TIME OUT" fino allo stop del server.
|
||||
video.onended = () => { this.mostraPlaceholder = true }
|
||||
this.playSpot()
|
||||
},
|
||||
playSpot() {
|
||||
@@ -320,14 +355,37 @@ export default {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
transition: opacity 0.8s ease-out;
|
||||
}
|
||||
|
||||
.timeout-video--fading {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.timeout-countdown {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
font-size: 4vh;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 6px 18px;
|
||||
border-radius: 12px;
|
||||
z-index: 501;
|
||||
}
|
||||
|
||||
.timeout-placeholder {
|
||||
color: #ff9800;
|
||||
font-size: 20vh;
|
||||
font-size: 14vh;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
animation: timeout-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user