From 527e800d01d919825de70c5a2871cec02b7de2a9 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 12 Jun 2024 20:33:30 +0100 Subject: [PATCH] rework web compression a bit --- src/lib/media/video/compress.ts | 20 ++- src/lib/media/video/compress.web.ts | 133 ++++++++++-------- src/view/com/composer/videos/VideoPreview.tsx | 4 +- src/view/com/composer/videos/state.ts | 25 +--- 4 files changed, 99 insertions(+), 83 deletions(-) diff --git a/src/lib/media/video/compress.ts b/src/lib/media/video/compress.ts index b9b431ad44..9564dc4f8e 100644 --- a/src/lib/media/video/compress.ts +++ b/src/lib/media/video/compress.ts @@ -7,6 +7,11 @@ import { const PRESET = 'faster' +export type CompressedVideo = { + uri: string + size: number +} + export async function compressVideo( file: string, callbacks?: { @@ -30,9 +35,18 @@ export async function compressVideo( 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 { - uri: success ? newFile : null, + return { + success, + video: {uri: newFile, size: res.size} as CompressedVideo, + } + } else { + throw new Error('Could not find output video') + } + } else { + return {success: false} } } diff --git a/src/lib/media/video/compress.web.ts b/src/lib/media/video/compress.web.ts index a10474dfba..533be3cbf9 100644 --- a/src/lib/media/video/compress.web.ts +++ b/src/lib/media/video/compress.web.ts @@ -56,71 +56,86 @@ export async function compressVideo( if (!ctx) throw new Error('Could not get canvas context') ctx.fillStyle = '#fff' - console.log('canvas', canvas) + try { + let wasTruncated = false + const videoBlob = await new Promise(async resolve => { + const chunks: Blob[] = [] + let options = { + mimeType: getSupportedMimeType(), + videoBitsPerSecond: 200000, + } + const recorder = new MediaRecorder(canvas.captureStream(25), options) - let wasTruncated = false - const videoBlob = await new Promise(async resolve => { - const chunks: Blob[] = [] - let options = { - mimeType: 'video/webm;codecs=h264', - 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})) + } - 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 + 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() - } 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() - 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() - } + return { + uri: URL.createObjectURL(videoBlob), } - if (recorder.state === 'recording') { - recorder.stop() - } - }) - - if (wasTruncated) { - Toast.show('Video was too long and was truncated') - } - - URL.revokeObjectURL(objectUrl) - - 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') } } diff --git a/src/view/com/composer/videos/VideoPreview.tsx b/src/view/com/composer/videos/VideoPreview.tsx index 7a9fa0e25e..63afbc298b 100644 --- a/src/view/com/composer/videos/VideoPreview.tsx +++ b/src/view/com/composer/videos/VideoPreview.tsx @@ -1,6 +1,6 @@ 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 {Text} from '#/components/Typography' @@ -8,7 +8,7 @@ export function VideoPreview({ video, clear, }: { - video: FileSystem.FileInfo + video: CompressedVideo clear: () => void }) { return ( diff --git a/src/view/com/composer/videos/state.ts b/src/view/com/composer/videos/state.ts index 52270f7a75..60fbf69ec4 100644 --- a/src/view/com/composer/videos/state.ts +++ b/src/view/com/composer/videos/state.ts @@ -1,5 +1,4 @@ import {useState} from 'react' -import * as FileSystem from 'expo-file-system' import {ImagePickerAsset} from 'expo-image-picker' import {msg} from '@lingui/macro' 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({ 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) { @@ -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}) - 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') - } + return compressed }, onError: error => { console.error('error', error) @@ -49,7 +36,7 @@ export function useVideoState({setError}: {setError: (error: string) => void}) { }) return { - video: data, + video: data?.video, onSelectVideo: mutate, videoPending: isPending, videoProcessingData: variables,