signal-desktop/ts/util/getStoryDuration.ts

100 lines
2.8 KiB
TypeScript

// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AttachmentType } from '../types/Attachment';
import {
hasFailed,
hasNotResolved,
isDownloaded,
isGIF,
isVideo,
} from '../types/Attachment';
import { count } from './grapheme';
import { SECOND } from './durations';
import * as log from '../logging/log';
import * as Errors from '../types/errors';
const DEFAULT_DURATION = 5 * SECOND;
const MAX_VIDEO_DURATION = 30 * SECOND;
export async function getStoryDuration(
attachment: AttachmentType
): Promise<number | undefined> {
if (hasFailed(attachment)) {
return DEFAULT_DURATION;
}
if (attachment.textAttachment) {
// Minimum 5 seconds. +1 second for every 15 characters past the first
// 15 characters (round up).
// For text stories that include a link, +2 seconds to the playback time.
const length = attachment.textAttachment.text
? count(attachment.textAttachment.text)
: 0;
const additionalSeconds = (Math.ceil(length / 15) - 1) * SECOND;
const linkPreviewSeconds = attachment.textAttachment.preview
? 2 * SECOND
: 0;
return DEFAULT_DURATION + additionalSeconds + linkPreviewSeconds;
}
if (!isDownloaded(attachment) || hasNotResolved(attachment)) {
return;
}
if (isGIF([attachment]) || isVideo([attachment])) {
const videoEl = document.createElement('video');
const { url } = attachment;
if (!url) {
return DEFAULT_DURATION;
}
let duration: number;
try {
duration = await new Promise<number>((resolve, reject) => {
function resolveAndRemove() {
resolve(videoEl.duration * SECOND);
videoEl.removeEventListener('loadedmetadata', resolveAndRemove);
}
videoEl.addEventListener('loadedmetadata', resolveAndRemove);
videoEl.addEventListener('error', () => {
reject(videoEl.error ?? new Error('Failed to load'));
});
videoEl.src = url;
});
} catch (error) {
log.error(
'getStoryDuration: Failed to load video duration',
Errors.toLogFormat(error)
);
return DEFAULT_DURATION;
} finally {
// Stop loading video
videoEl.pause();
videoEl.removeAttribute('src'); // empty source
videoEl.load();
}
if (isGIF([attachment])) {
// GIFs: Loop gifs 3 times or play for 5 seconds, whichever is longer.
return Math.min(
Math.max(duration * 3, DEFAULT_DURATION),
MAX_VIDEO_DURATION
);
}
// Video max duration: 30 seconds
return Math.min(duration, MAX_VIDEO_DURATION);
}
if (attachment.caption) {
const length = count(attachment.caption);
const additionalSeconds = (Math.ceil(length / 15) - 1) * SECOND;
return DEFAULT_DURATION + additionalSeconds;
}
return DEFAULT_DURATION;
}