import type { Asset, VideoAsset } from '../../core/model/Asset'; import { Button, downloadFile } from '../../ui'; import { videoFileUrl } from '../../viewport/VideoLibrary'; import { AssetTabEditor } from '../AssetTabEditor'; import { TOOLBAR_ICON_SVG } from '../panels/toolbarIcons'; import type { EditorState } from '../state'; import { VideoCanvas } from './VideoCanvas'; import { formatDuration } from './videoInfo'; const INFO_WIDTH = 220; /** * The Video tab (#402): the video with its clips, the stretches of it a sprite can wear instead of * the whole video. The header plays the picked clip, else the whole video (Space does too). */ export class VideoEditor extends AssetTabEditor { /** Public for the check scripts. */ readonly playButton: Button; /** Saves the MP4 the server made (#405): a recording of the game view is had this way. */ readonly downloadButton: Button; constructor(state: EditorState) { super(state, { infoWidth: INFO_WIDTH }); this.playButton = new Button('Play', { icon: TOOLBAR_ICON_SVG.play, minWidth: 72, tooltip: 'Play the picked clip, else the whole video (Space)', onClick: () => this.togglePlay(), }); this.downloadButton = new Button('Download', { tooltip: 'Save the video as an MP4 file', onClick: () => this.download(), }); const canvas = new VideoCanvas(state); canvas.onTogglePlay = () => this.togglePlay(); canvas.onStopped = () => this.syncPlayButton(); this.mount(canvas, [this.playButton, this.downloadButton]); state.on('load', () => this.canvas.stop()); state.on('selection', () => this.refresh()); } override selectedId(): string { return this.state.selectedVideoId ?? ''; } override isMine(asset: Asset): asset is VideoAsset { return asset.type === 'video'; } get isPlaying(): boolean { return this.canvas.isPlaying; } togglePlay(): void { if (this.canvas.isPlaying) this.canvas.stop(); else this.canvas.play(); this.syncPlayButton(); } private download(): void { const asset = this.asset(); if (asset?.file) downloadFile(videoFileUrl(asset.file), `${asset.name}.mp4`); } /** Only called while the tab is visible. */ tick(): void { this.canvas.tick(); } protected override closed(assetId: string): void { if (this.canvas.currentAssetId === assetId) this.canvas.stop(); } protected override infoText(asset: VideoAsset): string { if (!asset.file) return 'Not uploaded'; const span = this.canvas.shownSpan(); const whole = formatDuration(asset.duration); const length = span && span.end - span.start < asset.duration ? `${formatDuration(span.end - span.start)} of ${whole}` : whole; return `${asset.width} × ${asset.height} · ${length} · ${Number(asset.fps.toFixed(2))} fps`; } protected override syncExtras(asset: VideoAsset | null): void { this.fitButton.setEnabled(Boolean(asset?.file)); this.downloadButton.setEnabled(Boolean(asset?.file)); this.syncPlayButton(); } private syncPlayButton(): void { const playing = this.canvas.isPlaying; this.playButton.setIcon(playing ? TOOLBAR_ICON_SVG.stop : TOOLBAR_ICON_SVG.play); this.playButton.setText(playing ? 'Stop' : 'Play'); this.playButton.setEnabled(Boolean(this.asset()?.file)); } }