一步一步学习使用 MediaSource 实现动态媒体流

2024-04-14 07:28

本文主要是介绍一步一步学习使用 MediaSource 实现动态媒体流,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

学习前的参考

为什么视频网站的视频链接地址是blob? - 掘金

MediaSource - Web API 接口参考 | MDN

在示例中前往下载源代码:

netfix/demo/bufferWhenNeeded.html at gh-pages · nickdesaulniers/netfix · GitHub

 

        下载 demo 目录,对 bufferWhenNeeded.html 示例代码进行学习。

 

        直接运行所下载的示例代码,视频的播放效果是,每播放一段时间后,都会请求一段视频流。

 

        将源代码中的源视频地址(assetURL)替换为自己视频的地址后,出现视频无法播放的问题,但是控制台没有有用的报错信息(切换浏览器也如此):

 

        对比源代码所用的视频和自己的视频,发现源代码所用的视频在未点击播放时,已经加载了一小段。

 

        使用自己的视频时,也有以上的两个请求,但是并没有初始的视频段。

 

        观察源代码,根据以下信息,首先考虑到是 mimeCodec 与自己的视频不匹配的问题。

 

        需要解析自己的视频,获取到其 mimeCodec 信息。在示例代码中是使用 ./mp4info 命令来解析 frag_bunny.mp4,然后使用管道符使用 grep 提取出包含 Codec 的行来获取mimeCodec 信息。从 mp4info 命令入手查询。

        经过一番探索,使用 mp4box 的 -info 可以获取到视频的元数据信息。

        参考:

html - html5 video tag codecs attribute - Stack Overflow

        MP4box的下载和安装 :

MP4box是什么,win版mac版下载安装使用教程 - 老马奇遇记

        使用 mp4box -info name_of_video 获取视频的元数据信息:

 

        第一个是视频 codec: avc1.64001E;

        第二个是音频 codec:mp4a.40.5;

        则得出该视频的 mimeCodec 为 ' video/mp4; codecs="avc1.64001E, mp4a.40.5" '

        将源代码中的 mimeCodec 修改为此内容,查看页面结果,依旧不尽人意(对于同样的mimeCodec,可能有的浏览器支持,有的不支持,这里没有打印不支持此 mimeCodec):

 

        最后考虑是视频本身的格式问题。

        参考:

https://stackoverflow.com/questions/22996665/unable-to-get-mediasource-working-with-mp4-format-in-chrome

 

        应该将 mp4 文件进行片段化。

        使用上面的命令,将自己的视频按 5 秒的长度进行分隔 。

 

         执行后,会生成一个 xxx_dashinit.mp4 视频和 xxx_dash.mpd 文件:

        xxx_dashinit.mp4 是分割后的视频,xxx_dash.mpd 文件保存着分割的信息:

 

        在媒体播放器中显示该视频共 35 秒,先前使用 5 秒一个区间来进行分段,应该至少分为 7 段。

        在这里

<SegmentList timescale="16000" duration="80000">

        timescale 是一个时间的基准值,用于解释后面的 duration。也可以说他是 duration 的单位。在这里根据 timescale,duration 的单位是 1/16000 s,那么实际的 duration 为 1/16000 * 80000 = 5s,也就是每一个 SegmentURL 的分段时长(就是我们设定的五秒)。

<Initialization range="0-1463"/>

        Initialization 标签中定义的范围是视频的初始化段的字节范围。初始化段通常包含了解码媒体流所必需的信息,例如编解码器参数、帧类型、时间戳等。

        现在将源代码中的视频名称改为分段后所生成的视频名称,然后观察网页结果:

 

        未点击播放前,视频能够正常显示,并且已经有了第一个分段。

        点击播放,观察播放的过程。发现视频只能播放一个分段。原因是未能在第一个分段播放完之前及时请求第二个分段的视频流

 

        在源代码中,作者设定了五个分段:

 

         每次获取新的视频流的时机计算:

 

 

        有两个方法解决该问题:

                (a)缩小视频的分段来让每一次的请求获取更长的视频流;

                (b)修改新增视频流的时机计算方法,缩短更新的周期;

        另外,在播放之前,获取的第一个视频流的范围应当包含初始化段以及第一个有效段。

 

        在这里,第一个分段的长度应该 >= 252878,当小于该值的时候,获取到的分段不能正常解析播放:

 

        另外,在示例代码运行时,改变进度条的位置后,会使得视频停止播放(或不再请求视频流。)

