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

news2025/9/21 5:50:27

学习前的参考

为什么视频网站的视频链接地址是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;

            // 查看是否支持该 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 中某项的键值对:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1592744.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

量子信息产业生态研究(一):关于《量子技术公司营销指南(2023)》的讨论

写在前面。量子行业媒体量子内参&#xff08;Quantum Insider&#xff09;编制的《量子技术公司营销指南》是一本实用的英文手册&#xff0c;它旨在帮助量子科技公司建立有效的营销策略&#xff0c;同时了解如何将自己定位成各自的行业专家。本文对这篇指南的主要内容进行了翻译…

C语言中的编译和链接

系列文章目录 文章目录 ​编辑 系列文章目录 文章目录 前言 一、 翻译环境和运行环境 二、 翻译环境 2.1 编译 2.1.1 预处理 2.1.2 编译 2.1.2.1 词法分析 : 2.1.2.2 语法分析 2.1.2.3 语义分析 2.1.3 汇编 2.2 链接 三、运行环境 前言 在我们平常的写代码时&#xff0c;我们很…

set 类 和 map 类

1. 关联式容器 关联式容器也是用来存储数据的&#xff0c;与序列式容器不同的是&#xff0c;其里面存储的是<key, value>结构的 键值对&#xff0c;在数据检索时比序列式容器效率更高 2. 键值对 用来表示具有一一对应关系的一种结构&#xff0c;该结构中一般只包含…

你的系统是如何跟MySQL打交道的

1、Java 工程师眼中的数据库是什么东西? 从今天开始&#xff0c;我们将要开始一个MySQL的专栏&#xff0c;一起来研究MySQL数据库的底层原理和各种实践案例&#xff0c;以及互联网公司的技术方案。 现在我们先来看看&#xff0c;在一个Java工程师眼中的数据库是什么东西? 平时…

开源 Ruo-Yi 项目引入 Mybatis-Plus:3.5.3 报错ClassNotFoundException:

开源 Ruo-Yi 项目引入 Mybatis-Plus:3.5.3 报错ClassNotFoundException&#xff1a; Caused by: java.lang.ClassNotFoundException: com.baomidou.mybatisplus.extension.plugins.MybatisPlusInter1 分析问题 控制台报错说明我们引入的 mybatis-plus 的依赖里找不到com.baom…

怎么恢复删除的回收站数据?分享多种恢复方法

在日常使用电脑的过程中&#xff0c;回收站是我们经常打交道的一个功能。它帮助我们管理不再需要的文件&#xff0c;但有时候&#xff0c;我们可能会不小心删除了重要文件&#xff0c;或者误清空了回收站。那么&#xff0c;面对这种情况&#xff0c;我们该如何恢复删除的回收站…

小程序面试题之性能优化提高10道

1.你使用过哪些方法&#xff0c;来提高微信小程序的应用速度&#xff1f; 提高页面加载速度 用户行为预测 减少默认data的大小 组件化方案 控制包的大小 压缩代码&#xff0c;清理无用代码 采用分包策略 启用本地缓存 参考地址&#xff1a;https://blog.csdn.net/wu_xianqiang/…

Linux上的可执行文件在Windows上是不能运行的

一、概要 1、可执行文件的格式 Linux上的可执行文件是elf格式的 Windows上的可执行文件是exe格式的 Linux上的可执行文件在Windows上是不能运行的 2、程序的普通构建与静态构建 普通构建&#xff1a; 一个.c文件&#xff0c;用gcc命令编译成可执行文件(程序)&#xff0c…

数据结构排序篇上

排序的概念及其运用 排序的概念 排序 &#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 稳定性 &#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&…

分类预测 | Matlab实现基于迁移学习和GASF-CNN-Mutilhead-Attention格拉姆角场和卷积网络多头注意力机制多特征分类预测/故障识别

分类预测 | Matlab实现基于迁移学习和GASF-CNN-Mutilhead-Attention格拉姆角场和卷积网络多头注意力机制多特征分类预测/故障识别 目录 分类预测 | Matlab实现基于迁移学习和GASF-CNN-Mutilhead-Attention格拉姆角场和卷积网络多头注意力机制多特征分类预测/故障识别分类效果基…

