upend/webui/src/components/display/blobs/VideoViewer.svelte

221 lines
4.8 KiB
Svelte

<script lang="ts">
import { throttle } from "lodash";
import Spinner from "../../utils/Spinner.svelte";
import Icon from "../../utils/Icon.svelte";
import { API_URL } from "../../../lib/api";
export let address: string;
export let detail: boolean;
enum State {
LOADING = "loading",
PREVIEW = "preview",
PREVIEWING = "previewing",
PLAYING = "playing",
ERRORED = "errored",
}
let state = State.LOADING;
let videoEl: HTMLVideoElement;
let currentTime: number;
let timeCodeWidth: number;
let timeCodeLeft: string;
let timeCodeSize: string;
const seek = throttle((progress: number) => {
if (state === State.PREVIEWING && videoEl.duration) {
currentTime = videoEl.duration * progress;
if (timeCodeWidth) {
let timeCodeLeftPx = Math.min(
Math.max(videoEl.clientWidth * progress, timeCodeWidth / 2),
videoEl.clientWidth - timeCodeWidth / 2
);
timeCodeLeft = `${timeCodeLeftPx}px`;
timeCodeSize = `${videoEl.clientHeight / 9}px`;
}
}
}, 100);
function updatePreviewPosition(ev: MouseEvent) {
if (state === State.PREVIEW || state === State.PREVIEWING) {
state = State.PREVIEWING;
const bcr = videoEl.getBoundingClientRect();
const progress = (ev.clientX - bcr.x) / bcr.width;
seek(progress);
}
}
function resetPreview() {
if (state === State.PREVIEWING) {
state = State.PREVIEW;
videoEl.load();
}
}
function startPlaying() {
if (detail) {
state = State.PLAYING;
videoEl.play();
}
}
</script>
<div class="video-viewer {state}">
<div class="player" style="--icon-size: {detail ? 100 : 32}px">
{#if state === State.LOADING}
<Spinner />
{/if}
{#if state === State.LOADING || (!detail && state === State.PREVIEW)}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<img
class="thumb"
src="{API_URL}/thumb/{address}?mime=video"
alt="Preview for {address}"
on:load={() => (state = State.PREVIEW)}
on:mouseover={() => (state = State.PREVIEWING)}
on:error={() => (state = State.ERRORED)}
/>
{:else}
<!-- svelte-ignore a11y-media-has-caption -->
<video
preload={detail ? "auto" : "metadata"}
src="{API_URL}/raw/{address}"
poster="{API_URL}/thumb/{address}?mime=video"
on:mousemove={updatePreviewPosition}
on:mouseleave={resetPreview}
on:click|preventDefault={startPlaying}
controls={state === State.PLAYING}
bind:this={videoEl}
bind:currentTime
/>
{/if}
<div class="play-icon">
<div class="icon">
<Icon plain name="play" />
</div>
</div>
<div
class="timecode"
bind:clientWidth={timeCodeWidth}
style:left={timeCodeLeft}
style:font-size={timeCodeSize}
>
{#if videoEl?.duration && currentTime}
{#if videoEl.duration > 3600}{String(
Math.floor(currentTime / 3600)
).padStart(2, "0")}:{/if}{String(
Math.floor((currentTime % 3600) / 60)
).padStart(2, "0")}:{String(
Math.floor((currentTime % 3600) % 60)
).padStart(2, "0")}
{/if}
</div>
</div>
</div>
<style lang="scss">
.video-viewer {
min-width: 0;
min-height: 0;
&,
.player {
display: flex;
align-items: center;
min-height: 0;
flex-direction: column;
width: 100%;
height: 100%;
}
img,
video {
width: 100%;
max-height: 100%;
min-height: 0;
// background: rgba(128, 128, 128, 128);
transition: filter 0.2s;
}
.player {
position: relative;
}
.play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
$size: var(--icon-size);
width: $size;
height: $size;
color: white;
font-size: $size;
line-height: 1;
border-radius: calc($size / 4);
border: 0.07em solid white;
.icon {
margin-left: 0.04em;
}
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.timecode {
display: none;
pointer-events: none;
position: absolute;
top: 50%;
left: var(--left);
transform: translate(-50%, -50%);
font-feature-settings: "tnum";
font-weight: bold;
color: white;
opacity: 0.66;
}
&.loading {
.player > * {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
&.standby,
&.preview {
img,
video {
filter: brightness(0.75);
}
.play-icon {
opacity: 0.8;
}
}
&.previewing {
.timecode {
display: block;
}
video {
cursor: pointer;
}
}
}
</style>