-
[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
받았다면, 소스경로내 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)
이것으로 ffmpeg.wasm을 알아보았다. 이슈만 해결된다면 상당히 매력적인 라이브러리가 될 수 있을 것이다.
반응형'Javascript & TypeScript' 카테고리의 다른 글
[Svelte] 스벨트에서의 event capture와 bubbling (0) 2022.12.17 [React] ffmpeg.wasm 메모리 누수 문제 해결 (1) 2021.05.08 [Javascript Event] Drop 이벤트에 preventDefault, stopPropagation를 적용해도 동작하지 않는 현상 해결 (0) 2021.04.27 [React] React Router를 적용해도 첫 메인페이지("/") 위치만 표시되는 현상 해결법 (0) 2021.04.26 [gRPC] nodeJS 에선 google.protobuf.Any 가 지원되지 않는다. (3) 2021.03.01