自己尝试实现以及改进:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>MediaSource test</title>
</head>
<body><div> <video class="_video_" controls ></video> </div>
<script>const BASE_PATH = 'http://localhost:3000'var video = document.querySelector('._video_');var mediaSource = null;var segments = null;            // 视频段数组  [{start:0, end:1}]var totalSegments = 0;          // 总分段数var requestedSegments = [];     // 第 n - 1 段是否请求完成var sourceBuffer = null;var mimeCodec = null;var videoName = null;           // 根据视频的名称来获取视频流var segmentDuration = null;     // 每一段的时长,根据段数和视频总长计算得到,用于控制获取下一个视频流的时机var isUpdating = false;         // 是否正在进行请求和更新var shouldToSegment = 0;        // 每次用户移动视频播放定位到新的位置时,会更新其最大值var dealingSeeking = false;     // 是否正在处理一个 seeking 事件if('MediaSource' in window){(async () => {// 获取视频列表let getVideoList = await fetch(`${BASE_PATH}/get-all-video`);let tmp_videoList = await getVideoList.json();let videoList = tmp_videoList.data;// 要获取的视频的名字videoName = videoList[1].videoName// 获取指定的视频信息let getVideoInfo = await fetch(`${BASE_PATH}/get-video-info/${videoName}`);let videoInfo = await getVideoInfo.json();// 视频的分段segments = videoInfo.segments;// 视频的分段数量totalSegments = segments.length;for (var i = 0; i < totalSegments; ++i) requestedSegments[i] = false;mimeCodec = videoInfo.mimeType;// 查看是否支持该 mimeCodecif(MediaSource.isTypeSupported(mimeCodec)){mediaSource = new MediaSource;video.src = URL.createObjectURL(mediaSource);mediaSource.addEventListener('sourceopen', sourceOpen);}else{ console.log('不支持的 mimecodes') }})()}else{ console.error('不支持 MediaSource'); }function sourceOpen() {sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);// 请求第一个分段fetchAndAddSegment(0)// 视频播放的时候会触发该时间video.addEventListener('timeupdate', checkBuffer);// 视频就绪可以播放时会触发该事件video.addEventListener('canplay', function () {// video.duration 是视频的总时长,segmentDuration是每个分段的持续时间segmentDuration = video.duration / totalSegments;});// 用户已移动/跳跃到音频/视频(audio/video)中的新位置时触发;拖动滚动条也会触发(一直触发)video.addEventListener('seeking', seek);video.addEventListener('waiting', dealWaiting)mediaSource.removeEventListener('sourceopen', sourceOpen);};// 获取并添加指定长度的视频流async function fetchAndAddSegment(index) {if(isUpdating) return;if(index >= 0 && index < totalSegments && !haveAllSegments() && !sourceBuffer.updating){// 上锁isUpdating = true;let res = await fetch(`${BASE_PATH}/MP4/${videoName}`,{headers:{ 'Range':`bytes=${segments[index].start}-${segments[index].end}` }})let data = await res.arrayBuffer()requestedSegments[index] = true;sourceBuffer.appendBuffer(data);// 解锁isUpdating = false;}};// 检查是否需要请求新的段function checkBuffer(){var nextSegment = getNextSegment();if(nextSegment >= totalSegments && haveAllSegments()) {console.log('已是最后一个分段');if(mediaSource.readyState === 'open'){ mediaSource.endOfStream(); }video.removeEventListener('timeupdate', checkBuffer);video.removeEventListener('seeking', seek)video.removeEventListener('waiting', dealWaiting);}else if(shouldFetchNextSegment(nextSegment)){console.log(`请求下一个分段,当前视频时间节点:${video.currentTime}, 下一个分段;${nextSegment}`);fetchAndAddSegment(nextSegment);}};// 进度条人为改变时触发const seek = ()=>{console.log('seek')if(haveAllSegments() || mediaSource.readyState != 'open'){ return }else{// 当前的时间节点const currentTime = video.currentTime;// 应该追加到第几段let newShouldToSegment = Math.ceil(currentTime / segmentDuration / 0.5 + 1);// 是否应该获取更多的片段if(newShouldToSegment <= shouldToSegment) return;// 如果应该请求的分段较多,还差一个分段就能完成全部视频的加载,那么直接包含他else shouldToSegment = newShouldToSegment < totalSegments - 2 ? newShouldToSegment : totalSegments - 1;if(dealingSeeking || haveAllSegments()){ return; }else{// 上锁dealingSeeking = true;let i = 0;// 等待上一次更新完while(sourceBuffer.updating){ console.log(sourceBuffer.updating);i ++;if(i > 1000) return;}// 移除进度条发生变化时的监听事件,避免冲突video.removeEventListener('timeupdate', checkBuffer);// 持续检查并获取视频流片段const continueRequestSegment = ()=>{checkBuffer()let nextSegment = getNextSegment();if(nextSegment > shouldToSegment && requestedSegments[nextSegment - 1] || haveAllSegments()){console.log('移除 updateend 事件')sourceBuffer.removeEventListener('updateend', continueRequestSegment);if(!haveAllSegments()){console.log('重新添加 timeupdate 事件')video.addEventListener('timeupdate', checkBuffer);}// 解锁dealingSeeking = false;}}// 先添加 buffer 追加完成事件sourceBuffer.addEventListener('updateend', continueRequestSegment)// 检查完成后,如果需要请求新的分段,那么会在追加完成新的buffer后触发上面的 updateend 事件checkBuffer();}}}// 如果出现等待const dealWaiting = () =>{checkBuffer();video.addEventListener('timeupdate', checkBuffer);}// 获取下一个应该请求的分段const getNextSegment = () => {return requestedSegments.lastIndexOf(true) + 1}// 是否已获取完所有的分段const haveAllSegments = ()=> {return !requestedSegments.includes(false)}// 判断获取下一个分段的时机是否成熟function shouldFetchNextSegment(nextSegment) {return (video.currentTime > segmentDuration * (nextSegment - 1) * 0.5 && !requestedSegments[nextSegment] && nextSegment < totalSegments) || !requestedSegments[1];};
</script>
</body>
</html>

