TelegramPWA/src/lib/video-preview/VideoPreview.ts
2023-05-15 10:54:25 +02:00

132 lines
3.3 KiB
TypeScript

import { requestMutation } from '../fasterdom/fasterdom';
import { callApi } from '../../api/gramjs';
import { ApiMediaFormat } from '../../api/types';
import { IS_ANDROID, IS_IOS, ARE_WEBCODECS_SUPPORTED } from '../../util/windowEnvironment';
import launchMediaWorkers, { MAX_WORKERS } from '../../util/launchMediaWorkers';
const IS_MOBILE = IS_ANDROID || IS_IOS;
const PREVIEW_SIZE_RATIO = (IS_ANDROID || IS_IOS) ? 0.3 : 0.25;
const MAX_FRAMES = ARE_WEBCODECS_SUPPORTED && !IS_MOBILE ? 80 : 40;
const PREVIEW_MAX_SIDE = 200;
const connections = launchMediaWorkers();
let videoPreview: VideoPreview | undefined;
export class VideoPreview {
frames: Map<number, ImageBitmap> = new Map();
currentTime = 0;
canvas: HTMLCanvasElement;
constructor(url: string, canvas: HTMLCanvasElement) {
this.canvas = canvas;
connections.forEach(({ connector }, index) => {
void connector.request({
name: 'video-preview:init',
args: [
url,
MAX_FRAMES,
index,
MAX_WORKERS,
this.onFrame.bind(this),
],
});
});
}
private onFrame(time: number, frame: ImageBitmap) {
this.frames.set(time, frame);
if (time === this.currentTime) {
this.render(time);
}
}
private clearCache() {
this.frames.forEach((frame) => {
frame.close();
});
this.frames.clear();
}
render(time: number) {
this.currentTime = time;
const frame = this.frames.get(time);
if (!frame) return false;
requestMutation(() => {
this.canvas.width = frame.width;
this.canvas.height = frame.height;
const ctx = this.canvas.getContext('2d')!;
ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
});
return true;
}
destroy() {
this.clearCache();
connections.forEach(({ connector }) => {
void connector.request({
name: 'video-preview:destroy',
args: [],
});
});
}
}
export function getPreviewDimensions(width: number, height: number) {
width = Math.round(width * PREVIEW_SIZE_RATIO);
height = Math.round(height * PREVIEW_SIZE_RATIO);
const ratio = width / height;
if (width > PREVIEW_MAX_SIDE) {
width = PREVIEW_MAX_SIDE;
height = Math.round(width / ratio);
}
if (height > PREVIEW_MAX_SIDE) {
height = PREVIEW_MAX_SIDE;
width = Math.round(height * ratio);
}
return { width, height };
}
connections.forEach(({ worker }) => {
worker.addEventListener('message', async (e) => {
const { type, messageId, params } = e.data as {
type: string;
messageId: string;
params: { url: string; start: number; end: number };
};
if (type !== 'requestPart') {
return;
}
const result = await callApi('downloadMedia', { mediaFormat: ApiMediaFormat.Progressive, ...params });
if (!result) {
return;
}
const { arrayBuffer } = result;
worker.postMessage({
type: 'partResponse',
messageId,
result: arrayBuffer,
}, [arrayBuffer!]);
});
});
export function createVideoPreviews(url: string, canvas: HTMLCanvasElement) {
if (videoPreview) {
videoPreview.destroy();
}
videoPreview = new VideoPreview(url, canvas);
return () => videoPreview?.destroy();
}
export function renderVideoPreview(time: number) {
if (!videoPreview) return false;
return videoPreview.render(time);
}