import { AbstractUploader, CancelableOperation, CommonOptions, DEFAULT_CHUNK_SIZE, MAX_CHUNK_SIZE, MIN_CHUNK_SIZE, VideoUploadResponse, WithAccessToken, WithApiKey, WithUploadToken, } from "./abstract-uploader"; interface UploadOptions { file: File; chunkSize?: number; maxVideoDuration?: number; } export interface VideoUploaderOptionsWithUploadToken extends CommonOptions, UploadOptions, WithUploadToken { } export interface VideoUploaderOptionsWithAccessToken extends CommonOptions, UploadOptions, WithAccessToken { } export interface VideoUploaderOptionsWithApiKey extends CommonOptions, UploadOptions, WithApiKey { } export interface UploadProgressEvent { uploadedBytes: number; totalBytes: number; chunksCount: number; chunksBytes: number; currentChunk: number; currentChunkUploadedBytes: number; } export class VideoUploader extends AbstractUploader { private file: File; private chunkSize: number; private chunksCount: number; private fileSize: number; private fileName: string; private maxVideoDuration?: number; private currentChunkCancel?: () => void; private canceled = false; constructor( options: | VideoUploaderOptionsWithAccessToken | VideoUploaderOptionsWithUploadToken | VideoUploaderOptionsWithApiKey ) { super(options); if (!options.file) { throw new Error("'file' is missing"); } if ( options.chunkSize && (options.chunkSize < MIN_CHUNK_SIZE || options.chunkSize > MAX_CHUNK_SIZE) ) { throw new Error( `Invalid chunk size. Minimal allowed value: ${MIN_CHUNK_SIZE / 1024 / 1024 }MB, maximum allowed value: ${MAX_CHUNK_SIZE / 1024 / 1024}MB.` ); } this.chunkSize = options.chunkSize || DEFAULT_CHUNK_SIZE; this.file = options.file; this.fileSize = this.file.size; this.fileName = options.videoName || this.file.name; this.chunksCount = Math.ceil(this.fileSize / this.chunkSize); this.maxVideoDuration = options.maxVideoDuration; } public async upload(): Promise { if (this.maxVideoDuration !== undefined && !document) { throw Error( "document is undefined. Impossible to use the maxVideoDuration option. Remove it and try again." ); } if (this.maxVideoDuration !== undefined && (await this.isVideoTooLong())) { throw Error(`The submitted video is too long.`); } let res: VideoUploadResponse; for (let i = 0; i < this.chunksCount && !this.canceled; i++) { const cancelableOperation = this.uploadCurrentChunk(i); this.currentChunkCancel = cancelableOperation.cancel; res = await cancelableOperation.result; this.videoId = res.videoId; } if (this.onPlayableCallbacks.length > 0) { this.waitForPlayable(res!); } return res!; } public cancel(): void { this.canceled = true; if (this.currentChunkCancel) { this.currentChunkCancel(); } } private async isVideoTooLong(): Promise { return new Promise((resolve) => { const video = document.createElement("video"); video.preload = "metadata"; video.onloadedmetadata = () => { window.URL.revokeObjectURL(video.src); resolve(video.duration > this.maxVideoDuration!); }; video.src = URL.createObjectURL(this.file); }); } private uploadCurrentChunk( chunkNumber: number ): CancelableOperation { const firstByte = chunkNumber * this.chunkSize; const computedLastByte = (chunkNumber + 1) * this.chunkSize; const lastByte = computedLastByte > this.fileSize ? this.fileSize : computedLastByte; const chunksCount = Math.ceil(this.fileSize / this.chunkSize); const progressEventToUploadProgressEvent = ( event: ProgressEvent ): UploadProgressEvent => { return { uploadedBytes: event.loaded + firstByte, totalBytes: this.fileSize, chunksCount: this.chunksCount, chunksBytes: this.chunkSize, currentChunk: chunkNumber + 1, currentChunkUploadedBytes: event.loaded, }; }; return this.xhrWithRetrier({ onProgress: (event) => this.onProgressCallbacks.forEach((cb) => cb(progressEventToUploadProgressEvent(event)) ), body: this.createFormData(this.file, this.fileName, firstByte, lastByte), parts: { currentPart: chunkNumber + 1, totalParts: chunksCount, }, }); } }