미디어 소스 확장

프랑수아 보퍼트
프랑수아 보퍼트
조 메들리
조 메들리

미디어 소스 확장 프로그램 (MSE)은 오디오 또는 동영상 세그먼트에서 재생하기 위한 스트림을 빌드할 수 있는 JavaScript API입니다. 이 도움말에서는 다루지 않지만 사이트에 다음과 같은 작업을 하는 동영상을 삽입하려는 경우 MSE를 이해해야 합니다.

  • 적응형 스트리밍이란 기기 기능 및 네트워크 상태에 맞게 조정하는 것을 말합니다.
  • 적응형 스플라이싱(예: 광고 삽입)
  • 타임 시프팅
  • 성능 및 다운로드 크기 제어
기본 MSE 데이터 흐름
그림 1: 기본 MSE 데이터 흐름

MSE를 체인이라고 생각하면 됩니다. 그림과 같이 다운로드한 파일과 미디어 요소 사이에는 여러 레이어가 있습니다.

  • 미디어를 재생하기 위한 <audio> 또는 <video> 요소
  • 미디어 요소를 제공할 SourceBuffer가 있는 MediaSource 인스턴스
  • Response 객체에서 미디어 데이터를 검색하기 위한 fetch() 또는 XHR 호출
  • MediaSource.SourceBuffer를 피드하기 위한 Response.arrayBuffer() 호출

