미디어 소스 확장

François Beaufort
François Beaufort
Joe Medley
Joe Medley

미디어 소스 확장 프로그램 (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에서는 여기서 벗어날 수 있다고 생각합니다. 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 값이 모두 false인지 확인합니다.
  • 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.');
}
소스 속성(블로브)
그림 1: 소스 속성이 blob인 경우

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

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와 반환되는 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);
    });
}

최종 버전

다음은 전체 코드 예입니다. Media Source Extensions에 대해 알아보셨기를 바랍니다.

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);
    });
}

의견