Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d532f053ae | |||
| 6fc3c6e28e | |||
| 527e800d01 | |||
| e2223fe3ed | |||
| ed17e94cec | |||
| 6a54959080 | |||
| 1412d2a544 | |||
| af02488812 | |||
| 4bd0ff8ba2 |
@@ -202,6 +202,7 @@ module.exports = function (config) {
|
||||
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
|
||||
},
|
||||
],
|
||||
['@config-plugins/ffmpeg-kit-react-native', {package: 'min-gpl'}],
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v2h2V5H5Zm4 0v6h6V5H9Zm8 0v2h2V5h-2Zm2 4h-2v2h2V9Zm0 4h-2v2.444h2V13Zm0 4.444h-2V19h2v-1.556ZM15 19v-6H9v6h6Zm-8 0v-2H5v2h2Zm-2-4h2v-2H5v2Zm0-4h2V9H5v2Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 370 B |
@@ -52,6 +52,7 @@
|
||||
"@atproto/api": "^0.12.18",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@config-plugins/ffmpeg-kit-react-native": "^8.0.0",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@expo/html-elements": "^0.4.2",
|
||||
@@ -137,6 +138,7 @@
|
||||
"expo-updates": "~0.25.14",
|
||||
"expo-web-browser": "~13.0.3",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"ffmpeg-kit-react-native": "^6.0.2",
|
||||
"history": "^5.3.0",
|
||||
"js-sha256": "^0.9.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const VideoClip_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v2h2V5H5Zm4 0v6h6V5H9Zm8 0v2h2V5h-2Zm2 4h-2v2h2V9Zm0 4h-2v2.444h2V13Zm0 4.444h-2V19h2v-1.556ZM15 19v-6H9v6h6Zm-8 0v-2H5v2h2Zm-2-4h2v-2H5v2Zm0-4h2V9H5v2Z',
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as FileSystem from 'expo-file-system'
|
||||
import {
|
||||
FFmpegKit,
|
||||
FFmpegSessionCompleteCallback,
|
||||
ReturnCode,
|
||||
} from 'ffmpeg-kit-react-native'
|
||||
|
||||
const PRESET = 'faster'
|
||||
|
||||
export type CompressedVideo = {
|
||||
uri: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export async function compressVideo(
|
||||
file: string,
|
||||
callbacks?: {
|
||||
onProgress: (progress: number) => void
|
||||
},
|
||||
) {
|
||||
const {onProgress} = callbacks || {}
|
||||
const ext = file.split('.').pop()
|
||||
const newFile = file.replace(`.${ext}`, '.compressed.mp4')
|
||||
|
||||
const result = await new Promise((resolve: FFmpegSessionCompleteCallback) =>
|
||||
FFmpegKit.executeAsync(
|
||||
`-i ${file} -c:v libx264 -crf 25 -preset ${PRESET} -b:v 3M -vf "scale='if(gt(a,1),min(1920,iw),-1)':'if(gt(a,1),-1,min(1920,ih))'" -t 90 -c:a aac -b:a 320k -movflags +faststart ${newFile}`,
|
||||
resolve,
|
||||
undefined,
|
||||
stats => onProgress?.(stats.getTime()),
|
||||
),
|
||||
)
|
||||
|
||||
const success = ReturnCode.isSuccess(await result.getReturnCode())
|
||||
|
||||
if (success) {
|
||||
await FileSystem.deleteAsync(file)
|
||||
const res = await FileSystem.getInfoAsync(newFile, {size: true})
|
||||
if (res.exists) {
|
||||
console.log('compressed size', (res.size / 1024 / 1024).toFixed(2) + 'mb')
|
||||
|
||||
return {
|
||||
success,
|
||||
video: {uri: newFile, size: res.size} as CompressedVideo,
|
||||
}
|
||||
} else {
|
||||
throw new Error('Could not find output video')
|
||||
}
|
||||
} else {
|
||||
return {success: false}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
|
||||
const MAX_WIDTH = 1920
|
||||
const MAX_HEIGHT = 1920
|
||||
const MAX_VIDEO_SIZE = 1024 * 1024 * 100 // 100MB
|
||||
|
||||
export async function compressVideo(
|
||||
file: string,
|
||||
callbacks?: {
|
||||
onProgress: (progress: number) => void
|
||||
},
|
||||
) {
|
||||
const {onProgress} = callbacks || {}
|
||||
const blob = await fetch(file).then(res => res.blob())
|
||||
const objectUrl = URL.createObjectURL(blob)
|
||||
|
||||
const videoEl = document.createElement('video')
|
||||
videoEl.setAttribute('playsinline', 'playsinline')
|
||||
videoEl.setAttribute('controls', 'controls')
|
||||
videoEl.setAttribute('muted', 'muted')
|
||||
videoEl.setAttribute('src', objectUrl)
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
videoEl.addEventListener('error', reject, {once: true})
|
||||
videoEl.addEventListener('loadedmetadata', resolve, {once: true})
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
Toast.show('Failed to load video, this video format may not be supported')
|
||||
}
|
||||
|
||||
let {videoWidth, videoHeight} = videoEl
|
||||
let outputWidth = videoWidth
|
||||
let outputHeight = videoHeight
|
||||
if (outputWidth > outputHeight) {
|
||||
if (outputWidth > MAX_WIDTH) {
|
||||
const scale = MAX_WIDTH / outputWidth
|
||||
outputWidth = Math.round(outputWidth * scale)
|
||||
outputHeight = Math.round(outputHeight * scale)
|
||||
}
|
||||
} else {
|
||||
if (outputHeight > MAX_HEIGHT) {
|
||||
const scale = MAX_HEIGHT / outputHeight
|
||||
outputWidth = Math.round(outputWidth * scale)
|
||||
outputHeight = Math.round(outputHeight * scale)
|
||||
}
|
||||
}
|
||||
if (outputWidth % 2 === 1) outputWidth--
|
||||
if (outputHeight % 2 === 1) outputHeight--
|
||||
console.log({outputWidth, outputHeight})
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = outputWidth
|
||||
canvas.height = outputHeight
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Could not get canvas context')
|
||||
ctx.fillStyle = '#fff'
|
||||
|
||||
try {
|
||||
let wasTruncated = false
|
||||
const videoBlob = await new Promise<Blob>(async resolve => {
|
||||
const chunks: Blob[] = []
|
||||
let options = {
|
||||
mimeType: getSupportedMimeType(),
|
||||
videoBitsPerSecond: 200000,
|
||||
}
|
||||
const recorder = new MediaRecorder(canvas.captureStream(25), options)
|
||||
|
||||
recorder.onerror = console.log
|
||||
recorder.ondataavailable = e => {
|
||||
let size = chunks.reduce((acc, chunk) => acc + chunk.size, 0)
|
||||
if (size + e.data.size > MAX_VIDEO_SIZE) {
|
||||
wasTruncated = true
|
||||
recorder.stop()
|
||||
} else {
|
||||
chunks.push(e.data)
|
||||
}
|
||||
}
|
||||
recorder.onstop = () => {
|
||||
resolve(new Blob(chunks, {type: recorder.mimeType}))
|
||||
}
|
||||
|
||||
videoEl.play()
|
||||
recorder.start()
|
||||
|
||||
let lastCapture = Date.now()
|
||||
while (
|
||||
recorder.state === 'recording' &&
|
||||
videoEl.currentTime < videoEl.duration
|
||||
) {
|
||||
await new Promise(r => setTimeout(r, 1)) // NOTE: don't use requestAnimationFrame because it pauses with the tab isnt focused
|
||||
onProgress?.(videoEl.currentTime / videoEl.duration)
|
||||
ctx.fillRect(0, 0, outputWidth, outputHeight)
|
||||
ctx.drawImage(
|
||||
videoEl,
|
||||
0,
|
||||
0,
|
||||
videoWidth,
|
||||
videoHeight,
|
||||
0,
|
||||
0,
|
||||
outputWidth,
|
||||
outputHeight,
|
||||
)
|
||||
|
||||
if (Date.now() - lastCapture > 500) {
|
||||
recorder.requestData()
|
||||
lastCapture = Date.now()
|
||||
}
|
||||
}
|
||||
if (recorder.state === 'recording') {
|
||||
recorder.stop()
|
||||
}
|
||||
})
|
||||
|
||||
if (wasTruncated) {
|
||||
Toast.show('Video was too long and was truncated')
|
||||
}
|
||||
|
||||
return {
|
||||
uri: URL.createObjectURL(videoBlob),
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
Toast.show('Failed to compress video')
|
||||
} finally {
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
}
|
||||
}
|
||||
|
||||
function getSupportedMimeType() {
|
||||
if (MediaRecorder.isTypeSupported('video/mp4;codecs=h264')) {
|
||||
return 'video/mp4;codecs=h264'
|
||||
} else if (MediaRecorder.isTypeSupported('video/webm;codecs=h264')) {
|
||||
return 'video/webm;codecs=h264'
|
||||
} else if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9')) {
|
||||
return 'video/webm;codecs=vp9'
|
||||
} else {
|
||||
throw new Error('No supported video codec found')
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,10 @@ import {TextInput, TextInputRef} from './text-input/TextInput'
|
||||
import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
|
||||
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import {SelectVideoBtn} from './videos/SelectVideoBtn'
|
||||
import {useVideoState} from './videos/state'
|
||||
import {VideoPreview} from './videos/VideoPreview'
|
||||
import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress'
|
||||
|
||||
type CancelRef = {
|
||||
onPressCancel: () => void
|
||||
@@ -150,6 +154,14 @@ export const ComposePost = observer(function ComposePost({
|
||||
const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>(
|
||||
initQuote,
|
||||
)
|
||||
const {
|
||||
video,
|
||||
onSelectVideo,
|
||||
videoPending,
|
||||
videoProcessingData,
|
||||
clearVideo,
|
||||
videoProcessingProgress,
|
||||
} = useVideoState({setError})
|
||||
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
||||
const [extGif, setExtGif] = useState<Gif>()
|
||||
const [labels, setLabels] = useState<string[]>([])
|
||||
@@ -358,8 +370,9 @@ export const ComposePost = observer(function ComposePost({
|
||||
? _(msg`Write your reply`)
|
||||
: _(msg`What's up?`)
|
||||
|
||||
const canSelectImages = gallery.size < 4 && !extLink
|
||||
const hasMedia = gallery.size > 0 || Boolean(extLink)
|
||||
const canSelectImages =
|
||||
gallery.size < 4 && !extLink && !video && !videoPending
|
||||
const hasMedia = gallery.size > 0 || Boolean(extLink) || Boolean(video)
|
||||
|
||||
const onEmojiButtonPress = useCallback(() => {
|
||||
openPicker?.(textInput.current?.getCursorPosition())
|
||||
@@ -583,7 +596,15 @@ export const ComposePost = observer(function ComposePost({
|
||||
<QuoteX onRemove={() => setQuote(undefined)} />
|
||||
)}
|
||||
</View>
|
||||
) : undefined}
|
||||
) : null}
|
||||
{videoPending && videoProcessingData ? (
|
||||
<VideoTranscodeProgress
|
||||
input={videoProcessingData}
|
||||
progress={videoProcessingProgress}
|
||||
/>
|
||||
) : (
|
||||
video && <VideoPreview video={video} clear={clearVideo} />
|
||||
)}
|
||||
</Animated.ScrollView>
|
||||
<SuggestedLanguage text={richtext.text} />
|
||||
|
||||
@@ -602,6 +623,10 @@ export const ComposePost = observer(function ComposePost({
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
|
||||
<SelectVideoBtn
|
||||
onSelectVideo={onSelectVideo}
|
||||
disabled={!canSelectImages}
|
||||
/>
|
||||
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
|
||||
<SelectGifBtn
|
||||
onClose={focusTextInput}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Text} from '../../util/text/Text'
|
||||
// @ts-ignore no type definition -prf
|
||||
import ProgressCircle from 'react-native-progress/Circle'
|
||||
// @ts-ignore no type definition -prf
|
||||
import ProgressPie from 'react-native-progress/Pie'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
|
||||
import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {s} from 'lib/styles'
|
||||
import {Text} from '../../util/text/Text'
|
||||
|
||||
const DANGER_LENGTH = MAX_GRAPHEME_LENGTH
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {
|
||||
ImagePickerAsset,
|
||||
launchImageLibraryAsync,
|
||||
MediaTypeOptions,
|
||||
UIImagePickerPreferredAssetRepresentationMode,
|
||||
} from 'expo-image-picker'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {VideoClip_Stroke2_Corner0_Rounded as VideoClipIcon} from '#/components/icons/VideoClip'
|
||||
|
||||
const VIDEO_MAX_DURATION = 90
|
||||
|
||||
type Props = {
|
||||
onSelectVideo: (video: ImagePickerAsset) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function SelectVideoBtn({onSelectVideo, disabled}: Props) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const onPressSelectVideo = useCallback(async () => {
|
||||
const response = await launchImageLibraryAsync({
|
||||
exif: false,
|
||||
mediaTypes: MediaTypeOptions.Videos,
|
||||
videoMaxDuration: VIDEO_MAX_DURATION,
|
||||
quality: 1,
|
||||
legacy: true,
|
||||
preferredAssetRepresentationMode:
|
||||
UIImagePickerPreferredAssetRepresentationMode.Current,
|
||||
})
|
||||
if (response.assets && response.assets.length > 0) {
|
||||
onSelectVideo(response.assets[0])
|
||||
}
|
||||
}, [onSelectVideo])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
testID="openGifBtn"
|
||||
onPress={onPressSelectVideo}
|
||||
label={_(msg`Select GIF`)}
|
||||
accessibilityHint={_(msg`Opens GIF select dialog`)}
|
||||
style={a.p_sm}
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
color="primary"
|
||||
disabled={disabled}>
|
||||
<VideoClipIcon
|
||||
size="lg"
|
||||
style={disabled && t.atoms.text_contrast_low}
|
||||
/>
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
|
||||
import {CompressedVideo} from '#/lib/media/video/compress'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function VideoPreview({
|
||||
video,
|
||||
clear,
|
||||
}: {
|
||||
video: CompressedVideo
|
||||
clear: () => void
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Text>{JSON.stringify(video, null, 2)}</Text>
|
||||
<Button
|
||||
onPress={clear}
|
||||
label="Clear"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="solid">
|
||||
<ButtonText>Clear</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import Animated, {FadeIn} from 'react-native-reanimated'
|
||||
import * as FileSystem from 'expo-file-system'
|
||||
import {Image} from 'expo-image'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
import {FFmpegKit} from 'ffmpeg-kit-react-native'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
export function VideoTranscodeBackdrop({uri}: {uri: string}) {
|
||||
const {data: thumbnail} = useQuery({
|
||||
queryKey: ['thumbnail', uri],
|
||||
queryFn: async () => {
|
||||
const thumbnailJpg = `${FileSystem.cacheDirectory}/thumbnail.jpg`
|
||||
if ((await FileSystem.getInfoAsync(thumbnailJpg)).exists) {
|
||||
await FileSystem.deleteAsync(thumbnailJpg)
|
||||
}
|
||||
await FFmpegKit.execute(
|
||||
`-ss 00:00:01.000 -i ${uri} -vf 'scale=320:320:force_original_aspect_ratio=decrease' -frames:v 1 ${thumbnailJpg}`,
|
||||
)
|
||||
return thumbnailJpg
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Animated.View style={a.flex_1} entering={FadeIn}>
|
||||
{thumbnail && (
|
||||
<Image
|
||||
style={a.flex_1}
|
||||
source={thumbnail}
|
||||
cachePolicy="none"
|
||||
accessibilityIgnoresInvertColors
|
||||
blurRadius={15}
|
||||
contentFit="cover"
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
export function VideoTranscodeBackdrop({uri}: {uri: string}) {
|
||||
return <video src={uri} style={{flex: 1}} muted />
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
// @ts-expect-error no type definition
|
||||
import ProgressPie from 'react-native-progress/Pie'
|
||||
import {ImagePickerAsset} from 'expo-image-picker'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'
|
||||
|
||||
export function VideoTranscodeProgress({
|
||||
input,
|
||||
progress,
|
||||
}: {
|
||||
input: ImagePickerAsset
|
||||
progress: number
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.mt_md,
|
||||
t.atoms.bg_contrast_50,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
{aspectRatio: Math.max(input.width / input.height, 16 / 9)},
|
||||
]}>
|
||||
<VideoTranscodeBackdrop uri={input.uri} />
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.gap_lg,
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
]}>
|
||||
{input.duration ? (
|
||||
<ProgressPie
|
||||
size={64}
|
||||
borderWidth={4}
|
||||
borderColor={t.atoms.text.color}
|
||||
color={t.atoms.text.color}
|
||||
progress={progress}
|
||||
/>
|
||||
) : (
|
||||
<Loader size="xl" />
|
||||
)}
|
||||
<Text>Compressing...</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {useState} from 'react'
|
||||
import {ImagePickerAsset} from 'expo-image-picker'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {compressVideo} from '#/lib/media/video/compress'
|
||||
|
||||
export function useVideoState({setError}: {setError: (error: string) => void}) {
|
||||
const {_} = useLingui()
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
const {mutate, data, isPending, isError, reset, variables} = useMutation({
|
||||
mutationFn: async (asset: ImagePickerAsset) => {
|
||||
console.log(
|
||||
'uncompressed size',
|
||||
((asset.fileSize ?? 0) / 1024 / 1024).toFixed(2) + 'mb',
|
||||
)
|
||||
const compressed = await compressVideo(asset.uri, {
|
||||
onProgress: progressMs => {
|
||||
if (asset.duration) {
|
||||
setProgress(progressMs / asset.duration)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return compressed
|
||||
},
|
||||
onError: error => {
|
||||
console.error('error', error)
|
||||
setError(_(msg`Could not compress video`))
|
||||
},
|
||||
onMutate: () => {
|
||||
setProgress(0)
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
video: data?.video,
|
||||
onSelectVideo: mutate,
|
||||
videoPending: isPending,
|
||||
videoProcessingData: variables,
|
||||
videoError: isError,
|
||||
clearVideo: reset,
|
||||
videoProcessingProgress: progress,
|
||||
}
|
||||
}
|
||||
@@ -2929,6 +2929,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.1.1.tgz#21b11a1a3f18c3e7d62fd5f87438b7ed2c64c1f7"
|
||||
integrity sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw==
|
||||
|
||||
"@config-plugins/ffmpeg-kit-react-native@^8.0.0":
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@config-plugins/ffmpeg-kit-react-native/-/ffmpeg-kit-react-native-8.0.0.tgz#86d6f236bcab5b1d3faa768cea0e7508c25be544"
|
||||
integrity sha512-PUQHUBfRaGMBwPM8KnfHNVcWH0WvWO9QgIJYkb55/WGSmIdezsgJo/zlcXFexe7kkSTBqG/Pxnp5DGTYFBYBuQ==
|
||||
dependencies:
|
||||
semver "^7.3.5"
|
||||
|
||||
"@connectrpc/connect-express@^1.1.4":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@connectrpc/connect-express/-/connect-express-1.3.0.tgz#605cb536e041f5866868421ae00b1a794dcdd1ed"
|
||||
@@ -12512,6 +12519,11 @@ fetch-retry@^4.1.1:
|
||||
resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-4.1.1.tgz#fafe0bb22b54f4d0a9c788dff6dd7f8673ca63f3"
|
||||
integrity sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA==
|
||||
|
||||
ffmpeg-kit-react-native@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ffmpeg-kit-react-native/-/ffmpeg-kit-react-native-6.0.2.tgz#9eeac96ad89367c99480bd90431391405d4eb73e"
|
||||
integrity sha512-r9uSmahq8TeyIb7fXf3ft+uUXyoeWRFa99+khjo0TAzWO9y0z9wU7eGnab9JLw1MmCB9v64o4yojNluJhVm9nQ==
|
||||
|
||||
figures@^3.0.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
|
||||
|
||||
Reference in New Issue
Block a user