背景

承接这篇文章:ffmpeg的安装与简单使用

有一些视频不是被广泛支持的格式,在浏览器上无法播放。在H5时代,MSE是被广泛应用的一种媒体播放技术。所以我的思路是,在浏览器页面上点击文件时,前端打开websocket,发送请求媒体文件,后端接到请求后使用ffmpeg开始转码,然后不落盘直接pipe:到MSE,然后前端就能使用MSE进行播放了。

实现

开启websocket

前端:
使用socket-io:https://socket.io/docs/v4/server-installation/

import { io } from "socket.io-client";
var socket = io({
    transports: ["websocket", "polling"],
    closeOnBeforeunload: false,
    rememberUpgrade: true,
    withCredentials: true,
    timeout: 43200000,
});

socket.io.engine.on("connection_error", (err) => {
    console.log(err.req);      // the request object
    console.log(err.code);     // the error code, for example 1
    console.log(err.message);  // the error message, for example "Session ID unknown"
    console.log(err.context);  // some additional error context
});

socket.on('connect', function () {
    console.log("socket connect")
});

socket.on("disconnect", (err) => {
    console.log(err)
    console.log("socket disconnect");
    // socket.connect();
    // socket.emit('movieStream', {
    //     moviePath: moivePathCurrent.current + "/" + event.target.innerHTML,
    //     ss: ss,
    //     t: t,
    // });
});

socket.on("connect_error", (err) => {
    console.log(`connect_error due to`);
    console.log(err.req + " | " + err.message + " | " + err.context)
    socket.disconnect();
    console.log("change to polling")
    socket.io.opts.transports = ["polling"];
    socket.connect();
});

后端:
使用flask-socketio:https://flask-socketio.readthedocs.io/en/latest/intro.html#in...

from flask_socketio import SocketIO
from flask_socketio import send, emit
app = Flask(__name__)
app.config['SECRET_KEY'] = '{your unique secret}'
app.logger.setLevel(logging.INFO)
socketio = SocketIO(app, cors_allowed_origins="*", ping_timeout=43200000)

@socketio.on('connect')
def handle_message(data):
    print('connnected')
    emit("topic", data)

开启MSE

官网:https://developer.mozilla.org/en-US/docs/Web/API/MediaSource
JS:

const video = document.querySelector('video');
const mime = 'video/mp4; codecs="avc1.64001F, mp4a.40.2"';

var mediaSource = new MediaSource();

video.src = URL.createObjectURL(mediaSource);

mediaSource.addEventListener('sourceopen', onSourceOpen);
function onSourceOpen(_) {
}

HTML:

<video id="movieScreen" controls width="70%" preload="auto">
    {/* <source src={dirpath + "/TV.Episodes/Harley.Quinn/HarleyQuinn.S01E01"} type="video/mp4" /> */}
    {/* <source src={"/movie/my_video_manifest.mpd"}></source> */}
    Sorry, NO moive available now.
</video>

python后端转码

使用subprocess:https://docs.python.org/3/library/subprocess.html
该段内容放在后端socket处理函数中,如上述的def handle_message(data):

ffmpeg_command = [
    "ffmpeg",
    "-ss", str(ss),
    "-t", str(t),
    "-i", video_path,
    "-vn",
    "-f", "webm",
    "-c:a", "libvorbis",
    "pipe:",
]

print(ffmpeg_command)

pipe = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

outs, errs = pipe.communicate()
pipe.wait()

if outs:
    print("successfully this segment!")
    emit("audio", outs)
else:
    print("error: ",errs)

问题与回答

基本的实现应该照搬上面的代码,可能稍微自适应修改一下就可以了。

接下来的问题是:

  1. websocket该传什么内容?
  2. 使用ffmpeg该转什么格式?
  3. MSE该怎么使用?
  4. 为啥分割后的视频在MSE拼接的时候,段与段之间会有短暂的停顿?

回答

1. websocket该传什么内容?

websocket传送的内容取决于MSE接受什么数据。根据官网:https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer...

The appendBuffer() method of the SourceBuffer interface appends media segment data from an ArrayBuffer, a TypedArray or a DataView object to the SourceBuffer.

我们如果使用MSE。我们应该使用ArrayBuffer或者TypedArray或者DataView object

2. 使用ffmpeg该转什么格式?

这个同样应该要问MSE能够播放什么格式的音视频:

https://developer.mozilla.org/en-US/docs/Web/API/Media_Source...

Currently, MP4 containers with H.264 video and AAC audio codecs have support across all modern browsers, while others don't.

https://developer.mozilla.org/en-US/docs/Web/API/Media_Source...

