rework web compression a bit

This commit is contained in:
Samuel Newman
2024-06-12 20:33:30 +01:00
parent e2223fe3ed
commit 527e800d01
4 changed files with 99 additions and 83 deletions
+17 -3
View File
@@ -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}
} }
} }
+74 -59
View File
@@ -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 (
+6 -19
View File
@@ -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,