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

ฟร็องซัว โบฟอร์ต
François Beaufort
โจ เมดเลย์
Joe Medley

Media Source Extensions (MSE) คือ JavaScript API ที่ช่วยให้คุณสร้างสตรีมสำหรับการเล่นจากส่วนต่างๆ ของเสียงหรือวิดีโอ แม้ว่าในบทความนี้จะไม่ได้อธิบาย แต่คุณจำเป็นต้องเข้าใจ 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 หรือการเรียก appendBuffer() หรือ remove() ของ SourceBuffer ใน 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.');
}
แอตทริบิวต์แหล่งที่มาเป็น BLOB
รูปที่ 1: แอตทริบิวต์ของแหล่งที่มาในรูปแบบ BLOB

การส่งออบเจ็กต์ 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 เพื่อให้ล้ำสมัยมากขึ้น ผมจะใช้ 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 ซึ่งเป็น 2 วิธีในการบรรลุเป้าหมายนี้ การพูดคุยทั้งหมดของหัวข้อนี้อยู่นอกเหนือขอบเขต ของบทแนะนำนี้

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

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

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

ความคิดเห็น