跳到主要内容

音视频学习

基础概念

帧率

视频的本质是连续播放的图片,帧率表示着一秒钟刷新的图片的帧数,帧率为24FPS以上时人脑就会觉得比较流畅了,因此一般的动画或者电影的帧率为24FPS或30FPS。

分辨率

即每帧图片的分辨率,如1080P的视频指的是19201080像素,每一个像素都由RGB三个通道构成,即使用3 8位来表达。

码率

码率指的是单位时间内传输的比特数,对于一个给定24帧率、1080P的视频来说,它的码率为24 1920 1080 3 8比特每秒,约等于142MB每秒,可见未压缩的视频容量是很大的,所以我们需要采用视频编码来对视频进行压缩。

视频编码

视频编码即去除数据中冗余信息的压缩技术。

  • 软编码:使用CPU能力进行编码/解码,如使用JS或者WASM进行编码/解码
  • 硬编码:使用GPU能力进行编码/解码,如使用Video标签,或者Web Codecs进行编码/解码,性能更好。

常见视频编码方式:

  • H264(AVC1)
  • H265(HEVC)
  • VP8
  • VP9
  • AV1

参考https://www.zzsin.com/catalog/write_avc_decoder.html

音频编码

常见音频编码方式:

  • AAC
  • MP3
  • WAV
  • AC-3

封装格式

将编码后的视频、音频、以及如标题、时长、分辨率等信息打包再一起的文件,被称作封装格式(容器格式)。

常见音频编码方式:

  • MP4
  • MKV
  • WebM(基于MKV,主要视频编码格式为VP8、VP9)
  • MOV(Apple的QuickTime)
  • FLV
  • TS
  • AVI

编解码流程

完整流程如下:音频和视频采集完成后分别进行音频编码和视频编码、对编码后的产物进行封装,传输后在客户端进行解封装从而分离出音频和视频,再分别进行解码得到原始的音频和视频,再进行音视频同步,最后进行播放。

编解码流程

图片编码

常见图片编码方式:

  • PNG(无损压缩)
  • JPG(有损压缩)
  • JPEG
  • GIF

FFMPEG

ffprobe

查看文件信息

ffprobe input.mkv

ffplay

播放文件

ffplay input.mkv

常用命令

ffmpeg [全局选项] [输入选项] [-i 输入文件] [输出选项] [输出文件]

封装格式转换

ffmpeg -i input.mkv output.mp4

指定编码格式

对于命令ffmpeg -i input.mkv output.mkv,即使我们并没有改变容器格式,ffmpeg却可能会默认修改视频和音频的编码方式。因此,我们需要考虑手动指定编码方式,或者通过copy告诉ffmpeg来保留原本的编码格式。

ffmpeg -i input.mkv -codec:v libx265 output.mkv # 视频编码格式为h265
ffmpeg -i input.mkv -vcodec libx265 output.mkv # -vcodec 等于 -codec:v
ffmpeg -i input.mkv -c:v libx265 output.mkv # -c:v 等于 -codec:v

ffmpeg -i input.mkv -codec:a aac output.mkv # 音频编码格式为aac
ffmpeg -i input.mkv -acodec aac output.mkv # -acodec 等于 -codec:a
ffmpeg -i input.mkv -c:a aac output.mkv # -c:a 等于 -codec:a

ffmpeg -i input.mkv -c:v copy output.mkv # 通过copy指定不改变原本的视频编码格式
ffmpeg -i input.mkv -c:a copy output.mkv # 通过copy指定不改变原本的音频编码格式

去除视频流/音频流

ffmpeg -i input.mkv -vn -c:a copy output.mkv # 去除视频流
ffmpeg -i input.mkv -an -c:a copy output.mkv # 去除音频流
ffmpeg -i input.mkv -sn -c:a copy output.mkv # 去除字幕流

视频时长裁剪

ffmpeg -ss 00:10:00 -to 00:11:00 -i input.mkv output.mkv # 裁剪原视频的10分到11分

视频帧率改变

ffmpeg -y -i input.mkv -r 1 -c:v libx265 -c:a copy output.mkv # -r 1 设置为1帧

视频分辨率改变

TODO 两种方式的区别?

ffmpeg -y -i input.mkv -s 3840x2160  output.mkv 
ffmpeg -y -i input.mkv -vf scale=320:320 output.mkv # vf指的是视频滤镜

视频写入metadata

ffmpeg -i input.mkv -metadata author="akara" -c:v copy -c:a copy output.mkv

通过观察ffprobe打印的消息,我们知道有好几个层级的metadata,我们也可以写入到特定流当中。

ffmpeg -i input.mkv -metadata:s author="akara" -c:v copy -c:a copy output.mkv # 写到Stream的metadata
ffmpeg -i input.mkv -metadata:s:a:0 author="akara" -c:v copy -c:a copy output.mkv # 写到第一个音频流的metadata

ffmpeg -i input.mkv -metadata:s:a:0 encoder="" -c:v copy -c:a copy output.mkv # 删掉现有的metadata字段

提取视频帧

ffmpeg -i output.mkv -f image2 ./images/%d.png # -f images2 一定需要么
ffmpeg -i output.mkv -f image2 -r 1 ./images/%d.png # -r 1通过帧率控制生成图的间隔

图片生成视频

ffmpeg -r 2 -i %d.png output.mp4 # 生成2FPS的视频
ffmpeg -pattern_type glob -i '*.png' color.gif

图片解码成raw data