动态规划原理及其在优化问题中的应用解析

动态规划原理及其在优化问题中的应用解析 一、最优子结构二、重叠子问题三、何时使用动态规划法四、伪代码示例五、C代码示例七、详细说明动态规划原理7.1、最优子结构7.2 重叠子问题7.3 动态规划的实现 八、结论 动态规划是一种解决优化问题的方法&#xff0c;它通过将原问题分…

神经网络--反向传播算法推导

神经网络–反向传播算法推导 文章目录 神经网络--反向传播算法推导概述神经网络模型反向传导算法 概述 以监督学习为例&#xff0c;假设我们有训练样本集 ( x ( i ) , y ( i ) ) (x^{(i)},y^{(i)}) (x(i),y(i))&#xff0c;那么神经网络算法能提供一种复杂且非线性的假设模型 …

滚雪球学Java(75):Java零基础,轻松学会文件读写技巧

咦咦咦&#xff0c;各位小可爱&#xff0c;我是你们的好伙伴——bug菌&#xff0c;今天又来给大家普及Java SE相关知识点了&#xff0c;别躲起来啊&#xff0c;听我讲干货还不快点赞&#xff0c;赞多了我就有动力讲得更嗨啦&#xff01;所以呀&#xff0c;养成先点赞后阅读的好…

mp3怎样才能转换成wav格式?音频互相转换的方法

一&#xff0c;什么是WAV WAV&#xff0c;全称为波形音频文件&#xff08;Waveform Audio File Format&#xff09;&#xff0c;是一种由微软公司和IBM公司联合开发的音频文件格式。自1991年问世以来&#xff0c;WAV格式因其无损的音频质量和广泛的兼容性&#xff0c;成为了多…

【Godot4.2】CanvasItem绘图函数全解析 - 7.自定义节点TextBoard

概述 之前发布的几篇文章几乎阐述了CanvasItem绘图函数最基础的内容。 本篇结合draw_style_box()和TextParagraph类&#xff0c;自定义了一个可以自适应宽高显示多行文本&#xff0c;且带有一个样式盒作为背景的文字板节点TextBoard。 系列目录 0.概述1.绘制简单图形2.设定绘…

kali工具----域名IP及路由跟踪

域名IP及路由跟踪 测试网络范围内的IP地址或域名也是渗透测试的一个重要部分。通过测试网络范围内的IP地址或域名&#xff0c;确定是否有人入侵自己的网络中并损害系统。不少单位选择仅对局部IP基础架构进行渗透测试&#xff0c;但从现在的安全形势来看&#xff0c;只有对整个I…

RHCE--dns正反向解析小实验

一、准备工作 1.关闭防火墙 [rootserver ~]# setenforce 0 [rootserver ~]# systemctl stop firewalld 2.安装软件 [rootserver ~]# yum install bind -y 二、正向解析 服务端IP客户端IP网址192.168.203.128192.168.203.130www.openlab.com 服务端配置静态ip [root…

人工智能|机器学习——基于机器学习的信用卡办卡意愿模型预测项目

一、背景介绍 在金融领域&#xff0c;了解客户的信用卡办卡意愿对于银行和金融机构至关重要。借助机器学习技术&#xff0c;我们可以根据客户的历史数据和行为模式预测其是否有办理信用卡的倾向。本项目通过Python中的机器学习库&#xff0c;构建了两个常用的分类模型&#xff…

Mistral AI突围:开源大模型Mixtral 8x22B颠覆行业格局

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Spark-Scala语言实战(16)

在之前的文章中&#xff0c;我们学习了三道任务&#xff0c;运用之前学到的方法。想了解的朋友可以查看这篇文章。同时&#xff0c;希望我的文章能帮助到你&#xff0c;如果觉得我的文章写的不错&#xff0c;请留下你宝贵的点赞&#xff0c;谢谢。 Spark-Scala语言实战&#x…