Allow feature-gated 10-minute video uploads (#11388)
This commit is contained in:
@@ -19,6 +19,7 @@ export enum Features {
|
|||||||
PostThreadKnownLikersEnable = 'post_thread:known_likers:enable',
|
PostThreadKnownLikersEnable = 'post_thread:known_likers:enable',
|
||||||
PostThreadKnownLikersFetchEnable = 'post_thread:known_likers:fetch:enable',
|
PostThreadKnownLikersFetchEnable = 'post_thread:known_likers:fetch:enable',
|
||||||
CustomLogoJapanEnable = 'custom_logo:japan:enable',
|
CustomLogoJapanEnable = 'custom_logo:japan:enable',
|
||||||
|
VideoAllow10MinuteEnable = 'video:allow-10-minute:enable',
|
||||||
VideoMultipartUploadEnable = 'video:multipart_upload:enable',
|
VideoMultipartUploadEnable = 'video:multipart_upload:enable',
|
||||||
SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable',
|
SearchStarterPacksV2Enable = 'search_starter_packs_v2:enable',
|
||||||
FollowSortEnable = 'follow_sort:enable',
|
FollowSortEnable = 'follow_sort:enable',
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export const VIDEO_SERVICE = 'https://video.bsky.app'
|
|||||||
export const VIDEO_SERVICE_DID = 'did:web:video.bsky.app'
|
export const VIDEO_SERVICE_DID = 'did:web:video.bsky.app'
|
||||||
|
|
||||||
export const VIDEO_MAX_DURATION_MS = 3 * 60 * 1000 // 3 minutes in milliseconds
|
export const VIDEO_MAX_DURATION_MS = 3 * 60 * 1000 // 3 minutes in milliseconds
|
||||||
|
export const VIDEO_10_MINUTE_MAX_DURATION_MS = 10 * 60 * 1000
|
||||||
/**
|
/**
|
||||||
* Maximum size of a video in megabytes, _not_ mebibytes. Backend uses
|
* Maximum size of a video in megabytes, _not_ mebibytes. Backend uses
|
||||||
* ISO megabytes.
|
* ISO megabytes.
|
||||||
|
|||||||
@@ -47,8 +47,10 @@ export async function openPicker(opts?: ImagePickerOptions) {
|
|||||||
|
|
||||||
export async function openUnifiedPicker({
|
export async function openUnifiedPicker({
|
||||||
selectionCountRemaining,
|
selectionCountRemaining,
|
||||||
|
videoMaxDurationMs = VIDEO_MAX_DURATION_MS,
|
||||||
}: {
|
}: {
|
||||||
selectionCountRemaining: number
|
selectionCountRemaining: number
|
||||||
|
videoMaxDurationMs?: number
|
||||||
}) {
|
}) {
|
||||||
return await launchImageLibraryAsync({
|
return await launchImageLibraryAsync({
|
||||||
exif: false,
|
exif: false,
|
||||||
@@ -61,6 +63,6 @@ export async function openUnifiedPicker({
|
|||||||
preferredAssetRepresentationMode:
|
preferredAssetRepresentationMode:
|
||||||
UIImagePickerPreferredAssetRepresentationMode.Automatic,
|
UIImagePickerPreferredAssetRepresentationMode.Automatic,
|
||||||
videoExportPreset: VideoExportPreset.Passthrough,
|
videoExportPreset: VideoExportPreset.Passthrough,
|
||||||
videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000,
|
videoMaxDuration: videoMaxDurationMs / 1000,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import {
|
|||||||
MAX_GRAPHEME_LENGTH,
|
MAX_GRAPHEME_LENGTH,
|
||||||
SUPPORTED_MIME_TYPES,
|
SUPPORTED_MIME_TYPES,
|
||||||
type SupportedMimeTypes,
|
type SupportedMimeTypes,
|
||||||
|
VIDEO_10_MINUTE_MAX_DURATION_MS,
|
||||||
VIDEO_MAX_DURATION_MS,
|
VIDEO_MAX_DURATION_MS,
|
||||||
} from '#/lib/constants'
|
} from '#/lib/constants'
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
@@ -267,6 +268,12 @@ export const ComposePost = ({
|
|||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
|
const allow10MinuteVideos = ax.features.enabled(
|
||||||
|
ax.features.VideoAllow10MinuteEnable,
|
||||||
|
)
|
||||||
|
const videoMaxDurationMs = allow10MinuteVideos
|
||||||
|
? VIDEO_10_MINUTE_MAX_DURATION_MS
|
||||||
|
: VIDEO_MAX_DURATION_MS
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const currentDid = currentAccount!.did
|
const currentDid = currentAccount!.did
|
||||||
@@ -434,7 +441,7 @@ export const ComposePost = ({
|
|||||||
* Fail early on duration so we don't spend time compressing a video the
|
* Fail early on duration so we don't spend time compressing a video the
|
||||||
* server would reject anyway.
|
* server would reject anyway.
|
||||||
*/
|
*/
|
||||||
if (asset.duration != null && asset.duration > VIDEO_MAX_DURATION_MS) {
|
if (asset.duration != null && asset.duration > videoMaxDurationMs) {
|
||||||
composerDispatch({
|
composerDispatch({
|
||||||
type: 'update_post',
|
type: 'update_post',
|
||||||
postId: postId,
|
postId: postId,
|
||||||
@@ -442,7 +449,9 @@ export const ComposePost = ({
|
|||||||
type: 'embed_update_video',
|
type: 'embed_update_video',
|
||||||
videoAction: {
|
videoAction: {
|
||||||
type: 'to_error',
|
type: 'to_error',
|
||||||
error: l`Videos must be less than 3 minutes long.`,
|
error: allow10MinuteVideos
|
||||||
|
? l`Videos must be 10 minutes or less.`
|
||||||
|
: l`Videos must be less than 3 minutes long.`,
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -469,7 +478,16 @@ export const ComposePost = ({
|
|||||||
telemetry,
|
telemetry,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[l, i18n, agent, currentDid, composerDispatch, ax.metric],
|
[
|
||||||
|
l,
|
||||||
|
i18n,
|
||||||
|
agent,
|
||||||
|
currentDid,
|
||||||
|
composerDispatch,
|
||||||
|
ax.metric,
|
||||||
|
videoMaxDurationMs,
|
||||||
|
allow10MinuteVideos,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onInitVideo = useNonReactiveCallback(() => {
|
const onInitVideo = useNonReactiveCallback(() => {
|
||||||
@@ -566,7 +584,7 @@ export const ComposePost = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (asset.duration != null && asset.duration > VIDEO_MAX_DURATION_MS) {
|
if (asset.duration != null && asset.duration > videoMaxDurationMs) {
|
||||||
composerDispatch({
|
composerDispatch({
|
||||||
type: 'update_post',
|
type: 'update_post',
|
||||||
postId,
|
postId,
|
||||||
@@ -574,7 +592,9 @@ export const ComposePost = ({
|
|||||||
type: 'embed_update_video',
|
type: 'embed_update_video',
|
||||||
videoAction: {
|
videoAction: {
|
||||||
type: 'to_error',
|
type: 'to_error',
|
||||||
error: l`Videos must be less than 3 minutes long.`,
|
error: allow10MinuteVideos
|
||||||
|
? l`Videos must be 10 minutes or less.`
|
||||||
|
: l`Videos must be less than 3 minutes long.`,
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -646,7 +666,16 @@ export const ComposePost = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[l, i18n, agent, currentDid, composerDispatch, ax.metric],
|
[
|
||||||
|
l,
|
||||||
|
i18n,
|
||||||
|
agent,
|
||||||
|
currentDid,
|
||||||
|
composerDispatch,
|
||||||
|
ax.metric,
|
||||||
|
videoMaxDurationMs,
|
||||||
|
allow10MinuteVideos,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSelectDraft = useCallback(
|
const handleSelectDraft = useCallback(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {msg, plural} from '@lingui/core/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
VIDEO_10_MINUTE_MAX_DURATION_MS,
|
||||||
VIDEO_MAX_DURATION_MS,
|
VIDEO_MAX_DURATION_MS,
|
||||||
VIDEO_MAX_SIZE,
|
VIDEO_MAX_SIZE,
|
||||||
VIDEO_MAX_SIZE_MB,
|
VIDEO_MAX_SIZE_MB,
|
||||||
@@ -22,6 +23,7 @@ import {Button} from '#/components/Button'
|
|||||||
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
||||||
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
|
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
|
||||||
import * as toast from '#/components/Toast'
|
import * as toast from '#/components/Toast'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
import {isAnimatedGif} from './videos/isAnimatedGif'
|
import {isAnimatedGif} from './videos/isAnimatedGif'
|
||||||
import {hasWebCodecs} from './videos/metadata'
|
import {hasWebCodecs} from './videos/metadata'
|
||||||
@@ -236,9 +238,11 @@ async function processImagePickerAssets(
|
|||||||
{
|
{
|
||||||
selectionCountRemaining,
|
selectionCountRemaining,
|
||||||
allowedAssetTypes,
|
allowedAssetTypes,
|
||||||
|
videoMaxDurationMs,
|
||||||
}: {
|
}: {
|
||||||
selectionCountRemaining: number
|
selectionCountRemaining: number
|
||||||
allowedAssetTypes: AssetType | undefined
|
allowedAssetTypes: AssetType | undefined
|
||||||
|
videoMaxDurationMs: number
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
/*
|
/*
|
||||||
@@ -362,7 +366,7 @@ async function processImagePickerAssets(
|
|||||||
supportedAssets[0].duration = supportedAssets[0].duration * 1000
|
supportedAssets[0].duration = supportedAssets[0].duration * 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
if (supportedAssets[0].duration > VIDEO_MAX_DURATION_MS) {
|
if (supportedAssets[0].duration > videoMaxDurationMs) {
|
||||||
errors.add(SelectedAssetError.VideoTooLong)
|
errors.add(SelectedAssetError.VideoTooLong)
|
||||||
supportedAssets = []
|
supportedAssets = []
|
||||||
}
|
}
|
||||||
@@ -393,6 +397,13 @@ export function SelectMediaButton({
|
|||||||
autoOpen,
|
autoOpen,
|
||||||
}: SelectMediaButtonProps) {
|
}: SelectMediaButtonProps) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const allow10MinuteVideos = ax.features.enabled(
|
||||||
|
ax.features.VideoAllow10MinuteEnable,
|
||||||
|
)
|
||||||
|
const videoMaxDurationMs = allow10MinuteVideos
|
||||||
|
? VIDEO_10_MINUTE_MAX_DURATION_MS
|
||||||
|
: VIDEO_MAX_DURATION_MS
|
||||||
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
|
||||||
const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
|
const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
|
||||||
const sheetWrapper = useSheetWrapper()
|
const sheetWrapper = useSheetWrapper()
|
||||||
@@ -412,6 +423,7 @@ export function SelectMediaButton({
|
|||||||
} = await processImagePickerAssets(rawAssets, {
|
} = await processImagePickerAssets(rawAssets, {
|
||||||
selectionCountRemaining,
|
selectionCountRemaining,
|
||||||
allowedAssetTypes,
|
allowedAssetTypes,
|
||||||
|
videoMaxDurationMs,
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -436,9 +448,9 @@ export function SelectMediaButton({
|
|||||||
[SelectedAssetError.MaxVideos]: _(
|
[SelectedAssetError.MaxVideos]: _(
|
||||||
msg`You can only select one video at a time.`,
|
msg`You can only select one video at a time.`,
|
||||||
),
|
),
|
||||||
[SelectedAssetError.VideoTooLong]: _(
|
[SelectedAssetError.VideoTooLong]: allow10MinuteVideos
|
||||||
msg`Videos must be less than 3 minutes long.`,
|
? _(msg`Videos must be 10 minutes or less.`)
|
||||||
),
|
: _(msg`Videos must be less than 3 minutes long.`),
|
||||||
[SelectedAssetError.MaxGIFs]: _(
|
[SelectedAssetError.MaxGIFs]: _(
|
||||||
msg`You can only select one GIF at a time.`,
|
msg`You can only select one GIF at a time.`,
|
||||||
),
|
),
|
||||||
@@ -458,7 +470,14 @@ export function SelectMediaButton({
|
|||||||
errors,
|
errors,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[_, onSelectAssets, selectionCountRemaining, allowedAssetTypes],
|
[
|
||||||
|
_,
|
||||||
|
onSelectAssets,
|
||||||
|
selectionCountRemaining,
|
||||||
|
allowedAssetTypes,
|
||||||
|
videoMaxDurationMs,
|
||||||
|
allow10MinuteVideos,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPressSelectMedia = useCallback(async () => {
|
const onPressSelectMedia = useCallback(async () => {
|
||||||
@@ -481,7 +500,7 @@ export function SelectMediaButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {assets, canceled} = await sheetWrapper(
|
const {assets, canceled} = await sheetWrapper(
|
||||||
openUnifiedPicker({selectionCountRemaining}),
|
openUnifiedPicker({selectionCountRemaining, videoMaxDurationMs}),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (canceled) return
|
if (canceled) return
|
||||||
@@ -494,6 +513,7 @@ export function SelectMediaButton({
|
|||||||
sheetWrapper,
|
sheetWrapper,
|
||||||
processSelectedAssets,
|
processSelectedAssets,
|
||||||
selectionCountRemaining,
|
selectionCountRemaining,
|
||||||
|
videoMaxDurationMs,
|
||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user