Uint8Array migration
This commit is contained in:
parent
daf75190b8
commit
4ef0bf96cc
137 changed files with 2202 additions and 3170 deletions
|
@ -6,12 +6,12 @@ import moment from 'moment';
|
|||
import {
|
||||
isNumber,
|
||||
padStart,
|
||||
isArrayBuffer,
|
||||
isTypedArray,
|
||||
isFunction,
|
||||
isUndefined,
|
||||
omit,
|
||||
} from 'lodash';
|
||||
import { arrayBufferToBlob, blobToArrayBuffer } from 'blob-util';
|
||||
import { blobToArrayBuffer } from 'blob-util';
|
||||
|
||||
import { LoggerType } from './Logging';
|
||||
import * as MIME from './MIME';
|
||||
|
@ -63,14 +63,14 @@ export type AttachmentType = {
|
|||
cdnNumber?: number;
|
||||
cdnId?: string;
|
||||
cdnKey?: string;
|
||||
data?: ArrayBuffer;
|
||||
data?: Uint8Array;
|
||||
|
||||
/** Legacy field. Used only for downloading old attachments */
|
||||
id?: number;
|
||||
};
|
||||
|
||||
export type DownloadedAttachmentType = AttachmentType & {
|
||||
data: ArrayBuffer;
|
||||
data: Uint8Array;
|
||||
};
|
||||
|
||||
export type BaseAttachmentDraftType = {
|
||||
|
@ -85,9 +85,9 @@ export type BaseAttachmentDraftType = {
|
|||
|
||||
export type InMemoryAttachmentDraftType =
|
||||
| ({
|
||||
data?: ArrayBuffer;
|
||||
data?: Uint8Array;
|
||||
pending: false;
|
||||
screenshotData?: ArrayBuffer;
|
||||
screenshotData?: Uint8Array;
|
||||
} & BaseAttachmentDraftType)
|
||||
| {
|
||||
contentType: MIME.MIMEType;
|
||||
|
@ -138,7 +138,7 @@ export async function migrateDataToFileSystem(
|
|||
{
|
||||
writeNewAttachmentData,
|
||||
}: {
|
||||
writeNewAttachmentData: (data: ArrayBuffer) => Promise<string>;
|
||||
writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
|
||||
}
|
||||
): Promise<AttachmentType> {
|
||||
if (!isFunction(writeNewAttachmentData)) {
|
||||
|
@ -152,9 +152,9 @@ export async function migrateDataToFileSystem(
|
|||
return attachment;
|
||||
}
|
||||
|
||||
if (!isArrayBuffer(data)) {
|
||||
if (!isTypedArray(data)) {
|
||||
throw new TypeError(
|
||||
'Expected `attachment.data` to be an array buffer;' +
|
||||
'Expected `attachment.data` to be a typed array;' +
|
||||
` got: ${typeof attachment.data}`
|
||||
);
|
||||
}
|
||||
|
@ -169,19 +169,19 @@ export async function migrateDataToFileSystem(
|
|||
// {
|
||||
// id: string
|
||||
// contentType: MIMEType
|
||||
// data: ArrayBuffer
|
||||
// digest: ArrayBuffer
|
||||
// data: Uint8Array
|
||||
// digest: Uint8Array
|
||||
// fileName?: string
|
||||
// flags: null
|
||||
// key: ArrayBuffer
|
||||
// key: Uint8Array
|
||||
// size: integer
|
||||
// thumbnail: ArrayBuffer
|
||||
// thumbnail: Uint8Array
|
||||
// }
|
||||
|
||||
// // Outgoing message attachment fields
|
||||
// {
|
||||
// contentType: MIMEType
|
||||
// data: ArrayBuffer
|
||||
// data: Uint8Array
|
||||
// fileName: string
|
||||
// size: integer
|
||||
// }
|
||||
|
@ -232,10 +232,9 @@ export async function autoOrientJPEG(
|
|||
return attachment;
|
||||
}
|
||||
|
||||
const dataBlob = await arrayBufferToBlob(
|
||||
attachment.data,
|
||||
attachment.contentType
|
||||
);
|
||||
const dataBlob = new Blob([attachment.data], {
|
||||
type: attachment.contentType,
|
||||
});
|
||||
const { blob: xcodedDataBlob } = await scaleImageToLevel(
|
||||
dataBlob,
|
||||
attachment.contentType,
|
||||
|
@ -243,7 +242,7 @@ export async function autoOrientJPEG(
|
|||
);
|
||||
const xcodedDataArrayBuffer = await blobToArrayBuffer(xcodedDataBlob);
|
||||
|
||||
// IMPORTANT: We overwrite the existing `data` `ArrayBuffer` losing the original
|
||||
// IMPORTANT: We overwrite the existing `data` `Uint8Array` losing the original
|
||||
// image data. Ideally, we’d preserve the original image data for users who want to
|
||||
// retain it but due to reports of data loss, we don’t want to overburden IndexedDB
|
||||
// by potentially doubling stored image data.
|
||||
|
@ -251,7 +250,7 @@ export async function autoOrientJPEG(
|
|||
const xcodedAttachment = {
|
||||
// `digest` is no longer valid for auto-oriented image data, so we discard it:
|
||||
...omit(attachment, 'digest'),
|
||||
data: xcodedDataArrayBuffer,
|
||||
data: new Uint8Array(xcodedDataArrayBuffer),
|
||||
size: xcodedDataArrayBuffer.byteLength,
|
||||
};
|
||||
|
||||
|
@ -335,14 +334,11 @@ export function removeSchemaVersion({
|
|||
}
|
||||
|
||||
export function hasData(attachment: AttachmentType): boolean {
|
||||
return (
|
||||
attachment.data instanceof ArrayBuffer ||
|
||||
ArrayBuffer.isView(attachment.data)
|
||||
);
|
||||
return attachment.data instanceof Uint8Array;
|
||||
}
|
||||
|
||||
export function loadData(
|
||||
readAttachmentData: (path: string) => Promise<ArrayBuffer>
|
||||
readAttachmentData: (path: string) => Promise<Uint8Array>
|
||||
): (attachment?: AttachmentType) => Promise<AttachmentType> {
|
||||
if (!is.function_(readAttachmentData)) {
|
||||
throw new TypeError("'readAttachmentData' must be a function");
|
||||
|
@ -400,9 +396,12 @@ const THUMBNAIL_CONTENT_TYPE = MIME.IMAGE_PNG;
|
|||
export async function captureDimensionsAndScreenshot(
|
||||
attachment: AttachmentType,
|
||||
params: {
|
||||
writeNewAttachmentData: (data: ArrayBuffer) => Promise<string>;
|
||||
writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
|
||||
getAbsoluteAttachmentPath: (path: string) => Promise<string>;
|
||||
makeObjectUrl: (data: ArrayBuffer, contentType: MIME.MIMEType) => string;
|
||||
makeObjectUrl: (
|
||||
data: Uint8Array | ArrayBuffer,
|
||||
contentType: MIME.MIMEType
|
||||
) => string;
|
||||
revokeObjectUrl: (path: string) => void;
|
||||
getImageDimensions: (params: {
|
||||
objectUrl: string;
|
||||
|
@ -464,7 +463,9 @@ export async function captureDimensionsAndScreenshot(
|
|||
})
|
||||
);
|
||||
|
||||
const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer);
|
||||
const thumbnailPath = await writeNewAttachmentData(
|
||||
new Uint8Array(thumbnailBuffer)
|
||||
);
|
||||
return {
|
||||
...attachment,
|
||||
width,
|
||||
|
@ -503,7 +504,9 @@ export async function captureDimensionsAndScreenshot(
|
|||
objectUrl: screenshotObjectUrl,
|
||||
logger,
|
||||
});
|
||||
const screenshotPath = await writeNewAttachmentData(screenshotBuffer);
|
||||
const screenshotPath = await writeNewAttachmentData(
|
||||
new Uint8Array(screenshotBuffer)
|
||||
);
|
||||
|
||||
const thumbnailBuffer = await blobToArrayBuffer(
|
||||
await makeImageThumbnail({
|
||||
|
@ -514,7 +517,9 @@ export async function captureDimensionsAndScreenshot(
|
|||
})
|
||||
);
|
||||
|
||||
const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer);
|
||||
const thumbnailPath = await writeNewAttachmentData(
|
||||
new Uint8Array(thumbnailBuffer)
|
||||
);
|
||||
|
||||
return {
|
||||
...attachment,
|
||||
|
@ -876,14 +881,14 @@ export const save = async ({
|
|||
}: {
|
||||
attachment: AttachmentType;
|
||||
index?: number;
|
||||
readAttachmentData: (relativePath: string) => Promise<ArrayBuffer>;
|
||||
readAttachmentData: (relativePath: string) => Promise<Uint8Array>;
|
||||
saveAttachmentToDisk: (options: {
|
||||
data: ArrayBuffer;
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
}) => Promise<{ name: string; fullPath: string }>;
|
||||
}) => Promise<{ name: string; fullPath: string } | null>;
|
||||
timestamp?: number;
|
||||
}): Promise<string | null> => {
|
||||
let data: ArrayBuffer;
|
||||
let data: Uint8Array;
|
||||
if (attachment.path) {
|
||||
data = await readAttachmentData(attachment.path);
|
||||
} else if (attachment.data) {
|
||||
|
|
|
@ -42,7 +42,7 @@ export type AvatarIconType = GroupAvatarIconType | PersonalAvatarIconType;
|
|||
|
||||
export type AvatarDataType = {
|
||||
id: number | string;
|
||||
buffer?: ArrayBuffer;
|
||||
buffer?: Uint8Array;
|
||||
color?: AvatarColorType;
|
||||
icon?: AvatarIconType;
|
||||
imagePath?: string;
|
||||
|
|
89
ts/types/Conversation.ts
Normal file
89
ts/types/Conversation.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { computeHash } from '../Crypto';
|
||||
import { ConversationAttributesType } from '../model-types.d';
|
||||
|
||||
export type BuildAvatarUpdaterOptions = Readonly<{
|
||||
deleteAttachmentData: (path: string) => Promise<void>;
|
||||
doesAttachmentExist: (path: string) => Promise<boolean>;
|
||||
writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
|
||||
}>;
|
||||
|
||||
function buildAvatarUpdater({ field }: { field: 'avatar' | 'profileAvatar' }) {
|
||||
return async (
|
||||
conversation: Readonly<ConversationAttributesType>,
|
||||
data: Uint8Array,
|
||||
{
|
||||
deleteAttachmentData,
|
||||
doesAttachmentExist,
|
||||
writeNewAttachmentData,
|
||||
}: BuildAvatarUpdaterOptions
|
||||
): Promise<ConversationAttributesType> => {
|
||||
if (!conversation) {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
const avatar = conversation[field];
|
||||
|
||||
const newHash = computeHash(data);
|
||||
|
||||
if (!avatar || !avatar.hash) {
|
||||
return {
|
||||
...conversation,
|
||||
[field]: {
|
||||
hash: newHash,
|
||||
path: await writeNewAttachmentData(data),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { hash, path } = avatar;
|
||||
const exists = await doesAttachmentExist(path);
|
||||
if (!exists) {
|
||||
window.SignalWindow.log.warn(
|
||||
`Conversation.buildAvatarUpdater: attachment ${path} did not exist`
|
||||
);
|
||||
}
|
||||
|
||||
if (exists && hash === newHash) {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
await deleteAttachmentData(path);
|
||||
|
||||
return {
|
||||
...conversation,
|
||||
[field]: {
|
||||
hash: newHash,
|
||||
path: await writeNewAttachmentData(data),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const maybeUpdateAvatar = buildAvatarUpdater({ field: 'avatar' });
|
||||
export const maybeUpdateProfileAvatar = buildAvatarUpdater({
|
||||
field: 'profileAvatar',
|
||||
});
|
||||
|
||||
export async function deleteExternalFiles(
|
||||
conversation: ConversationAttributesType,
|
||||
{
|
||||
deleteAttachmentData,
|
||||
}: Pick<BuildAvatarUpdaterOptions, 'deleteAttachmentData'>
|
||||
): Promise<void> {
|
||||
if (!conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { avatar, profileAvatar } = conversation;
|
||||
|
||||
if (avatar && avatar.path) {
|
||||
await deleteAttachmentData(avatar.path);
|
||||
}
|
||||
|
||||
if (profileAvatar && profileAvatar.path) {
|
||||
await deleteAttachmentData(profileAvatar.path);
|
||||
}
|
||||
}
|
13
ts/types/Crypto.ts
Normal file
13
ts/types/Crypto.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export enum HashType {
|
||||
size256 = 'sha256',
|
||||
size512 = 'sha512',
|
||||
}
|
||||
|
||||
export enum CipherType {
|
||||
AES256CBC = 'aes-256-cbc',
|
||||
AES256CTR = 'aes-256-ctr',
|
||||
AES256GCM = 'aes-256-gcm',
|
||||
}
|
|
@ -152,7 +152,7 @@ export function parseAndWriteAvatar(
|
|||
message: MessageAttributesType;
|
||||
regionCode: string;
|
||||
logger: Pick<LoggerType, 'error'>;
|
||||
writeNewAttachmentData: (data: ArrayBuffer) => Promise<string>;
|
||||
writeNewAttachmentData: (data: Uint8Array) => Promise<string>;
|
||||
}
|
||||
): Promise<EmbeddedContactType> => {
|
||||
const { message, regionCode, logger } = context;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { replaceEmojiWithSpaces } from '../util/emoji';
|
|||
import { AttachmentType } from './Attachment';
|
||||
|
||||
export type LinkPreviewImage = AttachmentType & {
|
||||
data: ArrayBuffer;
|
||||
data: Uint8Array;
|
||||
};
|
||||
|
||||
export type LinkPreviewResult = {
|
||||
|
|
8
ts/types/SchemaVersion.ts
Normal file
8
ts/types/SchemaVersion.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
export const isValid = (value: unknown): boolean => {
|
||||
return Boolean(isNumber(value) && value >= 0);
|
||||
};
|
|
@ -9,7 +9,8 @@ import { strictAssert } from '../util/assert';
|
|||
import { dropNull } from '../util/dropNull';
|
||||
import { makeLookup } from '../util/makeLookup';
|
||||
import { maybeParseUrl } from '../util/url';
|
||||
import { base64ToArrayBuffer, deriveStickerPackKey } from '../Crypto';
|
||||
import * as Bytes from '../Bytes';
|
||||
import { deriveStickerPackKey, decryptAttachment } from '../Crypto';
|
||||
import type {
|
||||
StickerType,
|
||||
StickerPackType,
|
||||
|
@ -41,9 +42,6 @@ export type DownloadMap = Record<
|
|||
}
|
||||
>;
|
||||
|
||||
// TODO: remove once we move away from ArrayBuffers
|
||||
const FIXMEU8 = Uint8Array;
|
||||
|
||||
export const BLESSED_PACKS: Record<string, BlessedType> = {
|
||||
'9acc9e8aba563d26a4994e69263e3b25': {
|
||||
key: 'Wm3/OUjCjvubeq+T7MN1xp/DFueAd+0mhnoU0QoPahI=',
|
||||
|
@ -287,16 +285,10 @@ function getReduxStickerActions() {
|
|||
return actions.stickers;
|
||||
}
|
||||
|
||||
async function decryptSticker(
|
||||
packKey: string,
|
||||
ciphertext: ArrayBuffer
|
||||
): Promise<ArrayBuffer> {
|
||||
const binaryKey = base64ToArrayBuffer(packKey);
|
||||
const derivedKey = await deriveStickerPackKey(binaryKey);
|
||||
const plaintext = await window.textsecure.crypto.decryptAttachment(
|
||||
ciphertext,
|
||||
derivedKey
|
||||
);
|
||||
function decryptSticker(packKey: string, ciphertext: Uint8Array): Uint8Array {
|
||||
const binaryKey = Bytes.fromBase64(packKey);
|
||||
const derivedKey = deriveStickerPackKey(binaryKey);
|
||||
const plaintext = decryptAttachment(ciphertext, derivedKey);
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
@ -311,7 +303,7 @@ async function downloadSticker(
|
|||
strictAssert(id !== undefined && id !== null, "Sticker id can't be null");
|
||||
|
||||
const ciphertext = await window.textsecure.messaging.getSticker(packId, id);
|
||||
const plaintext = await decryptSticker(packKey, ciphertext);
|
||||
const plaintext = decryptSticker(packKey, ciphertext);
|
||||
|
||||
const sticker = ephemeral
|
||||
? await window.Signal.Migrations.processNewEphemeralSticker(plaintext)
|
||||
|
@ -413,8 +405,8 @@ export async function downloadEphemeralPack(
|
|||
const ciphertext = await window.textsecure.messaging.getStickerPackManifest(
|
||||
packId
|
||||
);
|
||||
const plaintext = await decryptSticker(packKey, ciphertext);
|
||||
const proto = Proto.StickerPack.decode(new FIXMEU8(plaintext));
|
||||
const plaintext = decryptSticker(packKey, ciphertext);
|
||||
const proto = Proto.StickerPack.decode(plaintext);
|
||||
const firstStickerProto = proto.stickers ? proto.stickers[0] : null;
|
||||
const stickerCount = proto.stickers.length;
|
||||
|
||||
|
@ -594,8 +586,8 @@ async function doDownloadStickerPack(
|
|||
const ciphertext = await window.textsecure.messaging.getStickerPackManifest(
|
||||
packId
|
||||
);
|
||||
const plaintext = await decryptSticker(packKey, ciphertext);
|
||||
const proto = Proto.StickerPack.decode(new FIXMEU8(plaintext));
|
||||
const plaintext = decryptSticker(packKey, ciphertext);
|
||||
const proto = Proto.StickerPack.decode(plaintext);
|
||||
const firstStickerProto = proto.stickers ? proto.stickers[0] : undefined;
|
||||
const stickerCount = proto.stickers.length;
|
||||
|
||||
|
|
4
ts/types/Storage.d.ts
vendored
4
ts/types/Storage.d.ts
vendored
|
@ -21,7 +21,7 @@ import type {
|
|||
|
||||
export type SerializedCertificateType = {
|
||||
expires: number;
|
||||
serialized: ArrayBuffer;
|
||||
serialized: Uint8Array;
|
||||
};
|
||||
|
||||
export type ZoomFactorType = 0.75 | 1 | 1.25 | 1.5 | 2 | number;
|
||||
|
@ -70,7 +70,7 @@ export type StorageAccessType = {
|
|||
maxPreKeyId: number;
|
||||
number_id: string;
|
||||
password: string;
|
||||
profileKey: ArrayBuffer;
|
||||
profileKey: Uint8Array;
|
||||
regionCode: string;
|
||||
registrationIdMap: Record<string, number>;
|
||||
remoteBuildExpiration: number;
|
||||
|
|
207
ts/types/VisualAttachment.ts
Normal file
207
ts/types/VisualAttachment.ts
Normal file
|
@ -0,0 +1,207 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import loadImage from 'blueimp-load-image';
|
||||
import { blobToArrayBuffer } from 'blob-util';
|
||||
import { toLogFormat } from './errors';
|
||||
import { MIMEType, IMAGE_PNG } from './MIME';
|
||||
import { LoggerType } from './Logging';
|
||||
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) => {
|
||||
const image = document.createElement('img');
|
||||
|
||||
image.addEventListener('load', () => {
|
||||
resolve({
|
||||
height: image.naturalHeight,
|
||||
width: image.naturalWidth,
|
||||
});
|
||||
});
|
||||
image.addEventListener('error', error => {
|
||||
logger.error('getImageDimensions error', toLogFormat(error));
|
||||
reject(error);
|
||||
});
|
||||
|
||||
image.src = objectUrl;
|
||||
});
|
||||
}
|
||||
|
||||
export type MakeImageThumbnailOptionsType = Readonly<{
|
||||
size: number;
|
||||
objectUrl: string;
|
||||
contentType?: MIMEType;
|
||||
logger: Pick<LoggerType, 'error'>;
|
||||
}>;
|
||||
|
||||
export function makeImageThumbnail({
|
||||
size,
|
||||
objectUrl,
|
||||
contentType = IMAGE_PNG,
|
||||
logger,
|
||||
}: MakeImageThumbnailOptionsType): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = document.createElement('img');
|
||||
|
||||
image.addEventListener('load', async () => {
|
||||
// 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,
|
||||
});
|
||||
|
||||
strictAssert(
|
||||
canvas instanceof HTMLCanvasElement,
|
||||
'loadImage must produce canvas'
|
||||
);
|
||||
|
||||
try {
|
||||
const blob = await canvasToBlob(canvas, contentType);
|
||||
resolve(blob);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
image.addEventListener('error', error => {
|
||||
logger.error('makeImageThumbnail error', toLogFormat(error));
|
||||
reject(error);
|
||||
});
|
||||
|
||||
image.src = objectUrl;
|
||||
});
|
||||
}
|
||||
|
||||
export type MakeVideoScreenshotOptionsType = Readonly<{
|
||||
objectUrl: string;
|
||||
contentType?: MIMEType;
|
||||
logger: Pick<LoggerType, 'error'>;
|
||||
}>;
|
||||
|
||||
export function makeVideoScreenshot({
|
||||
objectUrl,
|
||||
contentType = IMAGE_PNG,
|
||||
logger,
|
||||
}: MakeVideoScreenshotOptionsType): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
|
||||
function seek() {
|
||||
video.currentTime = 1.0;
|
||||
}
|
||||
|
||||
async function capture() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
const context = canvas.getContext('2d');
|
||||
strictAssert(context, 'Failed to get canvas context');
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
video.addEventListener('loadeddata', seek);
|
||||
video.removeEventListener('seeked', capture);
|
||||
|
||||
try {
|
||||
const image = canvasToBlob(canvas, contentType);
|
||||
resolve(image);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
video.addEventListener('loadeddata', seek);
|
||||
video.addEventListener('seeked', capture);
|
||||
|
||||
video.addEventListener('error', error => {
|
||||
logger.error('makeVideoScreenshot error', toLogFormat(error));
|
||||
reject(error);
|
||||
});
|
||||
|
||||
video.src = objectUrl;
|
||||
});
|
||||
}
|
||||
|
||||
export type MakeVideoThumbnailOptionsType = Readonly<{
|
||||
size: number;
|
||||
videoObjectUrl: string;
|
||||
logger: Pick<LoggerType, 'error'>;
|
||||
contentType: MIMEType;
|
||||
}>;
|
||||
|
||||
export async function makeVideoThumbnail({
|
||||
size,
|
||||
videoObjectUrl,
|
||||
logger,
|
||||
contentType,
|
||||
}: MakeVideoThumbnailOptionsType): Promise<Blob> {
|
||||
let screenshotObjectUrl: string | undefined;
|
||||
try {
|
||||
const blob = await makeVideoScreenshot({
|
||||
objectUrl: videoObjectUrl,
|
||||
contentType,
|
||||
logger,
|
||||
});
|
||||
const data = await blobToArrayBuffer(blob);
|
||||
screenshotObjectUrl = arrayBufferToObjectURL({
|
||||
data,
|
||||
type: contentType,
|
||||
});
|
||||
|
||||
// We need to wait for this, otherwise the finally below will run first
|
||||
const resultBlob = await makeImageThumbnail({
|
||||
size,
|
||||
objectUrl: screenshotObjectUrl,
|
||||
contentType,
|
||||
logger,
|
||||
});
|
||||
|
||||
return resultBlob;
|
||||
} finally {
|
||||
if (screenshotObjectUrl !== undefined) {
|
||||
revokeObjectUrl(screenshotObjectUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function makeObjectUrl(
|
||||
data: Uint8Array | ArrayBuffer,
|
||||
contentType: MIMEType
|
||||
): string {
|
||||
const blob = new Blob([data], {
|
||||
type: contentType,
|
||||
});
|
||||
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
export function revokeObjectUrl(objectUrl: string): void {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
export function toLogFormat(error: unknown): string {
|
||||
if (error instanceof Error && error.stack) {
|
||||
return error.stack;
|
||||
|
@ -10,3 +12,5 @@ export function toLogFormat(error: unknown): string {
|
|||
}
|
||||
|
||||
export class CapabilityError extends Error {}
|
||||
|
||||
export class ProfileDecryptError extends Error {}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue