- 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> |
| |
| 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; |
| |
| const chuncks = Math.ceil(filesize / chunckSize); |
| let start = 0, |
| end = chunckSize + start; |
| const url = 'http://localhost:3000'; |
| |
| |
| let sourceBuffer = null; |
| |
| 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(() => { |
| |
| 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; |
| |
| } |
| |
| |
| |
| sourceBuffer.appendBuffer(arrayBuffer); |
| } |
| } |
| |
| function handleMediaSource() { |
| return new Promise((resolve) => { |
| const mediaSource = new MediaSource(); |
| console.log('isTypeSupported => ', MediaSource.isTypeSupported(mimeCodec)); |
| |
| mediaSource.addEventListener('sourceopen', () => { |
| |
| |
| const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec); |
| |
| sourceBuffer.addEventListener('error', (e) => { |
| console.log('sourceBuffer error', e); |
| }); |
| |
| |
| 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}`); |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· 单线程的Redis速度为什么快?
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码