(MSE) video 分片加载

  • API使用: MediaSource + SourceBuffer
  • http Range



  • client.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <video id="video" controls width="500" height="300"></video>

    <script>
      /** @type {HTMLVideoElement} */
      const video = document.getElementById('video');
      const mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';

      init();
      async function init() {
        const filesize = await getFileSize();
        const chunckSize = 1024 * 1024 * 1; // 1MB
        // 分片片数
        const chuncks = Math.ceil(filesize / chunckSize);
        let start = 0,
          end = chunckSize + start;
        const url = 'http://localhost:3000';

        /** @type {SourceBuffer} */
        let sourceBuffer = null;
        /** @type {MediaSource} */
        let mediaSource = null;
        let preloadTime = 0;

        function handleProgress() {
          preloadTime = sourceBuffer.buffered.end(0);
          console.log('preloadTime => ', preloadTime);
        };
        let timer = null;
        function handlePlaying() {
          if (timer) {
            clearInterval(timer);
            timer = null;
          }
          timer = setInterval(() => {
            // 当播放位置与当前 SourceBuffer 中已缓冲区的最后一个片段的结尾处相差不超过 2 秒时,则开始下载下一个分片
            if (video.currentTime >= preloadTime - 2) {
              clearInterval(timer);
              timer = null;
              start = end + 1;
              end = start + chunckSize;

              if (start > filesize - 1) {
                // 流媒体结束
                sourceBuffer.abort();
                mediaSource.endOfStream();
                mediaSource = null;
                sourceBuffer = null;
                return;
              }

              handlePlaying();
              handle();
            }
          }, 300);
        }

        video.addEventListener('seeking', async () => {
          console.log('video.currentTime => ', video.currentTime);
          if (video.currentTime <= preloadTime) return;
        });
        video.addEventListener('play', handlePlaying);
        video.addEventListener('progress', handleProgress);

        handle();
        async function handle() {
          if (end > filesize - 1) {
            end = filesize - 1;
          }

          console.log('start, end => ', start, end, filesize);
          const partBlob = await ajax(url, {
            headers: { Range: `bytes=${start}-${end}` },
          });
          const arrayBuffer = await partBlob.arrayBuffer();

          if (!sourceBuffer) {
            let { sourceBuffer: _buf, mediaSource: _ms } = await handleMediaSource();
            sourceBuffer = _buf;
            mediaSource = _ms;
            // video.play();
          }

          // 将分片数据添加到 SourceBuffer 对象中
          // 方法将 `ArrayBuffer、TypedArray 或 DataView` 中的媒体片段数据添加到 SourceBuffer 对象中
          sourceBuffer.appendBuffer(arrayBuffer);
        }
      }

      function handleMediaSource() {
        return new Promise((resolve) => {
          const mediaSource = new MediaSource();
          console.log('isTypeSupported => ', MediaSource.isTypeSupported(mimeCodec));

          mediaSource.addEventListener('sourceopen', () => {
            // 根据给定的 MIME 类型创建一个新的 `SourceBuffer` 对象
            // SourceBuffer: 通过 `MediaSource` 对象传递到 `HTMLMediaElement` 并播放的媒体分块
            const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);

            sourceBuffer.addEventListener('error', (e) => {
              console.log('sourceBuffer error', e);
            });

            // sourceBuffer.appendBuffer(arrayBuffer);
            resolve({ sourceBuffer, mediaSource });
          });
          video.src = URL.createObjectURL(mediaSource);
        });
      }

      async function getFileSize() {
        const url = 'http://localhost:3000/filesize';
        const blob = await ajax(url);
        const size = await blob.text();
        return +size;
      }

      function ajax(url, options = {}) {
        return fetch(url, { ...options }).then((res) => res.blob());
      }
    </script>
  </body>
</html>
  • serve.js
import { serve } from 'bun';
import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
import { URL } from 'node:url';

const filePath = './frag_bunny.mp4';
const fileContentType = 'video/mp4';

const { size: fileSize } = await stat(filePath);

const server = serve({
  port: 3000,
  async fetch(req) {
    const { pathname } = new URL(req.url);
    console.log(' => ', req.method, pathname);

    if (pathname === '/filesize') {
      return new Response(fileSize.toString(), {
        headers: {
          'Access-Control-Allow-Origin': '*',
        },
      });
    }

    const range = req.headers.get('range');
    console.log('range => ', range);

    let [start, end] = [0, fileSize - 1];
    if (range) {
      const rangeStr = range.split('=')[1];

      const reg = /^bytes=\d+$/;
      if (reg.test(range)) {
        end = start = parseInt(rangeStr);
      } else {
        [start, end] = rangeStr.split('-');
        if (start === '' && end !== '') {
          start = fileSize - end;
          end = fileSize - 1;
        } else {
          start = start === '' ? fileSize - end : parseInt(start);
          end = end === '' ? fileSize - 1 : parseInt(end);
        }
      }
      console.log('start, end => ', start, end, fileSize);

      if (end > fileSize - 1 || start > fileSize - 1 || start > end || start < 0 || end < 0) {
        return new Response('请求范围不合法', { status: 416 });
      }
    }

    return new Response(createReadStream(filePath, { start, end }), {
      status: 206,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': 'Range',
        'Content-Type': fileContentType,
        'Content-Length': end - start + 1,
        'Content-Range': `bytes ${start}-${end}/${fileSize}`,
        'Accept-Ranges': 'bytes',
      },
    });
  },
});

console.log(`Listening on http://localhost:${server.port}`);
posted @ 2024-04-14 23:34  _clai  阅读(113)  评论(0编辑  收藏  举报