ffmpeg -i input.png -pix_fmt rgb32 output.rgb # rgb32
ffmpeg -i input.png -pix_fmt yuv444p 444.yuv # yuv444p
ffmpeg -i input.png -pix_fmt yuv422p 422.yuv # yuv422p 444的三分之一存储
ffmpeg -i input.png -pix_fmt yuv420p 420.yuv # yuv444p 444的二分之一存储

raw data编码成图片

ffmpeg -s 2x2 -pix_fmt yuv444p -i input.yuv output.jpg # 从而实现图片格式的转换

WebCodecs

Video Frame

const image = new Image();
image.src = './red.png';
image.onload = function() {
const frame = new VideoFrame(image, {
timestamp: 0
})
}

Video Encoder

const encoder = new VideoEncoder({
output(chunk, metadata) {
console.log('chunk', chunk, metadata)
},
error: console.error
})

encoder.configure({
codec: 'vp8',
width: 200,
height: 200
})
encoder.encode(frame)

Video Decoder

const decoder = new VideoDecoder({
output(frame) {
console.log('frame', frame)
},
error: console.error,
})

decoder.configure(config)
decoder.decode(chunks[0])

Image Decoder

fetch('./red.png').then(res => {
const imageDecoder = new ImageDecoder({
data: res.body,
type: 'image/png'
})

imageDecoder.decode({
frameIndex: 0,
}).then(result => {
// ctx.drawImage(result.image, 0, 0)
const buffer = new Uint8Array(result.image.allocationSize())
result.image.copyTo(buffer)
console.log('???', buffer) // raw data
})
})

WebRTC

MediaStream

利用浏览器的API我们能够获取屏幕、摄像机、麦克风,Video、Audio甚至Canvas元素的流数据。可以通过srcObject将流赋给媒体元素,也可以使用MediaRecorder实现录制功能。

const stream2 = new Media(stream.getVideoTracks());

video.srcObject = stream;

getDisplayMedia

const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true
})

getUserMedia

const stream = navigator.mediaDevices.getUserMedia({
video: true,
audio: true
})

captureMedia

const stream = canvas.captureMedia()
const stream2 = video.captureMedia()
const stream3 = audio.captureMedia()

MediaRecorder

const recorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs="h264,opus',
// mimeType: 'video/webm;codecs="vp9,opus"',
});

recorder.start(3000)
recorder.stop()
recorder.pause()
recorder.resume()
recorder.ondataavailable = function() {}

AudioContext

TODO

Media Source Extension

在HTML5中最简单的音视频播放方式是直接将资源链接传入video标签的src属性。这种加载方式也被称为流式加载,即并不需要等待资源完全下载好后才开始音视频的播放,而是一边加载数据一边进行播放。通过观察控制台可以发现,在加载一个大体积视频的时候,通常会发送多个请求来分段请求该资源,每个请求都会带着Range: byteds=<start>-的请求头,响应的状态码是206,响应头包括Content-Length: <length>; Content-Range: byted <start>-<end>/<length>

上述方式是直接将加载后的视频数据交给video自动进行解封装、解码、播放,这种方式虽然实现简单但也限制了我们扩展其功能的能力,实际上目前主流的视频播放器都不是使用这种原生方式播放视频的,相反它们都是借助媒体扩展(Media Source Extension)来实现各种定制化的需求,比如可以实现以下特性:

  1. HTML5本身不支持FLV、HLS(m3u8)容器格式,但我们可以先通过fetchajax手动请求资源数据,再将其转化为分段式MP4格式(FMP4),再通过MediaSource进行播放,从而实现对多种容器格式的兼容。
  2. 请求视频资源时可以通过持续发送请求的形式来进行分段请求(如可以通过Range头部实现),并依次通过appendBuffer来实现流式加载,从而可以实现分辨率的动态切换、视频源的切换、直播功能。(以前想要实现分辨率的切换本质是修改videosrc属性,重新加载新的视频源时可能会卡一下)

在介绍具体的实现之前,我们可以先观察一下主流的视频网站(Youtube、B站等)的播放器的行为。首先能够观察到它们的video标签的src并不是指向着一个真实存在的URL地址,而是一个类似blob:https://www.bilibili.com/ed89dd41-bff1-427e-80c4-fa796d34c3cb的虚拟URL;另外观察网络栏可以发现,在视频的播放过程中会持续发送请求来加载数据,一般来说每次请求的数据大小为几百KB。

接下来将会简单介绍MediaSource的API以及使用方式。

 <body>
<video id="video"></video>
<script>
const video = document.getElementById("video");
const mediaSource = new MediaSource();
mediaSource.addEventListener("sourceopen", function () {
const mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
fetch("http://server.com/my-video.mp4")
.then((res) => res.arrayBuffer())
.then((buf) => {
sourceBuffer.addEventListener("updateend", function () {
mediaSource.endOfStream();
video.play();
});
sourceBuffer.appendBuffer(buf);
});
});
const url = URL.createObjectURL(mediaSource);
video.src = url;
</script>
</body>

首先通过new MediaSource()创建mediaSource实例,通过URL.createObjectURL创建虚拟地址,然后把虚拟地址传给video,这会触发mediaSourcesourceopen事件,此时我们可以创建一个或多个sourceBuffer,然后发送请求并把获取到的媒体资源通过appendBuffer进行加载。

在这个例子中我们只发送了一个请求来获取完整的多媒体数据,在实际的应用中会使用多个请求来获取分段数据。

参考:https://medium.com/canal-tech/how-video-streaming-works-on-the-web-an-introduction-7919739f7e1