ส่วนขยายแหล่งที่มาของสื่อ

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

Media Source Extensions (MSE) เป็น JavaScript API ที่ช่วยให้คุณสร้างสตรีมสำหรับการเล่นจากส่วนของเสียงหรือวิดีโอ แม้ว่าบทความนี้จะไม่ครอบคลุมถึง MSE แต่คุณจำเป็นต้องทำความเข้าใจ MSE หากต้องการฝังวิดีโอในเว็บไซต์ที่ทําสิ่งต่างๆ เช่น

  • สตรีมมิงแบบปรับได้ ซึ่งเป็นอีกวิธีหนึ่งในการบอกว่าระบบจะปรับตามความสามารถของอุปกรณ์และสภาพเครือข่าย
  • การต่อคลิปแบบปรับขนาดได้ เช่น การแทรกโฆษณา
  • การเลื่อนเวลา
  • การควบคุมประสิทธิภาพและขนาดการดาวน์โหลด
โฟลว์ข้อมูล MSE พื้นฐาน
รูปที่ 1: การรับส่งข้อมูล MSE พื้นฐาน

คุณอาจมอง MSE เป็นเหมือนเชน ดังที่แสดงในรูปภาพ ไฟล์ที่ดาวน์โหลดมาและองค์ประกอบสื่อมีเลเยอร์หลายเลเยอร์

  • องค์ประกอบ <audio> หรือ <video> เพื่อเล่นสื่อ
  • อินสแตนซ์ MediaSource ที่มี SourceBuffer เพื่อส่งผ่านองค์ประกอบสื่อ
  • การเรียก fetch() หรือ XHR เพื่อดึงข้อมูลสื่อในออบเจ็กต์ Response
  • การโทรหา Response.arrayBuffer() เพื่อส่งฟีด MediaSource.SourceBuffer

ในทางปฏิบัติ เชนจะมีลักษณะดังนี้

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 พื้นฐาน ขั้นตอนการสร้างแต่ละขั้นตอนจะเพิ่มโค้ดลงในขั้นตอนก่อนหน้า

หมายเหตุเกี่ยวกับความชัดเจน

บทความนี้จะบอกทุกสิ่งที่คุณต้องรู้เกี่ยวกับการเล่นสื่อในหน้าเว็บไหม ไม่ เครื่องมือนี้มีไว้เพื่อช่วยให้คุณเข้าใจโค้ดที่ซับซ้อนมากขึ้นซึ่งอาจพบในที่อื่นๆ เอกสารนี้อธิบายแบบง่ายและยกเว้นหลายสิ่งเพื่อความชัดเจน เราคิดว่าเราอาจไม่ต้องดำเนินการใดๆ กับเรื่องนี้เนื่องจากเราแนะนำให้ใช้ไลบรารี เช่น Shaka Player ของ Google ด้วย เราจะระบุไว้ตลอดว่าส่วนใดที่เราจงใจทำให้เข้าใจง่าย

สิ่งที่ไม่ครอบคลุม

ต่อไปนี้คือตัวอย่างสิ่งที่เราจะไม่พูดถึง (เรียงลำดับไม่ตามหัวข้อ)

  • ตัวควบคุมการเล่น เราได้รับสิ่งเหล่านี้ได้ฟรีด้วยการใช้องค์ประกอบ HTML5 <audio> และ <video>
  • การจัดการข้อผิดพลาด

สำหรับใช้ในสภาพแวดล้อมที่ใช้งานจริง

ต่อไปนี้คือสิ่งที่เราขอแนะนําในการใช้งาน API ที่เกี่ยวข้องกับ MSE ในเวอร์ชันที่ใช้งานจริง

  • ก่อนเรียกใช้ API เหล่านี้ ให้จัดการเหตุการณ์ข้อผิดพลาดหรือข้อยกเว้นของ API และตรวจสอบ HTMLMediaElement.readyState และ MediaSource.readyState ค่าเหล่านี้อาจเปลี่ยนแปลงได้ก่อนที่จะส่งเหตุการณ์ที่เกี่ยวข้อง
  • ตรวจสอบว่าคําเรียก appendBuffer() และ remove() ก่อนหน้านี้ยังไม่ดําเนินการอยู่โดยตรวจสอบค่าบูลีน SourceBuffer.updating ก่อนอัปเดต mode, timestampOffset, appendWindowStart, appendWindowEnd ของ SourceBuffer หรือเรียกใช้ appendBuffer() หรือ remove() ใน SourceBuffer
  • สําหรับอินสแตนซ์ SourceBuffer ทั้งหมดที่เพิ่มลงใน MediaSource โปรดตรวจสอบว่าค่า updating ของอินสแตนซ์เหล่านั้นไม่ใช่ "จริง" ก่อนเรียกใช้ MediaSource.endOfStream() หรืออัปเดต MediaSource.duration
  • หากค่า 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: แอตทริบิวต์แหล่งที่มาในรูปแบบบล็อก

