一步一步学习使用 MediaSource 实现动态媒体流
学习前的参考
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):
最后考虑是视频本身的格式问题。
参考:
应该将 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;
// 查看是否支持该 mimeCodec
if(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 中某项的键值对:
原文地址:https://blog.csdn.net/hao_13/article/details/137609377
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!