音视频学习
基础概念
帧率
视频的本质是连续播放的图片,帧率表示着一秒钟刷新的图片的帧数,帧率为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
音频编码
常见音频编码方式:
- 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)来实现各种定制化的需求,比如可以实现以下特性:
- HTML5本身不支持FLV、HLS(m3u8)容器格式,但我们可以先通过fetch或ajax手动请求资源数据,再将其转化为分段式MP4格式(FMP4),再通过MediaSource进行播放,从而实现对多种容器格式的兼容。
- 请求视频资源时可以通过持续发送请求的形式来进行分段请求(如可以通过Range头部实现),并依次通过appendBuffer来实现流式加载,从而可以实现分辨率的动态切换、视频源的切换、直播功能。(以前想要实现分辨率的切换本质是修改video的src属性,重新加载新的视频源时可能会卡一下)
在介绍具体的实现之前,我们可以先观察一下主流的视频网站(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,这会触发mediaSource的sourceopen
事件,此时我们可以创建一个或多个sourceBuffer,然后发送请求并把获取到的媒体资源通过appendBuffer
进行加载。
在这个例子中我们只发送了一个请求来获取完整的多媒体数据,在实际的应用中会使用多个请求来获取分段数据。
参考:https://medium.com/canal-tech/how-video-streaming-works-on-the-web-an-introduction-7919739f7e1