
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>Document</title> |
| <style> |
| .bg, |
| .video, |
| .canvas { |
| width: 200px; |
| height: auto; |
| object-fit: contain; |
| box-sizing: border-box; |
| } |
| </style> |
| </head> |
| <body> |
| <img src="./school_overcast-s.jpg" alt="" class="bg" /> |
| <video src="./fire.mp4" controls autoplay loop muted class="video"></video> |
| <canvas class="canvas"></canvas> |
| |
| <script src="./mp4box.min.js"></script> |
| <script> |
| const imgBg = document.querySelector('.bg'); |
| |
| const video = document.querySelector('.video'); |
| |
| const canvas = document.querySelector('.canvas'); |
| |
| |
| const ctx = canvas.getContext('2d'); |
| |
| const mp4box = MP4Box.createFile(); |
| |
| |
| |
| let videoTrack = null, |
| videoDecoder = null; |
| |
| const videoFrames = []; |
| |
| let nbSampleTotal = 0, |
| countSample = 0; |
| |
| mp4box.onError = (e) => { |
| console.error('Error:', e); |
| }; |
| mp4box.onReady = (info) => { |
| console.log('Info:', info); |
| videoTrack = info.videoTracks[0]; |
| |
| if (videoTrack) { |
| |
| mp4box.setExtractionOptions(videoTrack.id, 'video', { |
| |
| nbSamples: 100, |
| }); |
| } |
| |
| |
| videoDecoder = new VideoDecoder({ |
| async output(videoFrame) { |
| |
| |
| const img = await createImageBitmap(videoFrame); |
| videoFrames.push({ |
| img, |
| duration: videoFrame.duration, |
| timestamp: videoFrame.timestamp, |
| }); |
| videoFrame.close(); |
| }, |
| error(err) { |
| console.log('videoDecoder error => ', err); |
| }, |
| }); |
| |
| nbSampleTotal = videoTrack.nb_samples; |
| |
| videoDecoder.configure({ |
| codec: videoTrack.codec, |
| codedWidth: videoTrack.track_width, |
| codedHeight: videoTrack.track_height, |
| description: getExtraData(), |
| }); |
| |
| mp4box.start(); |
| }; |
| mp4box.onSamples = (tranckId, ref, samples) => { |
| |
| |
| if (videoTrack.id === tranckId) { |
| mp4box.stop(); |
| |
| countSample += samples.length; |
| |
| for (const { is_sync, duration, data, cts } of samples) { |
| const type = is_sync ? 'key' : 'delta'; |
| |
| const chunk = new EncodedVideoChunk({ |
| type, |
| timestamp: cts, |
| duration, |
| data, |
| }); |
| |
| videoDecoder.decode(chunk); |
| } |
| |
| if (countSample === nbSampleTotal) { |
| videoDecoder.flush(); |
| } |
| } |
| }; |
| |
| |
| |
| |
| function getExtraData() { |
| const entry = mp4box.moov.traks[0].mdia.minf.stbl.stsd.entries[0]; |
| |
| const box = entry.avcC ?? entry.hvcC ?? entry.vpcC; |
| if (box) { |
| const stream = new DataStream(undefined, 0, DataStream.BIG_ENDIAN); |
| |
| box.write(stream); |
| return new Uint8Array(stream.buffer.slice(8)); |
| } |
| } |
| |
| let index = 0; |
| |
| |
| |
| |
| function drawFrame() { |
| const { img, timestamp, duration } = videoFrames[index]; |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| |
| ctx.globalCompositeOperation = 'source-over'; |
| |
| ctx.drawImage(imgBg, 0, 0, canvas.width, canvas.height); |
| |
| |
| ctx.globalCompositeOperation = 'screen'; |
| ctx.drawImage(img, 0, 0, canvas.width, canvas.height); |
| |
| index++; |
| |
| if (index === videoFrames.length) { |
| index = 0; |
| } |
| |
| video.requestVideoFrameCallback(drawFrame); |
| } |
| |
| fetch('./fire.mp4') |
| .then((res) => res.arrayBuffer()) |
| .then((buffer) => { |
| buffer.fileStart = 0; |
| |
| mp4box.appendBuffer(buffer); |
| mp4box.flush(); |
| }); |
| |
| video.addEventListener('loadedmetadata', () => { |
| if (video.videoWidth < video.offsetWidth) { |
| canvas.width = video.offsetWidth * devicePixelRatio; |
| canvas.height = video.offsetHeight * devicePixelRatio; |
| } else { |
| canvas.width = video.videoWidth * devicePixelRatio; |
| canvas.height = video.videoHeight * devicePixelRatio; |
| } |
| |
| video.requestVideoFrameCallback(drawFrame); |
| }); |
| </script> |
| </body> |
| </html> |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!