การที่สามารถส่งออบเจ็กต์ MediaSource ไปยังแอตทริบิวต์ src อาจดูแปลกๆ อยู่บ้าง โดยปกติแล้วจะเป็นสตริง แต่อาจเป็น Blob ก็ได้ หากตรวจสอบหน้าเว็บที่มีสื่อที่ฝังและองค์ประกอบสื่อของหน้านั้น คุณจะเห็นสิ่งที่เราหมายถึง

อินสแตนซ์ MediaSource พร้อมใช้งานไหม

URL.createObjectURL() เป็นแบบซิงโครนัส แต่ประมวลผลไฟล์แนบแบบอะซิงโครนัส ซึ่งจะทำให้เกิดความล่าช้าเล็กน้อยก่อนที่คุณจะดำเนินการใดๆ กับอินสแตนซ์ MediaSource ได้ แต่โชคดีที่การทดสอบเรื่องนี้ทำได้ วิธีที่ง่ายที่สุดคือใช้พร็อพเพอร์ตี้ MediaSource ชื่อ readyState พร็อพเพอร์ตี้ readyState อธิบายความสัมพันธ์ระหว่างอินสแตนซ์ MediaSource กับองค์ประกอบสื่อ โดยอาจมีค่าใดค่าหนึ่งต่อไปนี้

  • closed - อินสแตนซ์ MediaSource ไม่ได้แนบอยู่กับองค์ประกอบสื่อ
  • open - อินสแตนซ์ MediaSource แนบอยู่กับองค์ประกอบสื่อและพร้อมรับข้อมูลหรือกำลังรับข้อมูล
  • ended - อินสแตนซ์ MediaSource แนบอยู่กับองค์ประกอบสื่อและระบบได้ส่งข้อมูลทั้งหมดของอินสแตนซ์นั้นไปยังองค์ประกอบนั้นแล้ว

การค้นหาตัวเลือกเหล่านี้โดยตรงอาจส่งผลเสียต่อประสิทธิภาพ แต่โชคดีที่ MediaSource จะทริกเกอร์เหตุการณ์เมื่อ readyState มีการเปลี่ยนแปลงด้วย โดยเฉพาะอย่างยิ่ง 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 และตัวแปลงรหัส 2 รายการ สตริงนี้เป็นตัวสตริง mime สำหรับไฟล์วิดีโอ แต่ใช้ตัวแปลงรหัสแยกต่างหากสำหรับส่วนวิดีโอและเสียงของไฟล์

ข้อกำหนด MSE เวอร์ชัน 1 อนุญาตให้ User Agent กำหนดความต้องการที่แตกต่างกันได้ว่าจะกำหนดทั้งประเภท MIME และตัวแปลงรหัสหรือไม่ โดย User Agent บางรายการไม่กำหนด แต่อนุญาตให้ใช้เฉพาะประเภท MIME User Agent บางรายการ เช่น Chrome ต้องใช้ตัวแปลงรหัสสำหรับประเภท mime ที่ไม่ได้อธิบายตัวแปลงรหัสของตนเอง แทนที่จะพยายามจัดเรียงข้อมูลทั้งหมดนี้ คุณควรใส่ทั้ง 2 รายการ

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() polyfill

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 ซึ่งเป็น 2 วิธีในการดำเนินการนี้ การอภิปรายหัวข้อนี้อย่างละเอียดอยู่นอกเหนือขอบเขตของบทนํานี้

ประมวลผลออบเจ็กต์การตอบกลับ

ดูเหมือนว่าโค้ดจะเกือบเสร็จแล้ว แต่สื่อไม่เล่น เราต้องรับข้อมูลสื่อจากออบเจ็กต์ Response ไปยัง SourceBuffer

วิธีทั่วไปในการส่งข้อมูลจากออบเจ็กต์การตอบกลับไปยังอินสแตนซ์ MediaSourceคือการรับ ArrayBuffer จากออบเจ็กต์การตอบกลับและส่งไปยัง SourceBuffer เริ่มต้นด้วยการเรียกใช้ response.arrayBuffer() ซึ่งจะแสดงผลเป็นสัญญากับบัฟเฟอร์ ในโค้ดของฉัน เราได้ส่ง Promise นี้ไปยังประโยค then() ที่ 2 ซึ่งฉันจะเพิ่มต่อท้าย 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>
}

เรียกใช้ endOfStream()

หลังจากเพิ่ม ArrayBuffers ทั้งหมดแล้ว และคาดว่าจะไม่มีข้อมูลสื่อเพิ่มเติม ให้เรียกใช้ MediaSource.endOfStream() ซึ่งจะเปลี่ยน MediaSource.readyState เป็น ended และเรียกเหตุการณ์ 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);
    });
}

ความคิดเห็น