Descriptive error messages for video stories

This commit is contained in:
Fedor Indutny 2023-02-28 14:17:22 -08:00 committed by GitHub
parent c038c07b06
commit 4549291b7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 289 additions and 179 deletions

View file

@ -6313,12 +6313,20 @@
}, },
"StoryCreator__error--video-too-long": { "StoryCreator__error--video-too-long": {
"message": "Cannot post video to story because it is too long", "message": "Cannot post video to story because it is too long",
"description": "Error string for when a video post to story fails" "description": "(deleted 02/22/2023) Error string for when a video post to story fails"
}, },
"StoryCreator__error--video-unsupported": { "StoryCreator__error--video-unsupported": {
"message": "Cannot post video to story as it is an unsupported file format", "message": "Cannot post video to story as it is an unsupported file format",
"description": "Error string for when a video post to story fails" "description": "Error string for when a video post to story fails"
}, },
"icu:StoryCreator__error--video-too-long": {
"messageformat": "Cannot post video to story because it is longer than {maxDurationInSec, plural, one {1 second} other {# seconds}}.",
"description": "Error string for when a video post to story fails because video's duration is too long"
},
"icu:StoryCreator__error--video-too-big": {
"messageformat": "Cannot post video to story because it is larger than {limit}{units}.",
"description": "Error string for when a video post to story fails because video's file size is too big"
},
"StoryCreator__error--video-error": { "StoryCreator__error--video-error": {
"message": "Failed to load video", "message": "Failed to load video",
"description": "Error string for when a video post to story fails" "description": "Error string for when a video post to story fails"

View file

@ -17,6 +17,7 @@ import { reduceStorySendStatus } from '../util/resolveStorySendStatus';
export type PropsType = { export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
maxAttachmentSizeInKb: number;
me: ConversationType; me: ConversationType;
myStories: Array<MyStoryType>; myStories: Array<MyStoryType>;
onAddStory: () => unknown; onAddStory: () => unknown;
@ -32,6 +33,7 @@ function getNewestMyStory(story: MyStoryType): StoryViewType {
export function MyStoryButton({ export function MyStoryButton({
i18n, i18n,
maxAttachmentSizeInKb,
me, me,
myStories, myStories,
onAddStory, onAddStory,
@ -60,6 +62,7 @@ export function MyStoryButton({
return ( return (
<StoriesAddStoryButton <StoriesAddStoryButton
i18n={i18n} i18n={i18n}
maxAttachmentSizeInKb={maxAttachmentSizeInKb}
moduleClassName="StoryListItem StoryListItem--active-opactiy" moduleClassName="StoryListItem StoryListItem--active-opactiy"
onAddStory={onAddStory} onAddStory={onAddStory}
showToast={showToast} showToast={showToast}
@ -112,6 +115,7 @@ export function MyStoryButton({
<div className="MyStories__avatar-container"> <div className="MyStories__avatar-container">
<StoriesAddStoryButton <StoriesAddStoryButton
i18n={i18n} i18n={i18n}
maxAttachmentSizeInKb={maxAttachmentSizeInKb}
moduleClassName="StoryListItem--active-opacity" moduleClassName="StoryListItem--active-opacity"
onAddStory={onAddStory} onAddStory={onAddStory}
showToast={showToast} showToast={showToast}

View file

@ -29,6 +29,9 @@ export default {
i18n: { i18n: {
defaultValue: i18n, defaultValue: i18n,
}, },
maxAttachmentSizeInKb: {
defaultValue: 100 * 1024,
},
me: { me: {
defaultValue: getDefaultConversation(), defaultValue: getDefaultConversation(),
}, },

View file

@ -35,6 +35,7 @@ export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
isStoriesSettingsVisible: boolean; isStoriesSettingsVisible: boolean;
isViewingStory: boolean; isViewingStory: boolean;
maxAttachmentSizeInKb: number;
me: ConversationType; me: ConversationType;
myStories: Array<MyStoryType>; myStories: Array<MyStoryType>;
onForwardStory: (storyId: string) => unknown; onForwardStory: (storyId: string) => unknown;
@ -64,6 +65,7 @@ export function Stories({
i18n, i18n,
isStoriesSettingsVisible, isStoriesSettingsVisible,
isViewingStory, isViewingStory,
maxAttachmentSizeInKb,
me, me,
myStories, myStories,
onForwardStory, onForwardStory,
@ -122,6 +124,7 @@ export function Stories({
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
hiddenStories={hiddenStories} hiddenStories={hiddenStories}
i18n={i18n} i18n={i18n}
maxAttachmentSizeInKb={maxAttachmentSizeInKb}
me={me} me={me}
myStories={myStories} myStories={myStories}
onAddStory={file => onAddStory={file =>

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import React from 'react'; import React, { useState, useCallback } from 'react';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import type { ShowToastActionCreatorType } from '../state/ducks/toast'; import type { ShowToastActionCreatorType } from '../state/ducks/toast';
@ -13,10 +13,12 @@ import {
isVideoGoodForStories, isVideoGoodForStories,
ReasonVideoNotGood, ReasonVideoNotGood,
} from '../util/isVideoGoodForStories'; } from '../util/isVideoGoodForStories';
import { ConfirmationDialog } from './ConfirmationDialog';
export type PropsType = { export type PropsType = {
children?: ReactNode; children?: ReactNode;
i18n: LocalizerType; i18n: LocalizerType;
maxAttachmentSizeInKb: number;
moduleClassName?: string; moduleClassName?: string;
onAddStory: (file?: File) => unknown; onAddStory: (file?: File) => unknown;
onContextMenuShowingChanged?: (value: boolean) => void; onContextMenuShowingChanged?: (value: boolean) => void;
@ -26,68 +28,109 @@ export type PropsType = {
export function StoriesAddStoryButton({ export function StoriesAddStoryButton({
children, children,
i18n, i18n,
maxAttachmentSizeInKb,
moduleClassName, moduleClassName,
onAddStory, onAddStory,
showToast, showToast,
onContextMenuShowingChanged, onContextMenuShowingChanged,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const [error, setError] = useState<string | undefined>();
const onAddMedia = useCallback(() => {
const input = document.createElement('input');
input.accept = 'image/*,video/mp4';
input.type = 'file';
input.onchange = async () => {
const file = input.files ? input.files[0] : undefined;
if (!file) {
return;
}
const result = await isVideoGoodForStories(file, {
maxAttachmentSizeInKb,
});
if (
result.reason === ReasonVideoNotGood.UnsupportedCodec ||
result.reason === ReasonVideoNotGood.UnsupportedContainer
) {
showToast(ToastType.StoryVideoUnsupported);
return;
}
if (result.reason === ReasonVideoNotGood.TooLong) {
setError(
i18n('icu:StoryCreator__error--video-too-long', {
maxDurationInSec: result.maxDurationInSec,
})
);
return;
}
if (result.reason === ReasonVideoNotGood.TooBig) {
setError(
i18n('icu:StoryCreator__error--video-too-big', result.renderDetails)
);
return;
}
if (result.reason !== ReasonVideoNotGood.AllGoodNevermind) {
showToast(ToastType.StoryVideoError);
return;
}
onAddStory(file);
};
input.click();
}, [setError, showToast, i18n, maxAttachmentSizeInKb, onAddStory]);
return ( return (
<ContextMenu <>
ariaLabel={i18n('Stories__add')} <ContextMenu
i18n={i18n} ariaLabel={i18n('Stories__add')}
onMenuShowingChanged={onContextMenuShowingChanged} i18n={i18n}
menuOptions={[ onMenuShowingChanged={onContextMenuShowingChanged}
{ menuOptions={[
label: i18n('Stories__add-story--media'), {
onClick: () => { label: i18n('Stories__add-story--media'),
const input = document.createElement('input'); onClick: onAddMedia,
input.accept = 'image/*,video/mp4';
input.type = 'file';
input.onchange = async () => {
const file = input.files ? input.files[0] : undefined;
if (!file) {
return;
}
const result = await isVideoGoodForStories(file);
if (
result === ReasonVideoNotGood.UnsupportedCodec ||
result === ReasonVideoNotGood.UnsupportedContainer
) {
showToast(ToastType.StoryVideoUnsupported);
return;
}
if (result === ReasonVideoNotGood.TooLong) {
showToast(ToastType.StoryVideoTooLong);
return;
}
if (result !== ReasonVideoNotGood.AllGoodNevermind) {
showToast(ToastType.StoryVideoError);
return;
}
onAddStory(file);
};
input.click();
}, },
}, {
{ label: i18n('Stories__add-story--text'),
label: i18n('Stories__add-story--text'), onClick: () => onAddStory(),
onClick: () => onAddStory(), },
}, ]}
]} moduleClassName={moduleClassName}
moduleClassName={moduleClassName} popperOptions={{
popperOptions={{ placement: 'bottom',
placement: 'bottom', strategy: 'absolute',
strategy: 'absolute', }}
}} theme={Theme.Dark}
theme={Theme.Dark} >
> {children}
{children} </ContextMenu>
</ContextMenu> {error && (
<ConfirmationDialog
dialogName="StoriesAddStoryButton.error"
noDefaultCancelButton
actions={[
{
action: () => {
setError(undefined);
},
style: 'affirmative',
text: i18n('Confirmation--confirm'),
},
]}
i18n={i18n}
onClose={() => {
setError(undefined);
}}
>
{error}
</ConfirmationDialog>
)}
</>
); );
} }

View file

@ -59,6 +59,7 @@ export type PropsType = {
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
hiddenStories: Array<ConversationStoryType>; hiddenStories: Array<ConversationStoryType>;
i18n: LocalizerType; i18n: LocalizerType;
maxAttachmentSizeInKb: number;
me: ConversationType; me: ConversationType;
myStories: Array<MyStoryType>; myStories: Array<MyStoryType>;
onAddStory: (file?: File) => unknown; onAddStory: (file?: File) => unknown;
@ -78,6 +79,7 @@ export function StoriesPane({
getPreferredBadge, getPreferredBadge,
hiddenStories, hiddenStories,
i18n, i18n,
maxAttachmentSizeInKb,
me, me,
myStories, myStories,
onAddStory, onAddStory,
@ -123,6 +125,7 @@ export function StoriesPane({
</div> </div>
<StoriesAddStoryButton <StoriesAddStoryButton
i18n={i18n} i18n={i18n}
maxAttachmentSizeInKb={maxAttachmentSizeInKb}
moduleClassName="Stories__pane__add-story" moduleClassName="Stories__pane__add-story"
onAddStory={onAddStory} onAddStory={onAddStory}
showToast={showToast} showToast={showToast}
@ -155,6 +158,7 @@ export function StoriesPane({
<div className="Stories__pane__list"> <div className="Stories__pane__list">
<MyStoryButton <MyStoryButton
i18n={i18n} i18n={i18n}
maxAttachmentSizeInKb={maxAttachmentSizeInKb}
me={me} me={me}
myStories={myStories} myStories={myStories}
onAddStory={onAddStory} onAddStory={onAddStory}

View file

@ -288,13 +288,6 @@ StoryVideoError.args = {
}, },
}; };
export const StoryVideoTooLong = Template.bind({});
StoryVideoTooLong.args = {
toast: {
toastType: ToastType.StoryVideoTooLong,
},
};
export const StoryVideoUnsupported = Template.bind({}); export const StoryVideoUnsupported = Template.bind({});
StoryVideoUnsupported.args = { StoryVideoUnsupported.args = {
toast: { toast: {

View file

@ -278,14 +278,6 @@ export function ToastManager({
); );
} }
if (toastType === ToastType.StoryVideoTooLong) {
return (
<Toast onClose={hideToast}>
{i18n('StoryCreator__error--video-too-long')}
</Toast>
);
}
if (toastType === ToastType.StoryVideoUnsupported) { if (toastType === ToastType.StoryVideoUnsupported) {
return ( return (
<Toast onClose={hideToast}> <Toast onClose={hideToast}>

View file

@ -51,12 +51,14 @@ import {
resetLinkPreview, resetLinkPreview,
suspendLinkPreviews, suspendLinkPreviews,
} from '../../services/LinkPreview'; } from '../../services/LinkPreview';
import { getMaximumAttachmentSizeInKb, KIBIBYTE } from '../../util/attachments';
import { getRecipientsByConversation } from '../../util/getRecipientsByConversation';
import { import {
getMaximumAttachmentSizeInKb,
getRenderDetailsForLimit, getRenderDetailsForLimit,
processAttachment, KIBIBYTE,
} from '../../util/processAttachment'; } from '../../types/AttachmentSize';
import { getValue as getRemoteConfigValue } from '../../RemoteConfig';
import { getRecipientsByConversation } from '../../util/getRecipientsByConversation';
import { processAttachment } from '../../util/processAttachment';
import { hasDraftAttachments } from '../../util/hasDraftAttachments'; import { hasDraftAttachments } from '../../util/hasDraftAttachments';
import { isFileDangerous } from '../../util/isFileDangerous'; import { isFileDangerous } from '../../util/isFileDangerous';
import { isImage, isVideo, stringToMIMEType } from '../../types/MIME'; import { isImage, isVideo, stringToMIMEType } from '../../types/MIME';
@ -908,7 +910,7 @@ function preProcessAttachment(
return; return;
} }
const limitKb = getMaximumAttachmentSizeInKb(); const limitKb = getMaximumAttachmentSizeInKb(getRemoteConfigValue);
if (file.size / KIBIBYTE > limitKb) { if (file.size / KIBIBYTE > limitKb) {
return { return {
toastType: ToastType.FileSize, toastType: ToastType.FileSize,

View file

@ -63,7 +63,7 @@ const isRemoteConfigBucketEnabled = (
return innerIsBucketValueEnabled(name, flagValue, e164, uuid); return innerIsBucketValueEnabled(name, flagValue, e164, uuid);
}; };
const getRemoteConfig = createSelector( export const getRemoteConfig = createSelector(
getItems, getItems,
(state: ItemsStateType): ConfigMapType => state.remoteConfig || {} (state: ItemsStateType): ConfigMapType => state.remoteConfig || {}
); );

View file

@ -8,12 +8,15 @@ import type { LocalizerType } from '../../types/Util';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { SmartStoryCreator } from './StoryCreator'; import { SmartStoryCreator } from './StoryCreator';
import { Stories } from '../../components/Stories'; import { Stories } from '../../components/Stories';
import { getMaximumAttachmentSizeInKb } from '../../types/AttachmentSize';
import type { ConfigKeyType } from '../../RemoteConfig';
import { getMe } from '../selectors/conversations'; import { getMe } from '../selectors/conversations';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { getPreferredBadgeSelector } from '../selectors/badges'; import { getPreferredBadgeSelector } from '../selectors/badges';
import { import {
getHasStoryViewReceiptSetting, getHasStoryViewReceiptSetting,
getPreferredLeftPaneWidth, getPreferredLeftPaneWidth,
getRemoteConfig,
} from '../selectors/items'; } from '../selectors/items';
import { import {
getAddStoryData, getAddStoryData,
@ -67,6 +70,13 @@ export function SmartStories(): JSX.Element | null {
const hasViewReceiptSetting = useSelector(getHasStoryViewReceiptSetting); const hasViewReceiptSetting = useSelector(getHasStoryViewReceiptSetting);
const remoteConfig = useSelector(getRemoteConfig);
const maxAttachmentSizeInKb = getMaximumAttachmentSizeInKb(
(name: ConfigKeyType) => {
const value = remoteConfig[name]?.value;
return value ? String(value) : undefined;
}
);
const { pauseVoiceNotePlayer } = useAudioPlayerActions(); const { pauseVoiceNotePlayer } = useAudioPlayerActions();
if (!isShowingStoriesView) { if (!isShowingStoriesView) {
@ -79,6 +89,7 @@ export function SmartStories(): JSX.Element | null {
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
hiddenStories={hiddenStories} hiddenStories={hiddenStories}
i18n={i18n} i18n={i18n}
maxAttachmentSizeInKb={maxAttachmentSizeInKb}
me={me} me={me}
myStories={myStories} myStories={myStories}
onForwardStory={toggleForwardMessageModal} onForwardStory={toggleForwardMessageModal}

View file

@ -0,0 +1,46 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../logging/log';
import { parseIntOrThrow } from '../util/parseIntOrThrow';
import type * as RemoteConfig from '../RemoteConfig';
export const KIBIBYTE = 1024;
const MEBIBYTE = 1024 * 1024;
const DEFAULT_MAX = 100 * MEBIBYTE;
export const getMaximumAttachmentSizeInKb = (
getValue: typeof RemoteConfig.getValue
): number => {
try {
return (
parseIntOrThrow(
getValue('global.attachments.maxBytes'),
'preProcessAttachment/maxAttachmentSize'
) / KIBIBYTE
);
} catch (error) {
log.warn(
'Failed to parse integer out of global.attachments.maxBytes feature flag'
);
return DEFAULT_MAX / KIBIBYTE;
}
};
export function getRenderDetailsForLimit(limitKb: number): {
limit: string;
units: string;
} {
const units = ['kB', 'MB', 'GB'];
let u = -1;
let limit = limitKb * KIBIBYTE;
do {
limit /= KIBIBYTE;
u += 1;
} while (limit >= KIBIBYTE && u < units.length - 1);
return {
limit: limit.toFixed(0),
units: units[u],
};
}

View file

@ -35,7 +35,6 @@ export enum ToastType {
StoryReact = 'StoryReact', StoryReact = 'StoryReact',
StoryReply = 'StoryReply', StoryReply = 'StoryReply',
StoryVideoError = 'StoryVideoError', StoryVideoError = 'StoryVideoError',
StoryVideoTooLong = 'StoryVideoTooLong',
StoryVideoUnsupported = 'StoryVideoUnsupported', StoryVideoUnsupported = 'StoryVideoUnsupported',
TapToViewExpiredIncoming = 'TapToViewExpiredIncoming', TapToViewExpiredIncoming = 'TapToViewExpiredIncoming',
TapToViewExpiredOutgoing = 'TapToViewExpiredOutgoing', TapToViewExpiredOutgoing = 'TapToViewExpiredOutgoing',

View file

@ -3,10 +3,7 @@
import { omit } from 'lodash'; import { omit } from 'lodash';
import { blobToArrayBuffer } from 'blob-util'; import { blobToArrayBuffer } from 'blob-util';
import * as log from '../logging/log';
import { getValue } from '../RemoteConfig';
import { parseIntOrThrow } from './parseIntOrThrow';
import { scaleImageToLevel } from './scaleImageToLevel'; import { scaleImageToLevel } from './scaleImageToLevel';
import type { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
import { canBeTranscoded } from '../types/Attachment'; import { canBeTranscoded } from '../types/Attachment';
@ -14,26 +11,6 @@ import type { LoggerType } from '../types/Logging';
import * as MIME from '../types/MIME'; import * as MIME from '../types/MIME';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
export const KIBIBYTE = 1024;
const MEBIBYTE = 1024 * 1024;
const DEFAULT_MAX = 100 * MEBIBYTE;
export const getMaximumAttachmentSizeInKb = (): number => {
try {
return (
parseIntOrThrow(
getValue('global.attachments.maxBytes'),
'preProcessAttachment/maxAttachmentSize'
) / KIBIBYTE
);
} catch (error) {
log.warn(
'Failed to parse integer out of global.attachments.maxBytes feature flag'
);
return DEFAULT_MAX / KIBIBYTE;
}
};
// Upgrade steps // Upgrade steps
// NOTE: This step strips all EXIF metadata from JPEG images as // NOTE: This step strips all EXIF metadata from JPEG images as
// part of re-encoding the image: // part of re-encoding the image:

View file

@ -3,9 +3,10 @@
import MP4Box from 'mp4box'; import MP4Box from 'mp4box';
import { VIDEO_MP4, isVideo } from '../types/MIME'; import { VIDEO_MP4, isVideo } from '../types/MIME';
import { SECOND } from './durations'; import { KIBIBYTE, getRenderDetailsForLimit } from '../types/AttachmentSize';
import { explodePromise } from './explodePromise';
const MAX_VIDEO_DURATION = 30 * SECOND; const MAX_VIDEO_DURATION_IN_SEC = 30;
type MP4ArrayBuffer = ArrayBuffer & { fileStart: number }; type MP4ArrayBuffer = ArrayBuffer & { fileStart: number };
@ -13,6 +14,7 @@ export enum ReasonVideoNotGood {
AllGoodNevermind = 'AllGoodNevermind', AllGoodNevermind = 'AllGoodNevermind',
CouldNotReadFile = 'CouldNotReadFile', CouldNotReadFile = 'CouldNotReadFile',
TooLong = 'TooLong', TooLong = 'TooLong',
TooBig = 'TooBig',
UnsupportedCodec = 'UnsupportedCodec', UnsupportedCodec = 'UnsupportedCodec',
UnsupportedContainer = 'UnsupportedContainer', UnsupportedContainer = 'UnsupportedContainer',
} }
@ -25,70 +27,106 @@ function createMp4ArrayBuffer(src: ArrayBuffer): MP4ArrayBuffer {
return arrayBuffer as MP4ArrayBuffer; return arrayBuffer as MP4ArrayBuffer;
} }
export type IsVideoGoodForStoriesResultType = Readonly<
| {
reason: Exclude<
ReasonVideoNotGood,
ReasonVideoNotGood.TooLong | ReasonVideoNotGood.TooBig
>;
}
| {
reason: ReasonVideoNotGood.TooLong;
maxDurationInSec: number;
}
| {
reason: ReasonVideoNotGood.TooBig;
renderDetails: ReturnType<typeof getRenderDetailsForLimit>;
}
>;
export type IsVideoGoodForStoriesOptionsType = Readonly<{
maxAttachmentSizeInKb: number;
}>;
export async function isVideoGoodForStories( export async function isVideoGoodForStories(
file: File file: File,
): Promise<ReasonVideoNotGood> { { maxAttachmentSizeInKb }: IsVideoGoodForStoriesOptionsType
): Promise<IsVideoGoodForStoriesResultType> {
if (!isVideo(file.type)) { if (!isVideo(file.type)) {
return ReasonVideoNotGood.AllGoodNevermind; return { reason: ReasonVideoNotGood.AllGoodNevermind };
} }
if (file.type !== VIDEO_MP4) { if (file.type !== VIDEO_MP4) {
return ReasonVideoNotGood.UnsupportedContainer; return { reason: ReasonVideoNotGood.UnsupportedContainer };
} }
try { let src: ArrayBuffer;
const src = await new Promise<ArrayBuffer>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (reader.result) {
resolve(reader.result as ArrayBuffer);
} else {
reject(ReasonVideoNotGood.CouldNotReadFile);
}
};
reader.readAsArrayBuffer(file);
});
const arrayBuffer = createMp4ArrayBuffer(src); {
const { promise, resolve } = explodePromise<ArrayBuffer | undefined>();
const mp4 = MP4Box.createFile(); const reader = new FileReader();
await new Promise<void>((resolve, reject) => { reader.onload = () => {
mp4.onReady = info => { if (reader.result) {
// mp4box returns a `duration` in `timescale` units resolve(reader.result as ArrayBuffer);
const seconds = info.duration / info.timescale; } else {
const milliseconds = seconds * 1000; resolve(undefined);
}
};
reader.readAsArrayBuffer(file);
if (milliseconds > MAX_VIDEO_DURATION) { const maybeSrc = await promise;
reject(ReasonVideoNotGood.TooLong); if (maybeSrc === undefined) {
return; return { reason: ReasonVideoNotGood.CouldNotReadFile };
}
const codecs = /codecs="([\w,.]+)"/.exec(info.mime);
if (!codecs || !codecs[1]) {
reject(ReasonVideoNotGood.UnsupportedCodec);
return;
}
const isH264 = codecs[1]
.split(',')
.some(codec => codec.startsWith('avc1'));
if (!isH264) {
reject(ReasonVideoNotGood.UnsupportedCodec);
return;
}
resolve();
};
mp4.appendBuffer(arrayBuffer);
});
mp4.flush();
return ReasonVideoNotGood.AllGoodNevermind;
} catch (err) {
if (err instanceof Error) {
throw err;
} }
return err;
src = maybeSrc;
}
if (src.byteLength / KIBIBYTE > maxAttachmentSizeInKb) {
return {
reason: ReasonVideoNotGood.TooBig,
renderDetails: getRenderDetailsForLimit(maxAttachmentSizeInKb),
};
}
const arrayBuffer = createMp4ArrayBuffer(src);
const { promise, resolve } =
explodePromise<IsVideoGoodForStoriesResultType>();
const mp4 = MP4Box.createFile();
mp4.onReady = info => {
// mp4box returns a `duration` in `timescale` units
const seconds = info.duration / info.timescale;
if (seconds > MAX_VIDEO_DURATION_IN_SEC) {
resolve({
reason: ReasonVideoNotGood.TooLong,
maxDurationInSec: MAX_VIDEO_DURATION_IN_SEC,
});
return;
}
const codecs = /codecs="([\w,.]+)"/.exec(info.mime);
if (!codecs || !codecs[1]) {
resolve({ reason: ReasonVideoNotGood.UnsupportedCodec });
return;
}
const isH264 = codecs[1].split(',').some(codec => codec.startsWith('avc1'));
if (!isH264) {
resolve({ reason: ReasonVideoNotGood.UnsupportedCodec });
return;
}
resolve({ reason: ReasonVideoNotGood.AllGoodNevermind });
};
mp4.appendBuffer(arrayBuffer);
try {
return await promise;
} finally {
mp4.flush();
} }
} }

View file

@ -6,8 +6,13 @@ import type {
AttachmentType, AttachmentType,
InMemoryAttachmentDraftType, InMemoryAttachmentDraftType,
} from '../types/Attachment'; } from '../types/Attachment';
import { getMaximumAttachmentSizeInKb, KIBIBYTE } from './attachments'; import {
getMaximumAttachmentSizeInKb,
getRenderDetailsForLimit,
KIBIBYTE,
} from '../types/AttachmentSize';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import { getValue as getRemoteConfigValue } from '../RemoteConfig';
import { fileToBytes } from './fileToBytes'; import { fileToBytes } from './fileToBytes';
import { handleImageAttachment } from './handleImageAttachment'; import { handleImageAttachment } from './handleImageAttachment';
import { handleVideoAttachment } from './handleVideoAttachment'; import { handleVideoAttachment } from './handleVideoAttachment';
@ -68,26 +73,8 @@ export async function processAttachment(
} }
} }
export function getRenderDetailsForLimit(limitKb: number): {
limit: string;
units: string;
} {
const units = ['kB', 'MB', 'GB'];
let u = -1;
let limit = limitKb * KIBIBYTE;
do {
limit /= KIBIBYTE;
u += 1;
} while (limit >= KIBIBYTE && u < units.length - 1);
return {
limit: limit.toFixed(0),
units: units[u],
};
}
function isAttachmentSizeOkay(attachment: Readonly<AttachmentType>): boolean { function isAttachmentSizeOkay(attachment: Readonly<AttachmentType>): boolean {
const limitKb = getMaximumAttachmentSizeInKb(); const limitKb = getMaximumAttachmentSizeInKb(getRemoteConfigValue);
// this needs to be cast properly // this needs to be cast properly
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore