Javascript/Typescript

[이슈] MediaRecorder(Web API)로 촬영한 webm 영상이 정상 재생되지 않음

범데이 2022. 9. 30. 01:12

1. 개요

node.js환경의 vue-electron으로 윈도우즈 프로그램을 개발하는 프로젝트 중의 일이다.

빌드 된 프로그램의 Chromium기반 웹페이지에서 Web API 내장 라이브러리인 MediaRecorder로 영상을 촬영한 후, 클라우드에 업로드 한 뒤 링크를 따서 외부 프로그램에서 영삿 재생이 바로 가능하도록 구현해야 했다.

하지만 웹에서 스트림 형식으로 촬영하고 처리한 후 생성한 영상에 여럿 까다로운 문제가 발생할 줄 몰랐으니.. 이를 파악하고 처리했던 히스토리를 공유하고자 포스팅으로 작성해본다.

2. 영상 처리 로직

처음에 MediaRecorder 인스턴스를 아래와 같이 초기화 한 후,

// prompts the user for permission to use a media input which produces a 
// MediaStream  with tracks containing the requested types of media.
const stream = await navigator.mediaDevices.getUserMedia({...});

// Init MediaRecorder
this.mediaRecorder = new MediaRecorder(stream, {mimeType: "video/webm;codecs=vp9");



촬영 중에는 buffer형태의 데이터를 담는다.

// Regist ondataavailable event handler function
this.mediaRecorder.ondataavailable = this.handleDataAvailable;

...

handleDataAvailable(e: any) {
  console.log("video data available");
  if (e.data.size > 0) {
    this.recordedChunks.push(e.data);
  }
}



촬영이 종료되면 담은 buffer데이터를 blob타입으로 변환한 후, fs모듈을 사용해서 영상 파일로 떨군다.

// Regist onstop event handler function
this.mediaRecorder.onstop = this.handleOnStop;

async handleOnStop(e: any) {
  console.log("recording stopped!");

  ...

  try {
    const blob = new Blob(this.recordedChunks, {
      type: "video/webm;codecs=vp9",
    });

	blob.arrayBuffer().then((arrayBuffer: ArrayBuffer) => {
      const buffer = Buffer.from(arrayBuffer);
			
      fs.writeFile("video.webm", buffer, async (err: any) => {
        err ? console.error(err) : console.log(`video.webm write successed!`);
        ...
    } catch (err) {
      console.error("handleOnStop >> err=", err);
	  ...
    }
  }
  ...


이렇게해서 생성된 영상 파일은 firebase storage에 업로드 되고, 업로드 된 영상의 링크를 다른 프로그램이 받아서 바로 재생이 가능해야 했다.



3. 문제 발생1. non-seekable(탐색 불가)

3.1 문제 내용

다른 프로그램에서 영상의 링크를 받아 재생하는데, 재생 구간 탐색이 불가능 했다. 처음부터 흘러가는 영상을 끝까지 시청 하는건 가능했으나, 어떤 문제로 인해 구간 건너뛰기가 안되었는지 검토하게 되었다.
해외 포럼(링크1, 링크2)들에서 마찬가지로 webm영상의 구간 건너뛰기가 안되는 특성에 대해 다루고 있었다.

이는 webm의 특성상, 스트림 형태로 실시간 녹화가 이루어지다 보니, 헤더가 별도로 작성되기 전까지는 영상의 기간에 대한 메타데이터가 저장되지 않았다.

3.2 문제 해결

ts-ebml 라이브러리를 사용하여 지속시간과 같은 메타데이터를 추가해주어 검색 가능한 동영상으로 변환해 주었다.

아래와 같이 ts-ebml라이브러리를 import하고, 메서드를 구현해서, 기존 video blob 데이터에 duration과 같은 영상의 메타데이터를 삽입해 주었다.

import * as ebml from "ts-ebml";

const decoder = new ebml.Decoder();
const reader = new ebml.Reader();
const ebmlTools = ebml.tools;

async makeSeekableVideoBlobFromBlob(_blob: Blob, duration: number) {
    const buffer = Buffer.from(await _blob.arrayBuffer());

    reader.logging = true;
    reader.drop_default_duration = false;

    const elms = decoder.decode(buffer);
    elms.forEach((elm) => {
      reader.read(elm);
    });
    reader.stop();

    const refinedMetadataBuf = ebmlTools.makeMetadataSeekable(reader.metadatas, duration, reader.cues);
    const body = buffer.slice(reader.metadataSize);
    const refined = new Buffer(ebmlTools.concat([new Buffer(refinedMetadataBuf), body]).buffer);

    const blob = new Blob([refined]);

    return blob;
}


// call upper method
const processedVideoBlob = await videoProcessor.makeSeekableVideoBlobFromBlob(videoBlob, duration);


이후, 링크로 영상을 가져와 구간 탐색이 불가능했던 것이, 구간 탐색이 가능하게 해결되었다.



4. 문제 발생2. IOS단말기에서 재생이 되지 않음

4.1 문제 내용

IOS에서 해당 링크의 영상이 아예 나오지 않는 현상이 발생했다. 영상 대신에 배경 화면만 나올 뿐이었다.
이에 대한 원인은 찾지 못하였고, 동영상을 아예 mp4형식으로 변환해서 클라우드에 저장하는 방향으로 가게 되었다.

4.2 문제 해결

영상을 mp4로 변환하기 위해 사용한 라이브러리는 다음과 같다: fluent-ffmpeg, @ffmpeg-installer/ffmpeg

*fluent-ffmpeg: 영상을 변환하기 위한 ffmpeg 커맨드를 node.js에서 다룰수 있도록 제공
*@ffmpeg-installer/ffmpeg: 현재 플랫폼에 대한 ffmpeg 바이너리를 설치하고 경로와 버전을 제공.



아래의 코드를 살펴보면, 영상 변환에 필요한 라이브러리들을 import해준 후, fluent-ffmpegsetFfmpegPath() 메서드로 ffmpeg명령어를 실행할 파일의 경로를 설정해준다.

const ffmpegPath = window.require("@ffmpeg-installer/ffmpeg")
	.path.replace("app.asar", "app.asar.unpacked");
const ffmpeg = window.require("fluent-ffmpeg");
ffmpeg.setFfmpegPath(ffmpegPath);


그런다음 위의 3-2에서 사용하였던 변환 방식은 일단 빼두고, ffmpeg 라이브러리의 가이드에 따라 format을 mp4로 설정해주어, 커맨드를 실행하게 해준다. 파일을 저장하는데에 성공하였으면, 그 파일을 fs모듈로 읽어, blob형태로 선언 후 클라우드에 업로드해준다.

...
blob.arrayBuffer().then((arrayBuffer: ArrayBuffer) => {
    const buffer = Buffer.from(arrayBuffer);

    fs.writeFile(fileNameWebm, buffer, async (err: any) => {
    err ? console.error(err) : console.log(`${fileNameWebm} write successed!`);

    var command = ffmpeg(fileNameWebm).format("mp4");
    command
      .save(fileNameMp4)
      .on("end", () => {
        console.log(`video save successed! file: ${fileNameMp4}`);

        fs.readFile(fileNameMp4, (err: any, data: any) => {
          if (err) {
            console.error(`read video failure! file: ${fileNameMp4}`, err);
            return;
          } else {
            console.log(`read video successed! file: ${fileNameMp4}`);

            const blobMp4 = new Blob([data], {
              type: "video/mp4",
            });

			...
            fbs.uploadBlob(basePath, fileNameMp4, blobMp4)


이렇게 mp4 영상으로 변환 후 저장하게 하여, IOS에서 재생에 문제가 발생했던 것을 해결하였다.


5. 문제 발생3. Android 일부 기종에서 재생이 안됌

5.1 문제 내용

아직 문제가 끝이 아니었다. Android의 일부 기종에서 해당 링크의 영상이 재생이 안되었다.
확인되었던 건 갤럭시Z플립4 기종에서 재생이 안되었어서, 다른 단말기에서도 안될 확률이 농후하였다.

5.2 문제 해결

일단 당연하게도 mp4형식의 문제는 아니었다. 그래서 다른 재생 잘 되는 mp4영상과 내가 처리한 mp4영상을 Ubuntu의 file커맨드로 비교해 보았다.

그런데 두 mp4파일의 컨테이너값이 달랐다. (MP4 Base Media v1 <-> MP4 v2)

MP4 v2컨테이너로 이루어진 mp4영상이 재생이 잘 되는 영상이었어서, 영상을 mp4로 변환할때 해당 디코더(정식 명칭은 Microsoft MPEG-4 version 2)로 변환하도록 하였다.

이 또한 ffmpeg 라이브러리에서 지원하고 있었으며, outputOptions에 "-brand mp42" 를 추가로 넣어주어 MP4 v2 컨테이너로 변환되도록 하였다.

...
var command = ffmpeg(fileNameWebm).format("mp4").outputOptions(["-brand mp42"]);
...


그 결과, 재생이 안되던 단말기에서 영상이 정상 재생이 되었다.


6. 문제 발생4. 일부 웹캡에서 촬영한 영상이 재생에 문제 발생

6.1 문제 내용

아직 끝날때까지 끝난게 아니었다. 일부 저사양의 웹캠에서 촬영한 영상이 재생시에 뚝뚝 끊겨서 재생되고, 일부 기종에서는 아예 재생 조차도 안되는 문제가 발생했다.

무엇이 문제일까 하며 위에서 언급된 재생 잘되는 샘플 영상과 내가 변환한 영상의 메타데이터를 비교해 보았다.

6.2 문제 해결

파일의 모든 메타데이터를 보어준다는 온라인 사이트(metadata2go)에서 두 영상을 비교해 보았을때, 내가 변환 처리한 mp4에서 몇가지 메타데이터가 비어 있음을 확인하였다.

그중 가장 유력한 메타데이터는 fps(Frame Rate)였다.
우측(정상 재생되는 mp4영상)의 Frame Rate는 28.306인 정상적인 수치가 나타나는 반면, 좌측(내가 처리한 mp4영상)의 Frame Rate는 1000이라는 이상한 값이 나타났다.

실제 메타데이터가 어떻겠건 아무래도 영싱의 fps가 1000이라는 값은 비정상적인 수치이기 때문에, 영상 변환 과정에서 fps값을 별도로 설정해주기로 하였다.

이 작업도 마찬가지로 ffmpeg 라이브러리에서 지원하고 있었다. outputOptions에 "-filter:v, fps=30" 을 추가로 넣어주어, fps를 30으로 설정해주게 되었다.

(-filter:v, fps=30: video에 대한 fps값을 30으로 설정한다는 커맨드)

...
var command = ffmpeg(fileNameWebm).format("mp4").outputOptions(["-brand mp42", "-filter:v", "fps=30"]);
...


변환을 마친 후, 다시 메타데이터를 조회해 보니 Video Frame Rate30이라는 값으로 나타났다.


이후 동일한 웹캠으로 영상을 촬영 한 후, 처리된 영상을 재생해보니, 부드럽게 잘 재생이 되었다.


이후 현재까지도 웹캠들로 촬영 및 스마트폰 단말기들로 영상을 재생하였을때, 문제 없이 잘 동작하고 있다.

반응형