실제로 체인은 다음과 같습니다.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function (response) {
      return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function (e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

지금까지의 설명으로 문제를 해결할 수 있다면 이제 그만두셔도 됩니다. 더 자세한 설명을 알고 싶으시다면 계속 읽어 보시기 바랍니다. 기본적인 MSE 예를 만들어 이 체인을 살펴보겠습니다. 각 빌드 단계는 이전 단계에 코드를 추가합니다.

명확성 관련 참고사항

이 도움말에는 웹페이지에서 미디어를 재생하는 데 알아야 할 모든 내용이 설명되어 있나요? 아니요. 이 가이드는 다른 곳에서 찾을 수 있는 더 복잡한 코드를 이해하는 데 도움이 될 뿐입니다. 명확하게 하기 위해 이 문서에서는 많은 사항을 단순화하고 제외시킵니다. 이 방법은 Google의 Shaka Player와 같은 라이브러리도 사용하는 것이 좋으므로 이 문제는 생략할 수 있습니다. 의도적으로 단순화한 부분을 계속 메모하겠습니다.

다루지 않은 내용

여기서는 다루지 않을 몇 가지 사항을 특별한 순서 없이 소개합니다.

  • 재생 컨트롤 HTML5 <audio><video> 요소를 사용하여 이러한 객체를 무료로 얻습니다.
  • 오류 처리.

프로덕션 환경에서 사용

다음은 MSE 관련 API를 프로덕션에 사용할 때 권장되는 사항입니다.

  • 이러한 API를 호출하기 전에 오류 이벤트 또는 API 예외를 처리하고 HTMLMediaElement.readyStateMediaSource.readyState를 확인합니다. 이러한 값은 연결된 이벤트가 전달되기 전에 변경될 수 있습니다.
  • SourceBuffermode, timestampOffset, appendWindowStart, appendWindowEnd를 업데이트하거나 SourceBuffer에서 appendBuffer() 또는 remove()를 호출하기 전에 SourceBuffer.updating 부울 값을 확인하여 이전 appendBuffer()remove() 호출이 아직 진행 중이 아닌지 확인합니다.
  • MediaSource에 추가된 모든 SourceBuffer 인스턴스의 경우 MediaSource.endOfStream()를 호출하거나 MediaSource.duration를 업데이트하기 전에 updating 값이 true가 아닌지 확인합니다.
  • MediaSource.readyState 값이 ended인 경우 appendBuffer()remove()과 같은 호출 또는 SourceBuffer.mode 또는 SourceBuffer.timestampOffset를 설정하면 이 값이 open으로 전환됩니다. 즉, 여러 sourceopen 이벤트를 처리할 준비가 되어 있어야 합니다.
  • HTMLMediaElement error 이벤트를 처리할 때 MediaError.message의 콘텐츠는 테스트 환경에서 재현하기 어려운 오류의 경우 장애의 근본 원인을 확인하는 데 유용할 수 있습니다.

미디어 요소에 MediaSource 인스턴스 연결

요즘은 웹 개발의 여러 부분이 그렇듯이 특성 감지부터 시작합니다. 그런 다음 미디어 요소(<audio> 또는 <video> 요소)를 가져옵니다. 마지막으로 MediaSource의 인스턴스를 만듭니다. 이 요소는 URL로 변환되어 미디어 요소의 소스 속성으로 전달됩니다.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  // Is the MediaSource instance ready?
} else {
  console.log('The Media Source Extensions API is not supported.');
}
blob인 소스 속성
그림 1: blob인 소스 속성

MediaSource 객체를 src 속성에 전달할 수 있다는 것이 조금 이상하게 보일 수 있습니다. 일반적으로 문자열이지만 blob일 수도 있습니다. 삽입된 미디어가 있는 페이지를 검사하고 미디어 요소를 검사하면 그 의미를 알 수 있습니다.

MediaSource 인스턴스가 준비되었나요?

URL.createObjectURL()는 그 자체로 동기식이지만 첨부파일을 비동기적으로 처리합니다. 이로 인해 MediaSource 인스턴스로 작업을 실행하기 전에 약간의 지연이 발생합니다. 다행히 이를 테스트하는 방법이 있습니다. 가장 간단한 방법은 readyState라는 MediaSource 속성을 사용하는 것입니다. readyState 속성은 MediaSource 인스턴스와 미디어 요소 간의 관계를 설명합니다. 다음 값 중 하나를 가질 수 있습니다.

  • closed - MediaSource 인스턴스가 미디어 요소에 연결되지 않았습니다.
  • open - MediaSource 인스턴스가 미디어 요소에 연결되어 있으며 데이터를 수신할 준비가 되어 있거나 데이터를 수신하고 있습니다.
  • ended - MediaSource 인스턴스가 미디어 요소에 연결되고 모든 관련 데이터가 이 요소에 전달되었습니다.

이러한 옵션을 직접 쿼리하면 성능에 부정적인 영향을 줄 수 있습니다. 다행히 MediaSourcereadyState, 특히 sourceopen, sourceclosed, sourceended가 변경되면 이벤트를 실행합니다. 제가 빌드하는 예에서는 sourceopen 이벤트를 사용하여 동영상을 가져오고 버퍼링할 시기를 알려줍니다.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  <strong>mediaSource.addEventListener('sourceopen', sourceOpen);</strong>
} else {
  console.log("The Media Source Extensions API is not supported.")
}

<strong>function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  // Create a SourceBuffer and get the media file.
}</strong>

revokeObjectURL()도 호출했습니다. 너무 이른 것 같지만 미디어 요소의 src 속성이 MediaSource 인스턴스에 연결된 후에 언제든지 이 작업을 할 수 있습니다. 이 메서드를 호출해도 객체가 삭제되지는 않습니다. 플랫폼에서 적절한 시점에 가비지 컬렉션을 처리할 수 있으므로 즉시 이 메서드를 호출합니다.

SourceBuffer 만들기

이제 미디어 소스와 미디어 요소 간에 데이터를 이동하는 작업을 실제로 실행하는 객체인 SourceBuffer를 만들 차례입니다. SourceBuffer는 로드 중인 미디어 파일 유형과 관련이 있어야 합니다.

실제로는 적절한 값으로 addSourceBuffer()를 호출하면 됩니다. 아래 예에서 MIME 유형 문자열에 MIME 유형과 두 개의 코덱이 포함되어 있습니다. 동영상 파일의 MIME 문자열이지만 파일의 동영상 및 오디오 부분에는 별도의 코덱을 사용합니다.

