Video processing in browser using FFmpeg.wasm and Solidjs
Harsh Mangalam
Posted on May 7, 2022
Hi, Developers in this blog post i will show you how you can utilize ffmpeg in browser using their WASM binding with SolidJS.
According to WASM
WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.
FFmpeg
FFmpeg is a cross-platform solution to record, convert and stream audio and video.
FFmpeg.wasm
ffmpeg.wasm is a pure Webassembly / Javascript port of FFmpeg. It enables video & audio record, convert and stream right inside browsers.
SolidJS
Solidjs is a Reactive, Performant, Powerful, Pragmatic and Productive Javascript library.
In this post we will create a hook useFFmpeg hook that will handle to convert a video file into mp4 and play inside browser. We will design this web app using Hope ui.
Create new solidjs project and install dependencies.
> npx degit solidjs/templates/js ffmpeg-solidjs
> cd ffmpeg-solidjs
> pnpm i
> pnpm run dev
import{createStore}from"solid-js/store";import{onCleanup}from"solid-js";import{createFFmpeg,fetchFile}from"@ffmpeg/ffmpeg";exportdefaultfunctionuseFFmpeg(){const[store,setStore]=createStore({progress:null,videoURL:null,});constffmpeg=createFFmpeg({progress:(e)=>setStore("progress",e)});consttranscode=async (file)=>{const{name}=file;// load ffmpeg.wasm code awaitffmpeg.load();// write file to memory filesystem ffmpeg.FS("writeFile",name,awaitfetchFile(file));// convert video into mp4 awaitffmpeg.run("-i",name,"output.mp4");// read file from Memory filesystem constdata=ffmpeg.FS("readFile","output.mp4");consturl=URL.createObjectURL(newBlob([data.buffer],{type:"video/mp4"}));setStore("videoURL",url);setStore("progress",null);};consthandleFileChange=(e)=>{// start video conversion on file change transcode(e.target.files[0]);};onCleanup(()=>{// revoke created blob url object URL.revokeObjectURL(store.videoURL)})return{store,handleFileChange,};}
createFFmpeg() function will init ffmpeg and handle logging all the details during video manipulation , track progress of video manipulation etc..
Video progress will be stored in a reactive solidjs store so that we can utilize this store to do reactive stuff.
ffmpeg.load() is an async method that will download core ffmpeg and init ffmpeg wasm binary.
ffmpeg.FS() is a helper method that will help to work around memory filesystem in browser.
URL.createObjectURL() static method create blob object url that you can directly use inside html native element like <video>
After creating blob object url of video we will store inside reactive store so that we can utilize this to play video.
when component will unmount we will clear blob object occupied memory using URL.revokeObjectURL() static method.
Now we will connect useFFmpeg hook inside App.jsx.
import{Box,Button,CircularProgress,CircularProgressIndicator,CircularProgressLabel,Container,Flex,GridItem,Heading,HopeProvider,HStack,SimpleGrid,Text,VStack,}from"@hope-ui/solid";importuseFFmpegfrom"./hooks/useFFmpeg";import{Show}from"solid-js";functionApp(){letfileRef;const{store,handleFileChange}=useFFmpeg();return (<HopeProviderconfig={{initialColorMode:"dark"}}><ContainerminH={"100vh"}display="grid"placeItems={"center"}><Boxpy={"$4"}><HeadingfontSize={"$4xl"}textAlign="center">
Video processing in browser using
<Box><Textas="span"color={"$success10"}>
FFmpeg{""}</Text>
and{""}<Textas="span"color={"$primary10"}>
SolidJS
</Text></Box></Heading><HStackjustifyContent={"center"}mt={"$6"}><inputtype="file"name="file"id="file"hiddenonChange={[handleFileChange]}ref={fileRef}/><ButtononClick={()=>fileRef.click()}>Select Video File</Button></HStack><Showwhen={store.progress}><SimpleGridcolumns={{"@initial":1,"@sm":2,"@md":2,"@lg":3}}justifyContent={"center"}alignItems="center"mt={"$6"}spacing={"$4"}><GridItemmx="auto"><CircularProgressvalue={Math.round(store.progress?.ratio*100)}size={"$52"}><CircularProgressIndicatorcolor="$success10"/><CircularProgressLabel><VStackspacing={"$2"}><HeadingfontSize={"$xl"}>Ratio</Heading><HeadingfontSize={"$3xl"}>{Math.round(store.progress?.ratio*100)} %
</Heading></VStack></CircularProgressLabel></CircularProgress></GridItem><GridItemmx="auto"><CircularProgressvalue={100}size={"$52"}thickness={"$1"}><CircularProgressIndicatorcolor="$success10"/><CircularProgressLabel><VStackspacing={"$2"}><HeadingfontSize={"$xl"}>Duration</Heading><HeadingfontSize={"$3xl"}>{store.progress?.duration}</Heading></VStack></CircularProgressLabel></CircularProgress></GridItem><GridItemmx="auto"><CircularProgresssize={"$52"}indeterminate><CircularProgressIndicatorcolor="$primary10"/><CircularProgressLabel><VStackspacing={"$2"}><HeadingfontSize={"$xl"}>Time</Heading><HeadingfontSize={"$3xl"}>{store.progress?.time}</Heading></VStack></CircularProgressLabel></CircularProgress></GridItem></SimpleGrid></Show><Showwhen={store.videoURL}><FlexjustifyContent={"center"}mt={"$6"}><videosrc={store.videoURL}width={"400px"}height={"400px"}autoPlaycontrols/></Flex></Show></Box></Container></HopeProvider>);}exportdefaultApp;
In last we will configure vite to handle cross origin isolation by adding Cross-Origin-Embedder-Policy and Cross-Origin-Opener-Policy headers
Video processing in browser using FFmpeg and SolidJS
Usage
Those templates dependencies are maintained via pnpm via pnpm up -Lri.
This is the reason you see a pnpm-lock.yaml. That being said, any package manager will work. This file can be safely be removed once you clone a template.