100 lines
2.8 KiB
TypeScript
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;
|
|
}
|