rework web compression a bit
This commit is contained in:
@@ -7,6 +7,11 @@ import {
|
|||||||
|
|
||||||
const PRESET = 'faster'
|
const PRESET = 'faster'
|
||||||
|
|
||||||
|
export type CompressedVideo = {
|
||||||
|
uri: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
export async function compressVideo(
|
export async function compressVideo(
|
||||||
file: string,
|
file: string,
|
||||||
callbacks?: {
|
callbacks?: {
|
||||||
@@ -30,9 +35,18 @@ export async function compressVideo(
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await FileSystem.deleteAsync(file)
|
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 {
|
return {
|
||||||
uri: success ? newFile : null,
|
success,
|
||||||
|
video: {uri: newFile, size: res.size} as CompressedVideo,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Could not find output video')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {success: false}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,71 +56,86 @@ export async function compressVideo(
|
|||||||
if (!ctx) throw new Error('Could not get canvas context')
|
if (!ctx) throw new Error('Could not get canvas context')
|
||||||
ctx.fillStyle = '#fff'
|
ctx.fillStyle = '#fff'
|
||||||
|
|
||||||
console.log('canvas', canvas)
|
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)
|
||||||
|
|
||||||
let wasTruncated = false
|
recorder.onerror = console.log
|
||||||
const videoBlob = await new Promise<Blob>(async resolve => {
|
recorder.ondataavailable = e => {
|
||||||
const chunks: Blob[] = []
|
let size = chunks.reduce((acc, chunk) => acc + chunk.size, 0)
|
||||||
let options = {
|
if (size + e.data.size > MAX_VIDEO_SIZE) {
|
||||||
mimeType: 'video/webm;codecs=h264',
|
wasTruncated = true
|
||||||
videoBitsPerSecond: 200000,
|
recorder.stop()
|
||||||
}
|
} else {
|
||||||
const recorder = new MediaRecorder(canvas.captureStream(25), options)
|
chunks.push(e.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recorder.onstop = () => {
|
||||||
|
resolve(new Blob(chunks, {type: recorder.mimeType}))
|
||||||
|
}
|
||||||
|
|
||||||
recorder.onerror = console.log
|
videoEl.play()
|
||||||
recorder.ondataavailable = e => {
|
recorder.start()
|
||||||
let size = chunks.reduce((acc, chunk) => acc + chunk.size, 0)
|
|
||||||
if (size + e.data.size > MAX_VIDEO_SIZE) {
|
let lastCapture = Date.now()
|
||||||
wasTruncated = true
|
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()
|
recorder.stop()
|
||||||
} else {
|
|
||||||
chunks.push(e.data)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
recorder.onstop = () => {
|
|
||||||
resolve(new Blob(chunks, {type: recorder.mimeType}))
|
if (wasTruncated) {
|
||||||
|
Toast.show('Video was too long and was truncated')
|
||||||
}
|
}
|
||||||
|
|
||||||
videoEl.play()
|
return {
|
||||||
recorder.start()
|
uri: URL.createObjectURL(videoBlob),
|
||||||
|
|
||||||
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') {
|
} catch (err) {
|
||||||
recorder.stop()
|
console.error(err)
|
||||||
}
|
Toast.show('Failed to compress video')
|
||||||
})
|
} finally {
|
||||||
|
URL.revokeObjectURL(objectUrl)
|
||||||
if (wasTruncated) {
|
}
|
||||||
Toast.show('Video was too long and was truncated')
|
}
|
||||||
}
|
|
||||||
|
function getSupportedMimeType() {
|
||||||
URL.revokeObjectURL(objectUrl)
|
if (MediaRecorder.isTypeSupported('video/mp4;codecs=h264')) {
|
||||||
|
return 'video/mp4;codecs=h264'
|
||||||
return {
|
} else if (MediaRecorder.isTypeSupported('video/webm;codecs=h264')) {
|
||||||
uri: URL.createObjectURL(videoBlob),
|
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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import * as FileSystem from 'expo-file-system'
|
|
||||||
|
|
||||||
|
import {CompressedVideo} from '#/lib/media/video/compress'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ export function VideoPreview({
|
|||||||
video,
|
video,
|
||||||
clear,
|
clear,
|
||||||
}: {
|
}: {
|
||||||
video: FileSystem.FileInfo
|
video: CompressedVideo
|
||||||
clear: () => void
|
clear: () => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
import * as FileSystem from 'expo-file-system'
|
|
||||||
import {ImagePickerAsset} from 'expo-image-picker'
|
import {ImagePickerAsset} from 'expo-image-picker'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -13,6 +12,10 @@ export function useVideoState({setError}: {setError: (error: string) => void}) {
|
|||||||
|
|
||||||
const {mutate, data, isPending, isError, reset, variables} = useMutation({
|
const {mutate, data, isPending, isError, reset, variables} = useMutation({
|
||||||
mutationFn: async (asset: ImagePickerAsset) => {
|
mutationFn: async (asset: ImagePickerAsset) => {
|
||||||
|
console.log(
|
||||||
|
'uncompressed size',
|
||||||
|
((asset.fileSize ?? 0) / 1024 / 1024).toFixed(2) + 'mb',
|
||||||
|
)
|
||||||
const compressed = await compressVideo(asset.uri, {
|
const compressed = await compressVideo(asset.uri, {
|
||||||
onProgress: progressMs => {
|
onProgress: progressMs => {
|
||||||
if (asset.duration) {
|
if (asset.duration) {
|
||||||
@@ -20,24 +23,8 @@ export function useVideoState({setError}: {setError: (error: string) => void}) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!compressed.uri) {
|
|
||||||
throw new Error('Failed to compress video')
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await FileSystem.getInfoAsync(compressed.uri, {size: true})
|
return compressed
|
||||||
if (res.exists) {
|
|
||||||
console.log(
|
|
||||||
'uncompressed size',
|
|
||||||
(asset.fileSize! / 1024 / 1024).toFixed(2) + 'mb',
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
'compressed size',
|
|
||||||
(res.size / 1024 / 1024).toFixed(2) + 'mb',
|
|
||||||
)
|
|
||||||
return res
|
|
||||||
} else {
|
|
||||||
throw new Error('Could not find output video')
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
console.error('error', error)
|
console.error('error', error)
|
||||||
@@ -49,7 +36,7 @@ export function useVideoState({setError}: {setError: (error: string) => void}) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
video: data,
|
video: data?.video,
|
||||||
onSelectVideo: mutate,
|
onSelectVideo: mutate,
|
||||||
videoPending: isPending,
|
videoPending: isPending,
|
||||||
videoProcessingData: variables,
|
videoProcessingData: variables,
|
||||||
|
|||||||
Reference in New Issue
Block a user