- API使用:
MediaSource
+ SourceBuffer
http Range
<!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>
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}`);