MSE 사양의 버전 1에서는 사용자 에이전트가 MIME 유형과 코덱이 모두 필요한지 여부를 다르게 지정할 수 있습니다. 일부 사용자 에이전트는 필요하지 않지만 MIME 유형만 허용합니다. 예를 들어 Chrome과 같은 일부 사용자 에이전트는 코덱을 자체 설명하지 않는 MIME 유형을 위한 코덱이 필요합니다. 이 모든 것을 정리하려고 하기보다는 둘 다 포함하는 것이 좋습니다.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  <strong>
    var mime = 'video/webm; codecs="opus, vp09.00.10.08"'; // e.target refers to
    the mediaSource instance. // Store it in a variable so it can be used in a
    closure. var mediaSource = e.target; var sourceBuffer =
    mediaSource.addSourceBuffer(mime); // Fetch and process the video.
  </strong>;
}

미디어 파일 가져오기

MSE 예제를 인터넷에서 검색하면 XHR을 사용하여 미디어 파일을 검색하는 방법을 많이 찾을 수 있습니다. 첨단 기능을 사용하기 위해 Fetch API와 이 API가 반환하는 Promise를 사용하겠습니다. Safari에서 이 작업을 하려고 하면 fetch() 폴리필이 없으면 작동하지 않습니다.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  <strong>
    fetch(videoUrl) .then(function(response){' '}
    {
      // Process the response object.
    }
    );
  </strong>;
}

프로덕션 품질 플레이어는 다양한 브라우저를 지원하기 위해 여러 버전에서 동일한 파일을 사용합니다. 오디오와 동영상에 별도의 파일을 사용하여 언어 설정에 따라 오디오를 선택할 수 있습니다.

실제 코드에는 다양한 기기 기능 및 네트워크 조건에 맞게 조정될 수 있도록 다양한 해상도의 미디어 파일 사본이 여러 개 있습니다. 이러한 애플리케이션은 범위 요청 또는 세그먼트를 사용하여 동영상을 청크 단위로 로드하고 재생할 수 있습니다. 이를 통해 미디어가 재생되는 동안 네트워크 조건에 적응할 수 있습니다. 이를 달성하는 두 가지 방법인 DASH 또는 HLS라는 용어를 들어보셨을 것입니다. 이 주제에 대한 전체 논의는 이 소개의 범위를 벗어납니다.

응답 객체 처리

코드는 거의 완료된 것처럼 보이지만 미디어가 재생되지 않습니다. 미디어 데이터를 Response 객체에서 SourceBuffer로 가져와야 합니다.

응답 객체에서 MediaSource 인스턴스로 데이터를 전달하는 일반적인 방법은 응답 객체에서 ArrayBuffer를 가져와 SourceBuffer에 전달하는 것입니다. 먼저 버퍼에 프로미스를 반환하는 response.arrayBuffer()를 호출합니다. 내 코드에서 이 프로미스를 SourceBuffer에 추가하는 두 번째 then() 절에 전달했습니다.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      <strong>return response.arrayBuffer();</strong>
    })
    <strong>.then(function(arrayBuffer) {
      sourceBuffer.appendBuffer(arrayBuffer);
    });</strong>
}

endOfStream() 호출

모든 ArrayBuffers가 추가되고 더 이상 미디어 데이터가 예상되지 않으면 MediaSource.endOfStream()를 호출합니다. 이렇게 하면 MediaSource.readyStateended로 변경되고 sourceended 이벤트가 실행됩니다.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      return response.arrayBuffer();
    })
    .then(function(arrayBuffer) {
      <strong>sourceBuffer.addEventListener('updateend', function(e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });</strong>
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

최종 버전

다음은 전체 코드 예입니다. 미디어 소스 확장 프로그램에 대해 배우셨기를 바랍니다.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function (response) {
      return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function (e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

의견