Tiện ích nguồn nội dung nghe nhìn

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

Tiện ích nguồn nội dung đa phương tiện (MSE) là một API JavaScript cho phép bạn tạo luồng để phát từ các phân đoạn âm thanh hoặc video. Mặc dù không được đề cập trong bài viết này, nhưng bạn cần hiểu rõ về MSE nếu muốn nhúng các video vào trang web của mình để thực hiện những việc như:

  • Truyền phát thích ứng, là một cách nói khác để thích ứng với khả năng của thiết bị và điều kiện mạng
  • Ghép thích ứng, chẳng hạn như chèn quảng cáo
  • Chuyển đổi thời gian
  • Kiểm soát hiệu suất và kích thước tệp tải xuống
Luồng dữ liệu MSE cơ bản
Hình 1: Luồng dữ liệu MSE cơ bản

Bạn gần như có thể coi MSE là một chuỗi. Như minh hoạ trong hình, giữa tệp đã tải xuống và các phần tử nội dung nghe nhìn là một số lớp.

  • Phần tử <audio> hoặc <video> để phát nội dung nghe nhìn.
  • Một thực thể MediaSourceSourceBuffer để cung cấp phần tử nội dung nghe nhìn.
  • Lệnh gọi fetch() hoặc XHR để truy xuất dữ liệu phương tiện trong đối tượng Response.
  • Lệnh gọi đến Response.arrayBuffer() để cung cấp MediaSource.SourceBuffer.

Trong thực tế, chuỗi này có dạng như sau:

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

Nếu bạn có thể sắp xếp mọi thứ từ các phần giải thích cho đến thời điểm này, hãy dừng đọc ngay. Nếu bạn muốn biết thêm thông tin giải thích chi tiết, vui lòng đọc tiếp. Tôi sẽ hướng dẫn bạn thực hiện chuỗi này bằng cách xây dựng một ví dụ cơ bản về MSE. Mỗi bước xây dựng sẽ thêm mã vào bước trước.

Lưu ý về tính rõ ràng

Bài viết này có cho bạn biết mọi điều cần biết về cách phát nội dung nghe nhìn trên trang web không? Không, mục đích của bài viết này chỉ là giúp bạn hiểu được mã phức tạp hơn mà bạn có thể tìm thấy ở nơi khác. Để làm rõ, tài liệu này đơn giản hoá và loại trừ nhiều nội dung. Chúng tôi cho rằng có thể bỏ qua vấn đề này vì bạn cũng nên sử dụng một thư viện như Shaka Player của Google. Tôi sẽ lưu ý trong suốt quá trình tôi cố tình đơn giản hoá.

Một số điều không được đề cập

Dưới đây là một số nội dung tôi sẽ không đề cập đến (không theo thứ tự cụ thể).

  • Các nút điều khiển chế độ phát. Chúng ta có được những giá trị đó miễn phí nhờ sử dụng các phần tử HTML5 <audio><video>.
  • Xử lý lỗi.

Để sử dụng trong môi trường phát hành công khai

Dưới đây là một số điều tôi đề xuất khi sử dụng các API liên quan đến MSE trong phiên bản chính thức:

  • Trước khi thực hiện lệnh gọi trên các API này, hãy xử lý mọi sự kiện lỗi hoặc ngoại lệ API và kiểm tra HTMLMediaElement.readyStateMediaSource.readyState. Các giá trị này có thể thay đổi trước khi các sự kiện liên kết được phân phối.
  • Đảm bảo các lệnh gọi appendBuffer()remove() trước đó không còn đang diễn ra bằng cách kiểm tra giá trị boolean SourceBuffer.updating trước khi cập nhật mode, timestampOffset, appendWindowStart, appendWindowEnd của SourceBuffer hoặc gọi appendBuffer() hoặc remove() trên SourceBuffer.
  • Đối với tất cả các thực thể SourceBuffer được thêm vào MediaSource, hãy đảm bảo rằng không có giá trị updating nào của các thực thể đó là true trước khi gọi MediaSource.endOfStream() hoặc cập nhật MediaSource.duration.
  • Nếu giá trị MediaSource.readyStateended, các lệnh gọi như appendBuffer()remove() hoặc thiết lập SourceBuffer.mode hay SourceBuffer.timestampOffset sẽ khiến giá trị này chuyển đổi thành open. Điều này có nghĩa là bạn nên sẵn sàng xử lý nhiều sự kiện sourceopen.
  • Khi xử lý các sự kiện HTMLMediaElement error, nội dung của MediaError.message có thể hữu ích để xác định nguyên nhân gốc rễ của lỗi, đặc biệt là đối với các lỗi khó tái hiện trong môi trường thử nghiệm.

Đính kèm một thực thể MediaSource vào một phần tử nội dung nghe nhìn

Giống như nhiều việc trong quá trình phát triển web ngày nay, bạn bắt đầu bằng việc phát hiện tính năng. Tiếp theo, hãy lấy một phần tử nội dung đa phương tiện, có thể là phần tử <audio> hoặc <video>. Cuối cùng, hãy tạo một thực thể của MediaSource. URL này được chuyển đổi thành URL và truyền vào thuộc tính nguồn của phần tử nội dung nghe nhìn.

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.');
}
Thuộc tính nguồn dưới dạng blob
Hình 1: Thuộc tính nguồn dưới dạng blob

Việc truyền một đối tượng MediaSource đến thuộc tính src có vẻ hơi kỳ lạ. Thường là chuỗi, nhưng cũng có thể là blob. Nếu kiểm tra một trang có nội dung nghe nhìn được nhúng và kiểm tra thành phần nội dung đa phương tiện của trang đó, bạn sẽ hiểu ý của tôi.

Phiên bản MediaSource đã sẵn sàng chưa?

URL.createObjectURL() tự nó là đồng bộ; tuy nhiên, lớp này xử lý tệp đính kèm một cách không đồng bộ. Việc này gây ra độ trễ nhỏ trước khi bạn có thể làm bất cứ việc gì với thực thể MediaSource. May mắn là có những cách để kiểm thử điều này. Cách đơn giản nhất là sử dụng thuộc tính MediaSource có tên là readyState. Thuộc tính readyState mô tả mối quan hệ giữa một thực thể MediaSource và một phần tử nội dung nghe nhìn. Thuộc tính này có thể có một trong những giá trị sau:

  • closed – Phiên bản MediaSource không được đính kèm vào phần tử nội dung nghe nhìn.
  • open – Phiên bản MediaSource được đính kèm vào một phần tử nội dung nghe nhìn và sẵn sàng nhận dữ liệu hoặc đang nhận dữ liệu.
  • ended – Thực thể MediaSource được đính kèm vào một phần tử nội dung đa phương tiện và tất cả dữ liệu của thực thể đã được truyền đến phần tử đó.

Việc truy vấn trực tiếp các tuỳ chọn này có thể ảnh hưởng tiêu cực đến hiệu suất. Rất may là MediaSource cũng kích hoạt các sự kiện khi readyState thay đổi, cụ thể là sourceopen, sourceclosed, sourceended. Trong ví dụ tôi đang tạo, tôi sẽ sử dụng sự kiện sourceopen để cho tôi biết thời điểm tìm nạp và lưu video vào bộ đệm.

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>

Lưu ý rằng tôi cũng đã gọi revokeObjectURL(). Tôi biết việc này có vẻ là sớm, nhưng tôi có thể làm việc này bất cứ lúc nào sau khi thuộc tính src của phần tử nội dung đa phương tiện được kết nối với một thực thể MediaSource. Việc gọi phương thức này sẽ không huỷ bỏ bất kỳ đối tượng nào. Phương thức này cho phép nền tảng xử lý việc thu thập rác vào thời điểm thích hợp, đó là lý do tôi gọi phương thức này ngay lập tức.

Tạo SourceBuffer

Bây giờ, đã đến lúc tạo SourceBuffer. Đây là đối tượng thực sự thực hiện việc chuyển dữ liệu giữa các nguồn nội dung nghe nhìn và các phần tử nội dung nghe nhìn. SourceBuffer phải dành riêng cho loại tệp nội dung nghe nhìn mà bạn đang tải.

Trên thực tế, bạn có thể thực hiện việc này bằng cách gọi addSourceBuffer() với giá trị phù hợp. Lưu ý rằng trong ví dụ bên dưới, chuỗi loại mime chứa một loại mime và hai bộ mã hoá và giải mã. Đây là một chuỗi mime cho tệp video, nhưng sử dụng các bộ mã hoá và giải mã riêng biệt cho phần video và âm thanh của tệp.

Phiên bản 1 của thông số kỹ thuật MSE cho phép các tác nhân người dùng khác nhau về việc có yêu cầu cả loại MIME và bộ mã hoá và giải mã hay không. Một số tác nhân người dùng không yêu cầu, nhưng chỉ cho phép loại MIME. Một số tác nhân người dùng, chẳng hạn như Chrome, yêu cầu bộ mã hoá và giải mã cho các loại mime không tự mô tả bộ mã hoá và giải mã của chúng. Thay vì cố gắng sắp xếp tất cả, bạn chỉ nên đưa cả hai vào.

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

Tải tệp nội dung nghe nhìn

Nếu tìm kiếm trên Internet các ví dụ về MSE, bạn sẽ thấy rất nhiều ví dụ truy xuất tệp phương tiện bằng XHR. Để tiên tiến hơn, tôi sẽ sử dụng API Tìm nạpCam kết trả về. Nếu bạn đang cố gắng thực hiện việc này trong Safari, thì việc này sẽ không hoạt động nếu không có polyfill 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>;
}

Trình phát chất lượng phát hành chính thức sẽ có cùng một tệp ở nhiều phiên bản để hỗ trợ nhiều trình duyệt. Ứng dụng có thể sử dụng các tệp riêng biệt cho âm thanh và video để cho phép chọn âm thanh dựa trên chế độ cài đặt ngôn ngữ.

Mã thực tế cũng sẽ có nhiều bản sao của tệp phương tiện ở nhiều độ phân giải để có thể thích ứng với nhiều chức năng của thiết bị và điều kiện mạng. Một ứng dụng như vậy có thể tải và phát video theo từng phần bằng cách sử dụng yêu cầu phạm vi hoặc phân đoạn. Điều này cho phép điều chỉnh theo điều kiện mạng trong khi phát nội dung nghe nhìn. Bạn có thể đã nghe thấy các thuật ngữ DASH hoặc HLS. Đây là hai phương thức để thực hiện việc này. Việc thảo luận đầy đủ về chủ đề này nằm ngoài phạm vi của phần giới thiệu này.

Xử lý đối tượng phản hồi

Mã này gần như đã hoàn tất, nhưng nội dung nghe nhìn không phát. Chúng ta cần truyền dữ liệu nội dung nghe nhìn từ đối tượng Response đến SourceBuffer.

Cách thông thường để truyền dữ liệu từ đối tượng phản hồi đến thực thể MediaSource là lấy ArrayBuffer từ đối tượng phản hồi và truyền đối tượng đó đến SourceBuffer. Bắt đầu bằng cách gọi response.arrayBuffer(). Phương thức này sẽ trả về một lời hứa cho vùng đệm. Trong mã của mình, tôi đã truyền lời hứa này đến mệnh đề then() thứ hai, trong đó tôi thêm lời hứa này vào SourceBuffer.

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

Gọi endOfStream()

Sau khi tất cả ArrayBuffers được thêm vào và không có dữ liệu phương tiện nào khác được dự kiến, hãy gọi MediaSource.endOfStream(). Thao tác này sẽ thay đổi MediaSource.readyState thành ended và kích hoạt sự kiện 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);
    });
}

Phiên bản cuối cùng

Sau đây là ví dụ về mã hoàn chỉnh. Tôi hy vọng bạn đã học được một số điều về Tiện ích nguồn nội dung đa phương tiện.

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

Phản hồi