-
API使用:
MediaSource
+SourceBuffer
-
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 decounce(func, delay = 1000) {let timer = null;return (...args) => {if (timer) {clearTimeout(timer);}timer = setTimeout(() => {func(...args);}, delay);};}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}`);