2021-06-25 16:08:16 +00:00
|
|
|
// Copyright 2018-2021 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
import loadImage from 'blueimp-load-image';
|
|
|
|
import { blobToArrayBuffer } from 'blob-util';
|
|
|
|
import { toLogFormat } from './errors';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { MIMEType } from './MIME';
|
|
|
|
import { IMAGE_PNG } from './MIME';
|
|
|
|
import type { LoggerType } from './Logging';
|
2021-09-24 00:49:05 +00:00
|
|
|
import { arrayBufferToObjectURL } from '../util/arrayBufferToObjectURL';
|
|
|
|
import { strictAssert } from '../util/assert';
|
|
|
|
import { canvasToBlob } from '../util/canvasToBlob';
|
|
|
|
|
|
|
|
export { blobToArrayBuffer };
|
|
|
|
|
|
|
|
export type GetImageDimensionsOptionsType = Readonly<{
|
|
|
|
objectUrl: string;
|
|
|
|
logger: Pick<LoggerType, 'error'>;
|
|
|
|
}>;
|
|
|
|
|
|
|
|
export function getImageDimensions({
|
|
|
|
objectUrl,
|
|
|
|
logger,
|
|
|
|
}: GetImageDimensionsOptionsType): Promise<{ width: number; height: number }> {
|
|
|
|
return new Promise((resolve, reject) => {
|
2018-07-09 21:29:13 +00:00
|
|
|
const image = document.createElement('img');
|
|
|
|
|
|
|
|
image.addEventListener('load', () => {
|
|
|
|
resolve({
|
|
|
|
height: image.naturalHeight,
|
|
|
|
width: image.naturalWidth,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
image.addEventListener('error', error => {
|
2018-07-21 19:00:08 +00:00
|
|
|
logger.error('getImageDimensions error', toLogFormat(error));
|
2018-07-09 21:29:13 +00:00
|
|
|
reject(error);
|
|
|
|
});
|
|
|
|
|
|
|
|
image.src = objectUrl;
|
|
|
|
});
|
2021-09-24 00:49:05 +00:00
|
|
|
}
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export type MakeImageThumbnailOptionsType = Readonly<{
|
|
|
|
size: number;
|
|
|
|
objectUrl: string;
|
|
|
|
contentType?: MIMEType;
|
|
|
|
logger: Pick<LoggerType, 'error'>;
|
|
|
|
}>;
|
|
|
|
|
|
|
|
export function makeImageThumbnail({
|
2018-07-21 19:00:08 +00:00
|
|
|
size,
|
|
|
|
objectUrl,
|
2021-09-24 00:49:05 +00:00
|
|
|
contentType = IMAGE_PNG,
|
2018-07-21 19:00:08 +00:00
|
|
|
logger,
|
2021-09-24 00:49:05 +00:00
|
|
|
}: MakeImageThumbnailOptionsType): Promise<Blob> {
|
|
|
|
return new Promise((resolve, reject) => {
|
2018-07-09 21:29:13 +00:00
|
|
|
const image = document.createElement('img');
|
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
image.addEventListener('load', async () => {
|
2018-07-09 21:29:13 +00:00
|
|
|
// using components/blueimp-load-image
|
|
|
|
|
|
|
|
// first, make the correct size
|
|
|
|
let canvas = loadImage.scale(image, {
|
|
|
|
canvas: true,
|
|
|
|
cover: true,
|
|
|
|
maxWidth: size,
|
|
|
|
maxHeight: size,
|
|
|
|
minWidth: size,
|
|
|
|
minHeight: size,
|
|
|
|
});
|
|
|
|
|
|
|
|
// then crop
|
|
|
|
canvas = loadImage.scale(canvas, {
|
|
|
|
canvas: true,
|
|
|
|
crop: true,
|
|
|
|
maxWidth: size,
|
|
|
|
maxHeight: size,
|
|
|
|
minWidth: size,
|
|
|
|
minHeight: size,
|
|
|
|
});
|
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
strictAssert(
|
|
|
|
canvas instanceof HTMLCanvasElement,
|
|
|
|
'loadImage must produce canvas'
|
|
|
|
);
|
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
try {
|
|
|
|
const blob = await canvasToBlob(canvas, contentType);
|
|
|
|
resolve(blob);
|
|
|
|
} catch (err) {
|
|
|
|
reject(err);
|
|
|
|
}
|
2018-07-09 21:29:13 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
image.addEventListener('error', error => {
|
2018-07-21 19:00:08 +00:00
|
|
|
logger.error('makeImageThumbnail error', toLogFormat(error));
|
2018-07-09 21:29:13 +00:00
|
|
|
reject(error);
|
|
|
|
});
|
|
|
|
|
|
|
|
image.src = objectUrl;
|
|
|
|
});
|
2021-09-24 00:49:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export type MakeVideoScreenshotOptionsType = Readonly<{
|
|
|
|
objectUrl: string;
|
|
|
|
contentType?: MIMEType;
|
|
|
|
logger: Pick<LoggerType, 'error'>;
|
|
|
|
}>;
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function makeVideoScreenshot({
|
2018-07-21 19:00:08 +00:00
|
|
|
objectUrl,
|
2021-09-24 00:49:05 +00:00
|
|
|
contentType = IMAGE_PNG,
|
2018-07-21 19:00:08 +00:00
|
|
|
logger,
|
2021-09-24 00:49:05 +00:00
|
|
|
}: MakeVideoScreenshotOptionsType): Promise<Blob> {
|
|
|
|
return new Promise((resolve, reject) => {
|
2018-07-09 21:29:13 +00:00
|
|
|
const video = document.createElement('video');
|
|
|
|
|
2019-09-24 20:14:53 +00:00
|
|
|
function seek() {
|
|
|
|
video.currentTime = 1.0;
|
|
|
|
}
|
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
async function capture() {
|
2018-07-09 21:29:13 +00:00
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
canvas.width = video.videoWidth;
|
|
|
|
canvas.height = video.videoHeight;
|
2021-09-24 00:49:05 +00:00
|
|
|
const context = canvas.getContext('2d');
|
|
|
|
strictAssert(context, 'Failed to get canvas context');
|
|
|
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2022-07-21 16:49:26 +00:00
|
|
|
video.removeEventListener('loadeddata', seek);
|
2019-09-24 20:14:53 +00:00
|
|
|
video.removeEventListener('seeked', capture);
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
try {
|
|
|
|
const image = canvasToBlob(canvas, contentType);
|
|
|
|
resolve(image);
|
|
|
|
} catch (err) {
|
|
|
|
reject(err);
|
|
|
|
}
|
2018-07-09 21:29:13 +00:00
|
|
|
}
|
|
|
|
|
2019-09-24 20:14:53 +00:00
|
|
|
video.addEventListener('loadeddata', seek);
|
|
|
|
video.addEventListener('seeked', capture);
|
|
|
|
|
2018-07-09 21:29:13 +00:00
|
|
|
video.addEventListener('error', error => {
|
2018-07-21 19:28:37 +00:00
|
|
|
logger.error('makeVideoScreenshot error', toLogFormat(error));
|
2018-07-09 21:29:13 +00:00
|
|
|
reject(error);
|
|
|
|
});
|
|
|
|
|
|
|
|
video.src = objectUrl;
|
|
|
|
});
|
2021-09-24 00:49:05 +00:00
|
|
|
}
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export type MakeVideoThumbnailOptionsType = Readonly<{
|
|
|
|
size: number;
|
|
|
|
videoObjectUrl: string;
|
|
|
|
logger: Pick<LoggerType, 'error'>;
|
|
|
|
contentType: MIMEType;
|
|
|
|
}>;
|
|
|
|
|
|
|
|
export async function makeVideoThumbnail({
|
2018-07-21 19:28:37 +00:00
|
|
|
size,
|
|
|
|
videoObjectUrl,
|
|
|
|
logger,
|
|
|
|
contentType,
|
2021-09-24 00:49:05 +00:00
|
|
|
}: MakeVideoThumbnailOptionsType): Promise<Blob> {
|
|
|
|
let screenshotObjectUrl: string | undefined;
|
2018-07-09 21:29:13 +00:00
|
|
|
try {
|
2021-09-24 00:49:05 +00:00
|
|
|
const blob = await makeVideoScreenshot({
|
2018-07-21 19:00:08 +00:00
|
|
|
objectUrl: videoObjectUrl,
|
2018-07-21 19:28:37 +00:00
|
|
|
contentType,
|
2018-07-21 19:00:08 +00:00
|
|
|
logger,
|
|
|
|
});
|
2018-07-09 21:29:13 +00:00
|
|
|
const data = await blobToArrayBuffer(blob);
|
|
|
|
screenshotObjectUrl = arrayBufferToObjectURL({
|
|
|
|
data,
|
2018-07-21 19:28:37 +00:00
|
|
|
type: contentType,
|
2018-07-09 21:29:13 +00:00
|
|
|
});
|
|
|
|
|
2018-07-21 19:28:37 +00:00
|
|
|
// We need to wait for this, otherwise the finally below will run first
|
2021-09-24 00:49:05 +00:00
|
|
|
const resultBlob = await makeImageThumbnail({
|
2018-07-21 19:00:08 +00:00
|
|
|
size,
|
|
|
|
objectUrl: screenshotObjectUrl,
|
2018-07-21 19:28:37 +00:00
|
|
|
contentType,
|
2018-07-21 19:00:08 +00:00
|
|
|
logger,
|
|
|
|
});
|
2018-07-21 19:28:37 +00:00
|
|
|
|
|
|
|
return resultBlob;
|
2018-07-09 21:29:13 +00:00
|
|
|
} finally {
|
2021-09-24 00:49:05 +00:00
|
|
|
if (screenshotObjectUrl !== undefined) {
|
|
|
|
revokeObjectUrl(screenshotObjectUrl);
|
|
|
|
}
|
2018-07-09 21:29:13 +00:00
|
|
|
}
|
2021-09-24 00:49:05 +00:00
|
|
|
}
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function makeObjectUrl(
|
|
|
|
data: Uint8Array | ArrayBuffer,
|
|
|
|
contentType: MIMEType
|
|
|
|
): string {
|
2018-07-09 21:29:13 +00:00
|
|
|
const blob = new Blob([data], {
|
|
|
|
type: contentType,
|
|
|
|
});
|
|
|
|
|
|
|
|
return URL.createObjectURL(blob);
|
2021-09-24 00:49:05 +00:00
|
|
|
}
|
2018-07-09 21:29:13 +00:00
|
|
|
|
2021-09-24 00:49:05 +00:00
|
|
|
export function revokeObjectUrl(objectUrl: string): void {
|
2018-07-09 21:29:13 +00:00
|
|
|
URL.revokeObjectURL(objectUrl);
|
2021-09-24 00:49:05 +00:00
|
|
|
}
|