While browser support for the various media containers with MSE is spotty, usage of the H.264 video codec, AAC audio codec, and MP4 container format is a common baseline.

所以,我尽可能将视频转换成为MP4容器H.264视频AAC音频

3. MSE该怎么使用?

MSE

其实比较简单的,可以参考本篇文章的章节:开启MSE

从后端传来的数据,通过appendBuffer方法塞到SourceBuffer中去。
通过addSourceBuffer方法,创建新的SourceBufferMediaSource中去。
通过URL.createObjectURL()方法,将videoMediaSource建立联系。

注意:MSE多个视频轨道只能播放SourceBufferList排名第一的那个轨道

4. 为啥分割后的视频在MSE拼接的时候,段与段之间会有短暂的停顿?

说来非常惭愧,这个问题困扰了我起码2个月。尝试了无数的方式想要消除停顿的0.1秒。 当然现在说起就是上面的一句话,而且这些失败的尝试大概率也不会写在文章中,一将成名万骨枯!

原因是:
使用ffmpeg切割的时候,视频和音频并不会完全按照指定的时间间隔分割,会多出来或者少一点。尤其当需要重新编码的情况下,尤其不可避免。

ffmpeg切割通常使用的就是如下命令:

ffmpeg -ss {开始秒数/时间点} -t {切割多长} -i {input} {output}
ffmpeg -ss {开始秒数/时间点} -to {结束秒数/时间点} -i {input} {output}

-ss -t -to参数当然是作为input的参数最好。但是因为视频存在关键帧或者各种格式之间不不可调和的问题,音频不知道啥原因。可以看到截取后,音视频都有几ms的延长:
ffmpeg longer duration
track 2是音频,上面的信息就是视频的。关注点在duration with fragments,视频多了17ms,音频多了33ms。

为了得到图上的数据,你需要安装Bento4https://www.bento4.com/downloads/。提供了很多分析MP4格式的工具。

我的解决办法:
在后台不直接将原视频切割成小块。而是,分成两轨,视频轨和音频轨。因为视频轨可以在ffmpeg参数中加上-filter:v fps=30来指定framerate,从而让ffmpeg自动抽帧,获得指定时间的视频片段。相信我,在浏览器上播放根本感觉不出来抽帧差别。

音频的话,可以不分段,或者直接分一个很长的段,比如600s。从而减少误差。

送到MSE的时候,分别创建一个SourceBuffer(轨道,track),一个用来放视频,一个用来放音频。我音频后来采用的webm容器,编码libvorbis。

前端js:

const audioSourceBuffer = mediaSource.addSourceBuffer('audio/webm;codecs=vorbis');
const sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.64001F, mp4a.40.2"');

音频ffmpeg:

ffmpeg_command = [
  "ffmpeg",
  "-ss", str(ss),
  "-t", str(t),
  "-i", video_path,
  "-vn",
  "-f", "webm",
  "-c:a", "libvorbis",
  "pipe:",
]

视频ffmpeg:

ffmpeg_command = [
    "ffmpeg",
    "-an",
    "-ss", str(ss),
    "-t", str(t),
    "-i", video_path,
    "-f", "mp4",
    "-c:v", "libx264",
    "-filter:v", "fps=30",
    "-movflags", "frag_keyframe+default_base_moof+empty_moov",
    "pipe:",
]

后记

两个月的精华总结下来就只有上面这么多。

部分还能找得到的有用的参考:

MDN上关于MSE的示例:https://github.com/nickdesaulniers/netfix/blob/gh-pages/demo/bufferWhenNeeded.html
MSE支持的mimetype:https://chromium.googlesource.com/external/w3c/web-platform-t...
mimetype集合:https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_H...
如何就转成MSE支持的格式:https://developer.mozilla.org/en-US/docs/Web/API/Media_Source...
如何使用DASH:https://developer.mozilla.org/en-US/docs/Web/Media/DASH_Adapt...
ffmpeg改变framerate:https://trac.ffmpeg.org/wiki/ChangingFrameRate
ffmpeg合并多个视频:https://trac.ffmpeg.org/wiki/Concatenate
dash.js:https://github.com/Dash-Industry-Forum/dash.js/
MSE示例:https://www.cnblogs.com/Tomato-wang/p/15353705.html
解决之道受到这个回答的启发:https://stackoverflow.com/questions/64839836/playing-one-of-multiple-audio-tracks-in-sync-with-a-video
一些知识点:https://developer.huawei.com/consumer/cn/forum/topic/02024325...


BreezingSummer
45 声望0 粉丝