Video processing in browser using FFmpeg.wasm and Solidjs

Harsh Mangalam - May 7 '22 - - Dev Community

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 



Enter fullscreen mode Exit fullscreen mode

Add Hope ui in solidjs project



> pnpm add @hope-ui/solid @stitches/core solid-transition-group



Enter fullscreen mode Exit fullscreen mode

Add FFmpeg.wasm dependency



> pnpm add @ffmpeg/ffmpeg



Enter fullscreen mode Exit fullscreen mode

Let's create useFFmpeg.jsx file inside src/hooks.



import { createStore } from "solid-js/store";
import { onCleanup } from "solid-js";
import { createFFmpeg, fetchFile } from "@ffmpeg/ffmpeg";

export default function useFFmpeg() {
  const [store, setStore] = createStore({
    progress: null, 
    videoURL: null,
  });

  const ffmpeg = createFFmpeg({ progress: (e) => setStore("progress", e) });

  const transcode = async (file) => {
    const { name } = file;
    // load ffmpeg.wasm code 
    await ffmpeg.load();
    // write file to  memory filesystem 
    ffmpeg.FS("writeFile", name, await fetchFile(file));
    // convert video into mp4 
    await ffmpeg.run("-i", name, "output.mp4");
    // read file from Memory filesystem 
    const data = ffmpeg.FS("readFile", "output.mp4");
    const url = URL.createObjectURL(
      new Blob([data.buffer], { type: "video/mp4" })
    );

    setStore("videoURL", url);
    setStore("progress", null);
  };

  const handleFileChange = (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,
  };
}




Enter fullscreen mode Exit fullscreen mode

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";
import useFFmpeg from "./hooks/useFFmpeg";
import { Show } from "solid-js";
function App() {
  let fileRef;
  const { store, handleFileChange } = useFFmpeg();
  return (
    <HopeProvider config={{ initialColorMode: "dark" }}>
      <Container minH={"100vh"} display="grid" placeItems={"center"}>
        <Box py={"$4"}>
          <Heading fontSize={"$4xl"} textAlign="center">
            Video processing in browser using
            <Box>
              <Text as="span" color={"$success10"}>
                FFmpeg{" "}
              </Text>
              and{" "}
              <Text as="span" color={"$primary10"}>
                SolidJS
              </Text>
            </Box>
          </Heading>

          <HStack justifyContent={"center"} mt={"$6"}>
            <input
              type="file"
              name="file"
              id="file"
              hidden
              onChange={[handleFileChange]}
              ref={fileRef}
            />
            <Button onClick={() => fileRef.click()}>Select Video File</Button>
          </HStack>

          <Show when={store.progress}>
            <SimpleGrid columns={{ "@initial": 1, "@sm": 2, "@md": 2,"@lg":3 }} justifyContent={"center"} alignItems="center" mt={"$6"} spacing={"$4"}>
              <GridItem mx="auto">
                <CircularProgress
                  value={Math.round(store.progress?.ratio * 100)}
                  size={"$52"}
                >
                  <CircularProgressIndicator color="$success10" />
                  <CircularProgressLabel>
                    <VStack spacing={"$2"}>
                      <Heading fontSize={"$xl"}>Ratio</Heading>
                      <Heading fontSize={"$3xl"}>
                        {Math.round(store.progress?.ratio * 100)} %
                      </Heading>
                    </VStack>
                  </CircularProgressLabel>
                </CircularProgress>
              </GridItem>

              <GridItem mx="auto">
                <CircularProgress value={100} size={"$52"} thickness={"$1"}>
                  <CircularProgressIndicator color="$success10" />
                  <CircularProgressLabel>
                    <VStack spacing={"$2"}>
                      <Heading fontSize={"$xl"}>Duration</Heading>
                      <Heading fontSize={"$3xl"}>
                        {store.progress?.duration}
                      </Heading>
                    </VStack>
                  </CircularProgressLabel>
                </CircularProgress>
              </GridItem>

              <GridItem mx="auto">
                <CircularProgress size={"$52"} indeterminate>
                  <CircularProgressIndicator color="$primary10" />
                  <CircularProgressLabel>
                    <VStack spacing={"$2"}>
                      <Heading fontSize={"$xl"}>Time</Heading>
                      <Heading fontSize={"$3xl"}>{store.progress?.time}</Heading>
                    </VStack>
                  </CircularProgressLabel>
                </CircularProgress>
              </GridItem>
            </SimpleGrid>
          </Show>

          <Show when={store.videoURL}>
            <Flex justifyContent={"center"} mt={"$6"}>
              <video
                src={store.videoURL}
                width={"400px"}
                height={"400px"}
                autoPlay
                controls
              />
            </Flex>
          </Show>

        </Box>



      </Container>
    </HopeProvider>
  );
}

export default App;




Enter fullscreen mode Exit fullscreen mode

In last we will configure vite to handle cross origin isolation by adding Cross-Origin-Embedder-Policy and
Cross-Origin-Opener-Policy headers




import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";

export default defineConfig({
  plugins: [
    solidPlugin(),
    {
      name: "configure-response-headers",
      configureServer: (server) => {
        server.middlewares.use((_req, res, next) => {
          res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
          res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
          next();
        });
      },
    },
  ],
  build: {
    target: "esnext",
    polyfillDynamicImport: false,
  },
});





Enter fullscreen mode Exit fullscreen mode

Now start your dev server and select any video file.



pnpm run dev



Enter fullscreen mode Exit fullscreen mode

Home

Video

GitHub logo harshmangalam / ffmpeg-solidjs

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.

$ npm install # or pnpm install or yarn install
Enter fullscreen mode Exit fullscreen mode

Learn more on the Solid Website and come chat with us on our Discord

Available Scripts

In the project directory, you can run:

npm dev or npm start

Runs the app in the development mode.
Open http://localhost:3000 to view it in the browser.

The page will reload if you make edits.

npm run build

Builds the app for production to the dist folder.
It correctly bundles Solid in production mode and optimizes the build for the best performance.

The build is minified and the filenames include the hashes.
Your app is ready to be deployed!

Deployment

You can…

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .