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

[Tên người]
François Beaufort
Joe Medley
Joe Medley

Tiện ích nguồn nội dung nghe nhì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õ MSE nếu muốn nhúng video vào trang web để thực hiện những việc như:

  • Truyền phát thích ứng, một cách khác để nói về việc thích ứng với khả năng của thiết bị và tình trạng mạng
  • Nối thích ứng, chẳng hạn như chèn quảng cáo
  • Dịch chuyển thời gian
  • Kiểm soát hiệu suất và kích thước 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 cửa hàng. Như minh hoạ trong hình, giữa tệp đã tải xuống và các phần tử nội dung đa phương tiện có một số lớp.

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

Trong thực tế, chuỗi sẽ 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ể hiểu được nội dung giải thích cho đến thời điểm này, hãy ngừng đọc ngay. Nếu bạn muốn được giải thích chi tiết hơn, vui lòng đọc tiếp. Tôi sẽ tìm hiểu về 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 tạo bản 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 sẽ cung cấp mọi điều bạn cần phải biết về phát nội dung đa phương tiện trên trang web chứ? Không, công cụ này chỉ nhằm giúp bạn hiểu mã phức tạp hơn mà bạn có thể tìm thấy ở nơi khác. Xin lưu ý rằng tài liệu này sẽ đơn giản hoá và loại trừ nhiều nội dung. Chúng tôi cho rằng mình có thể tránh được việc này vì chúng tôi cũng đề xuất 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 đơn giản hoá một cách có chủ ý.

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

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

  • Điều khiển chế độ phát. Chúng tôi nhận được các quảng cáo đó 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 sản xuất

Sau đây là một số điều tôi đề xuất khi sử dụng 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 trường hợp ngoại lệ về API, đồng thời kiểm tra HTMLMediaElement.readyStateMediaSource.readyState. Các giá trị này có thể thay đổi trước khi sự kiện liên kết được phân phối.
  • Hãy đảm bảo rằng các lệnh gọi appendBuffer()remove() trước đó vẫ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 mọi thực thể SourceBuffer được thêm vào MediaSource, hãy đảm bảo không có giá trị updating nào là đúng 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 việc đặt SourceBuffer.mode hay SourceBuffer.timestampOffset sẽ khiến giá trị này chuyển đổi sang open. Điều này có nghĩa là bạn nên chuẩn bị để 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 trong việc xác định nguyên nhân gốc của lỗi, đặc biệt đối với các lỗi khó tái tạo trong môi trường kiểm thử.

Đính kèm một bản sao MediaSource vào phần tử đa phương tiện

Giống như nhiều hoạt động phát triển web hiện nay, bạn cầ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 nghe nhìn, đó 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 thành URL và được truyền đến thuộc tính nguồn của phần tử 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);
  // 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 chuyển đối tượng MediaSource đến thuộc tính src có vẻ hơi lạ. Chúng 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 đó, thì bạn sẽ thấy ý tôi là gì.

Thực thể MediaSource đã sẵn sàng chưa?

URL.createObjectURL() có tính chất đồng bộ; tuy nhiên, tệp này sẽ xử lý tệp đính kèm một cách không đồng bộ. Điều này sẽ gây ra sự chậm trễ đôi chút trước khi bạn có thể thực hiện thao tác với thực thể MediaSource. Thật may là có nhiều cách để kiểm tra đ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 thực thể MediaSource và phần tử nội dung đa phương tiện. Loại dữ liệu này có thể có một trong những giá trị sau:

  • closed – Thực thể MediaSource không được đính kèm vào phần tử phương tiện.
  • open – Thực thể 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 thành phần nội dung đa phương tiện và tất cả dữ liệu của thành phần đó đã được truyền đến thành phần đó.

Việc trực tiếp truy vấn những tuỳ chọn này có thể ảnh hưởng tiêu cực đến hiệu suất. May mắn 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 xây dựng, 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 rằng việc này có vẻ quá sớm, nhưng tôi có thể làm điều này bất cứ lúc nào sau khi thuộc tính src của phần tử đ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 không huỷ bất kỳ đối tượng nào. API này cho phép nền tảng xử lý việc thu gom rác tại một thời điểm thích hợp, đó là lý do tôi gọi ngay tính năng này.

Tạo SourceBuffer

Bây giờ, đã đến lúc tạo SourceBuffer. Đây là đối tượng thực sự thực hiện công việc chuyển đổi dữ liệu giữa nguồn nội dung đa phương tiện và phần tử nội dung đa phương tiệ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.

Trong 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ị thích hợp. Hãy lưu ý rằng trong ví dụ bên dưới, chuỗi loại MIME chứa một loại MIME và 2 codec. Đây là 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 các phần video và âm thanh của tệp.

Phiên bản 1 của quy cách MSE cho phép các tác nhân người dùng khác nhau về việc có cần cả loại MIME và codec 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. Ví dụ: một số tác nhân người dùng (Chrome) yêu cầu bộ mã hoá và giải mã cho 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ả những thứ này, tốt hơn là chỉ nên đưa vào cả hai.

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 bạn tìm kiếm các ví dụ về MSE trên Internet, bạn sẽ tìm thấy nhiều yêu cầu truy xuất các tệp nội dung nghe nhìn bằng XHR. Để cải tiến hơn nữa, tôi sẽ sử dụng API Tìm nạpPromise mà API trả về. Nếu bạn đang cố gắng thực hiện việc này trong Safari, thì thao tá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 chính thức sẽ có cùng một tệp trong nhiều phiên bản để hỗ trợ các trình duyệt khác nhau. Dịch vụ này 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ã trong thực tế cũng sẽ có nhiều bản sao của các tệp nội dung nghe nhìn ở nhiều độ phân giải để có thể thích ứng với các tính năng khác nhau 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 nhiều đoạ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 thích ứng với điều kiện mạng trong khi nội dung nghe nhìn đang phát. Bạn có thể đã nghe nói đến thuật ngữ DASH (Truyền phát thích ứng động qua HTTP) hoặc HLS (Phát trực tuyến dựa trên HTTP), đây là hai phương pháp để thực hiện điều này. Nội dung 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 được. Chúng ta cần lấy dữ liệu nội dung nghe nhìn từ đối tượng Response vào 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 dữ liệu đó đến SourceBuffer. Bắt đầu bằng cách gọi response.arrayBuffer() để trả về một lời hứa vào vùng đệm. Trong mã của mình, tôi đã truyền lời hứa này vào 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 thêm tất cả ArrayBuffers và dự kiến không có thêm dữ liệu nội dung nghe nhìn nào khác, 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ề đoạn mã hoàn chỉnh. Tôi hy vọng bạn đã tìm hiểu điều gì đó về Tiện ích nguồn nội dung nghe nhì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);
    });
}

Ý kiến phản hồi