ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Javascript] 브라우저에서 사용하는 인코딩 및 디코딩 패키지 ffmpeg.wasm 소개
    Javascript & TypeScript 2021. 5. 5. 18:10
    반응형

    미디어파일을 인코딩/디코딩 하는 작업을 구현하는 프로그래머라면 ffmpeg란 오픈소스 툴을 들어본적이 있을 것이다. 카카오 팟플레이어 곰플레이어 등, 상당수의 미디어 플레이어나 인코딩 프로그램에 직/간접적으로 사용되는만큼 파급력이 높다. CLI 명령어 기반이라 이를 편히 사용할 수 있는 UI 기반 툴도 많이 나와있다.

     

    그러나 특성상 웹에서 사용은 어려웠다. 물론 호스팅을 통해 미디어파일을 인코딩/디코딩 서비스를 제공하는 웹사이트도 심심찮게 볼 수 있었겠지만, 어디까지나 내 미디어파일이 웹서버에 업로드되어 처리한 후, 결과를 내려받는 형식이기 때문에 함부로 이용하면 소중한 지적재산권(?)을 내 손으로 넘겨주는 꼴이 될 수 있다.

     

    이와 같은 불상사를 방지할 수 있는 패키지. ffmpeg.wasm를 소개한다.

     

    1. ffmpeg.wasm란?

    ffmpeg.wasm은 웹어샘블리(wasm)로 포팅된 ffmpeg를 javasciprt 에서 사용할 수 있도록 제공하는 인터페이스 형식의 패키지이다. Github에 MIT 라이선스로 배포되어있어, 별도의 법령에 얽메이지 않고 사용가능하다.

     

    ffmpeg가 LGPL/GPL 라이선스를 바탕으로 오픈되어있다는걸 안다면 이 부분에서 의아할 수 있는데, wasm 포팅 및 관련 도구(ffmpeg.wasm-core)는 따로 존재하며, 이번에 소개하는 ffmpeg.wasm는 https://unpkg.com에 등록된 wasm를 사용한다.

     

    2. 사용방법(with React Typescript)

    사용관련 예제는 ffmpegwasm.github.io에서도 참조가능하나 응용하기엔 조금 부족한 부분이 있다. 따라서 별도작성한 예제로 설명하겠다. 아래 소스코드를 받은 뒤, npm i 명령어로 관련 패키지를 설치한다.

     

    github.com/ddochea0314/example-ffmpeg-wasm

     

    ddochea0314/example-ffmpeg-wasm

    wasm 기반 ffmpeg를 사용하는 인터페이스 패키지 ffmpeg.wasm react 사용 예제를 정리하기 위한 코드 - ddochea0314/example-ffmpeg-wasm

    github.com

    받았다면, 소스경로내 src/views를 확인해보라. 각 예제코드를 구현했다. Example1은 ffmpeg.wasm 기본 react 예제와 크게 차이점 없으므로 Example2로 넘어간다.

     

    Example2 전체소스

    import { createFFmpeg } from "@ffmpeg/ffmpeg";
    import React, { useState, useRef } from "react";
    
    function Mp3CoverImport(): JSX.Element {
      const fileCoverHtml = useRef<HTMLInputElement>(null);
      const fileMp3Html = useRef<HTMLInputElement>(null);
    
      const [message, setMessage] = useState("Click Start to import");
      const [downloadLink, setDownloadLink] = useState("");
      const ffmpeg = createFFmpeg({
        log: true,
      });
    
      const getFileExtension = (file: File) => file.name.split(".")[1];
    
      const getFile = (file: React.RefObject<HTMLInputElement>) => {
        if (file.current && file.current.files && file.current.files.length !== 0) {
          return file.current.files[0];
        } else {
          return null;
        }
      };
    
      const doImport = async () => {
        setMessage("Loading ffmpeg-core.js");
        await ffmpeg.load();
        const cover = getFile(fileCoverHtml);
        const mp3 = getFile(fileMp3Html);
        if (cover && mp3) {
          const coverName = `test.${getFileExtension(cover)}`;
          ffmpeg.FS(
            "writeFile",
            coverName,
            new Uint8Array(await cover.arrayBuffer())
          );
          ffmpeg.FS(
            "writeFile",
            "test.mp3",
            new Uint8Array(await mp3.arrayBuffer())
          );
          setMessage("Start Import");
          const args = [
            "-i",
            "test.mp3",
            "-i",
            coverName,
            "-c:a",
            "copy",
            "-c:v",
            "copy",
            "-map",
            "0:0",
            "-map",
            "1:0",
            "-id3v2_version",
            "3",
            "output.mp3",
          ]; // 명령어와 파라메터가 반드시 분할되어 입력해야한다. "-c:a copy -c:v copy" 처럼 묶으면 정상 동작하지 않는다.
          await ffmpeg.run(...args);
          setMessage("Complete Import");
          const data = ffmpeg.FS("readFile", "output.mp3");
          URL.revokeObjectURL(downloadLink);
          setDownloadLink(
            URL.createObjectURL(new Blob([data.buffer], { type: "audio/mp3" }))
          );
        } else {
          setMessage("Can not Import. need file check. 😪");
        }
      };
    
      return (
        <div>
          <h1>Example 2. Import Image mp3 cover</h1>
          <p />
          <label htmlFor="img">image </label>
          <input
            ref={fileCoverHtml}
            id="img"
            type="file"
            accept=".png,.jpg,.jpeg"
          />
          <label htmlFor="mp3">mp3 </label>
          <input ref={fileMp3Html} id="mp3" type="file" accept=".mp3" />
          <p />
          <button onClick={doImport}>Start</button>
          <p>{message}</p>
          {downloadLink.length !== 0 && (
            <a href={downloadLink} download="result.mp3">
              download
            </a>
          )}
        </div>
      );
    }
    
    export default Mp3CoverImport;
    

    전체 소스코드에서 중요한 부분은 ffmpeg.FS와 ffmpeg.run 를 사용하는 doImport() 부분이다. ffmpeg.FS는 nodeJS의 fs 와 같은 역할이라고 생각하면 이해하기 쉽다.

     

    아래 코드는 ffmpeg.wasm에서 파일을 사용할 수 있도록 웹어셈블리에 버퍼를 쓰는 코드이다. ffmpeg 작업관련한 파일들은 전부 FS 함수의 "writeFile" 작업을 해줘야 run 함수에서 argument로 사용가능하다.

    ffmpeg.FS("writeFile", coverName, new Uint8Array(await cover.arrayBuffer()));

    작업할 파일을 모두 등록했다면 run을 호출하여 작업을 실행시킬 수 있다. 주석에도 적혀있겠지만, args 값에 들어가는  ffmpeg의 명령어와 값이 고정값이라 할지라도, 묶어서 입력하면 동작하지 않는다.

          const args = [
            "-i",
            "test.mp3",
            "-i",
            coverName,
            "-c:a",
            "copy",
            "-c:v",
            "copy",
            "-map",
            "0:0",
            "-map",
            "1:0",
            "-id3v2_version",
            "3",
            "output.mp3",
          ]; // 명령어와 파라메터가 반드시 분할되어 입력해야한다. "-c:a copy -c:v copy" 처럼 묶으면 정상 동작하지 않는다.
          await ffmpeg.run(...args);

     

    완료된 결과물은 "readFile" 을 통해 javascript 객체로 가져올 수 있다.

    const data = ffmpeg.FS("readFile", "output.mp3");

    받은 data 변수는 buffer 및 Blob을 이용하여 사용가능하다. 예제는 결과물을 Blob URL로 만들어 다운로드 받을수 있게 해준다.

     

    서버가 필요없기 때문에 firebase 에서 호스팅이 가능하며, 트래픽에 대해서도 걱정이 없어 아주 마음에 드는 라이브러리지만 한가지 큰 문제가 존재한다.

     

    반응형

    3. 현재 발견된 이슈

    2021.05.05 기준 npm에 올라온 ffmpeg의 최신버전(v0.9.8)은 메모리 누수가 심각하며, 이를 해제할 수 있는 로직이 구현되어있지 않은 상황이다. 원인은 run에서 작업 진행과 함께 생성되는 blob 및 webWorker 가 지워지지 않는 현상으로 보여진다.

    * FS 함수의 unlink 안써서 그런거 아니냐고 할 수도 있는데, 사용유무에 관계없이 누수가 발생했다.

    * 메모리 누수문제는 코딩 실수로 인한 문제였다. 자세한 내용은 ddochea.tistory.com/149 작성했다.

     

    쌓여만 가고 지울 수 없는 상황

    스크린샷에선 많아보이지 않지만, 메모리 스냅샷을 찍어보면 한번 작업을 수행할 때마다 메모리가 GB단위로 증가한다. 새로고침을 쓰면 메모리가 정리되지만, 이 방법만으로 정식 서비스에 도입하긴 어려울 듯 하다.

     

    관련해서는 이슈로 올라온 상태이다.

    * 메모리 누수는 개발 실수로 인한 사항이다. 그러나 worker thread 증가문제는 존재하긴 한다.

     

    Should worker threads exit? · Issue #136 · ffmpegwasm/ffmpeg.wasm (github.com)

     

    Should worker threads exit? · Issue #136 · ffmpegwasm/ffmpeg.wasm

    Describe the bug In the Chrome debugger I can see the worker threads from ffmpeg when the ffmpeg command has finished. I expected them to exit (but I really have no idea). To Reproduce Steps to rep...

    github.com

    이것으로 ffmpeg.wasm을 알아보았다. 이슈만 해결된다면 상당히 매력적인 라이브러리가 될 수 있을 것이다.

    반응형

    댓글

Designed by Tistory.