// 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 { 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((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; }