updates 33.1 23

This commit is contained in:
2026-08-10 01:35:13 -03:00
parent dfb1991ae3
commit 33f0559268
2 changed files with 144 additions and 0 deletions

View File

@@ -0,0 +1,143 @@
<script setup lang="ts">
import { ref, watch, onBeforeUnmount } from 'vue'
/**
* Agnostic seekable media player.
*
* Two-way bindable playback position and play state:
* <VideoPlayer :src="url" v-model:currentTime="t" v-model:playing="p" />
*
* External `currentTime` changes seek the element (guarded against the
* feedback loop with the element's own `timeupdate` events).
*/
const props = withDefaults(defineProps<{
/** Media source URL */
src: string
/** Playback position in seconds (v-model:currentTime) */
currentTime?: number
/** Whether the media is playing (v-model:playing) */
playing?: boolean
}>(), {
currentTime: 0,
playing: false,
})
const emit = defineEmits<{
'update:currentTime': [value: number]
'update:playing': [value: boolean]
/** Media duration once metadata has loaded */
duration: [value: number]
/** Playback reached the end */
ended: []
}>()
const video = ref<HTMLVideoElement | null>(null)
/** True while we are applying an external seek, so the resulting
* `timeupdate` does not echo back out as an update. */
let seekingFromProp = false
/** Tolerance (s) below which we treat positions as already in sync. */
const EPSILON = 0.25
watch(() => props.currentTime, (t) => {
const el = video.value
if (!el) return
if (Math.abs(el.currentTime - t) < EPSILON) return
seekingFromProp = true
el.currentTime = t
})
watch(() => props.playing, (shouldPlay) => {
const el = video.value
if (!el) return
if (shouldPlay && el.paused) {
void el.play().catch(() => { /* autoplay may be blocked; ignore */ })
} else if (!shouldPlay && !el.paused) {
el.pause()
}
})
function onTimeUpdate() {
const el = video.value
if (!el) return
if (seekingFromProp) {
seekingFromProp = false
return
}
emit('update:currentTime', el.currentTime)
}
function onSeeked() {
// Clear the guard once the browser settles the requested seek.
seekingFromProp = false
const el = video.value
if (el) emit('update:currentTime', el.currentTime)
}
function onLoadedMetadata() {
const el = video.value
if (!el) return
emit('duration', el.duration)
// Honour an initial position requested before metadata was ready.
if (props.currentTime && Math.abs(el.currentTime - props.currentTime) >= EPSILON) {
seekingFromProp = true
el.currentTime = props.currentTime
}
}
function onPlay() { emit('update:playing', true) }
function onPause() { emit('update:playing', false) }
function onEnded() {
emit('update:playing', false)
emit('ended')
}
onBeforeUnmount(() => {
// Release the media resource promptly.
const el = video.value
if (el) {
el.pause()
el.removeAttribute('src')
el.load()
}
})
</script>
<template>
<div class="video-player">
<video
ref="video"
class="video-el"
:src="src"
controls
preload="metadata"
@timeupdate="onTimeUpdate"
@seeked="onSeeked"
@loadedmetadata="onLoadedMetadata"
@play="onPlay"
@pause="onPause"
@ended="onEnded"
/>
</div>
</template>
<style scoped>
.video-player {
display: flex;
width: 100%;
height: 100%;
background: var(--surface-0);
align-items: center;
justify-content: center;
overflow: hidden;
}
.video-el {
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
</style>

View File

@@ -14,6 +14,7 @@ export { default as ResizeHandle } from './components/ResizeHandle.vue'
export { default as SplitPane } from './components/SplitPane.vue' export { default as SplitPane } from './components/SplitPane.vue'
export { default as ParameterEditor } from './components/ParameterEditor.vue' export { default as ParameterEditor } from './components/ParameterEditor.vue'
export type { ConfigField } from './components/ParameterEditor.vue' export type { ConfigField } from './components/ParameterEditor.vue'
export { default as VideoPlayer } from './components/VideoPlayer.vue'
// Renderers // Renderers
export { default as LogRenderer } from './renderers/LogRenderer.vue' export { default as LogRenderer } from './renderers/LogRenderer.vue'