express:

const express = require('express');
const fs = require('fs')
const path = require('path')
require('./config/mongo_config')
const videoModel = require('./model/videos_model')const app = express();app.use(express.json());
app.use(express.urlencoded({extended: true}));app.use((req, res, next)=>{res.setHeader('Access-Control-Allow-Origin', '*')next()
})// 将/MP4设置为静态资源目录;访问测试 http://localhost:3000/MP4/xiaoli_5s_dashinit.mp4
app.use('/MP4', express.static(path.join(__dirname, 'MP4')))// 获取所有视频列表
app.get('/get-all-video', (req, res)=>{videoModel.find({},{videoName:1, _id:0}).then(data =>{res.json({result:true,data:data})}).catch(err=>{console.log(err);res.json();})
})// 获取视频的分段列表
app.get('/get-video-info/:videoName', (req, res)=>{let videoName = req.params.videoName;if(!videoName){res.json({result:false, msg:'缺少参数'})}else{videoModel.findOne({videoName: videoName}).then(data =>{if(data){res.json({result:true, videoName:data.videoName, segments:data.segments,mimeType:data.mimeType})}else{res.json({result:false, msg:'没有数据'})                }}).catch(err => {console.log(err)res.json({})})}
})app.use((req, res)=>{ res.status(404) })app.listen(3000, ()=>{console.log('3000 listening')})

一些处理和获取视频信息的代码: 

const {exec} = require('child_process')
const path = require('path')
const fs = require('fs')let videoName = '一路向北.mp4'
let videoDashName = videoName.split('.').join('_dashinit.')
let videoDashMpd = videoName.replace('.mp4', '_dash.mpd')// 按 5s 一个区间分割视频
// exec(`mp4box -dash 5000 -rap -frag-rap ${videoName}`, (err, stdout, stderr)=>{
//     if(err){
//         console.log('执行错误')
//     }else{
//         console.log(stdout);
//         console.log(stderr);
//     }
// })// 读取视频分段信息
// fs.readFile(videoDashMpd, 'utf-8', (err, data)=>{
//     if(err){
//         console.log('读取错误');
//     }else{
//         var re_ = /<SegmentURL .*>/g
//         let res = [...data.matchAll(re_)]
//             .map((e, i) => {
//                 let arr = e[0].split('="')[1].split('"')[0].split('-')
//                 return {start: i === 0 ? 0 : parseInt(arr[0]), end: parseInt(arr[1])}
//             })
//         console.log(res)
//     }
// })// 获取 mimeCodec 信息(也可以从 .mpd 文件中获取)
// exec(`mp4box -info ${videoDashName}`,(err, stdout, stderr)=>{
//     if(err){
//         console.log('执行错误')
//     }else{
//         // console.log(stdout);
//         // console.log(stderr);
//         // 需要从 stderr 中获取输入的信息,而不是stdout
//         let arr = stderr.split('\n')
//             .filter(e => e.includes('Codec Parameters'))
//             .map(e => e.split(':')[1].trim())
//         let mimeCodec = `video/mp4; codecs="${arr[0]}, ${arr[1]}"`
//     }
// })

 mongodb 中某项的键值对:

这篇关于一步一步学习使用 MediaSource 实现动态媒体流的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/902430

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Linux使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本