import type { UploadedVideo } from '../core/store/VideoStore'; import type { MadeRecording } from '../viewport/GameRecorder'; import { adoptVideo, type VideoImportDeps } from './videoImport'; /** How often the server is asked whether a recording is converted, as the video picker does. */ const POLL_MS = 1000; type UploadState = 'uploading' | 'converting' | 'added' | 'elsewhere' | 'failed'; /** What became of a recording on its way into the project (test hook). */ export interface RecordingUpload { name: string; state: UploadState; /** The name the server gave it, once it is up. */ file: string; assetId: string; bytes: number; error: string; } /** * Brings a recording of the game view into the project (#405): up to the server, which converts it * like any video, then an asset named after the recording once it is ready. Only while the editor * is not playing: a run plays a copy of the project that is thrown away, and so would the asset be. */ export class RecordingUploads { private readonly uploads: RecordingUpload[] = []; constructor(private readonly deps: VideoImportDeps) {} add(recording: MadeRecording): void { const upload: RecordingUpload = { name: recording.name, state: 'uploading', file: '', assetId: '', bytes: recording.blob.size, error: '', }; this.uploads.push(upload); void this.bring(recording, upload).catch((error: unknown) => { upload.state = 'failed'; upload.error = error instanceof Error ? error.message : String(error); this.deps.state.setNotice(`Could not save the recording "${recording.name}": ${upload.error}`, 'error'); }); } info(): RecordingUpload[] { return this.uploads.map((upload) => ({ ...upload })); } private async bring(recording: MadeRecording, upload: RecordingUpload): Promise { const { state, videos } = this.deps; const projectId = state.project.id; state.setNotice(`Uploading the recording "${recording.name}"…`); let video: UploadedVideo = await videos.upload( recording.blob, 'application/octet-stream', `${recording.name}.mkv`, ); upload.file = video.file; upload.state = 'converting'; while (video.status !== 'ready') { if (video.status === 'failed') throw new Error(video.error ?? 'the server could not convert it'); await wait(POLL_MS); video = (await videos.list()).find((v) => v.file === upload.file) ?? video; } while (state.mode !== 'edit') await wait(POLL_MS); if (state.project.id !== projectId) { upload.state = 'elsewhere'; state.setNotice(`The recording "${recording.name}" is ready – the video picker has it.`); return; } const outcome = adoptVideo(video, recording.name, this.deps); if (!outcome.assetId) throw new Error(outcome.error ?? 'the project did not take it'); upload.state = 'added'; upload.assetId = outcome.assetId; state.setNotice(`Added the recording "${recording.name}" to the project.`); } } function wait(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); }