整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

初识视频原理和ffmpeg

初识视频原理和ffmpeg

文导读

阅读本文你将获得以下知识:

了解视频的基本原理。

了解 FFmpeg 是什么,和一些常用的用法。

用 FFmpeg 搭建简单的视频直播推流。

FFmpeg 在 NodeJS 中的一些用法。

背景

短视频大行其道的年代,作为程序员势必需要了解:视频编辑背后的原理和技术。本文简略的描述了视频的组成原理和常用的视频编辑工具,以及在 NodeJS 中的用法。

想要了解视频原理,首先应该从图像原理开始说起。

图像基础

1. 像素

图像画面由一个数字序列表示的图像中的一个最小单位色块,被称之像素(pixel/px)。

注意:像素只有位图才会有,是用来记录位图图像的。

我们所说的图像大小为1920*1080,指的就是长宽各有 1920 和 1080 的像素点,那么一张1920*1080的图片总共有的像素点为:1920*1080=2073600个像素点。

图像的大小如何计算?

图像的大小:像素数量 * 像素大小=图片大小,而 像素大小 和 像素深度1有关系。RGB表示的真彩色能表示256×256×256=16,777,216,就是我们常见的1600万色,是人眼可见的全部色彩,超出没有意义。RGB的像素深度有1bit、4bit、8bit、16bit、24bit、32bit,如在ps中下图在新建一张画布选择8bit(指 rgb 每种颜色占 8bit),那这样1 px=3 * 8bit=24bit,俗称24 位图。根据以上公式就能算出如下图图像的大小:500 * 378 * 24 / 8=567000Byte=567000Byte / 1024=553.7109375 Kb,和 ps 显示的图像大小一样。

但往往真实的图片大小远比以上计算的结果小很多, 这是因为导出的图片都经过压缩的,关于图片压缩技术可自行搜索学习。

视频基础

1. 视频和图像的关系?

视频就是图片一帧一帧连起来的产物,连起来的越快看着越流畅,用 帧率(就是每秒播放图片的数量 FPS)来衡量视频的流畅度。那么根据图片大小的算法就能算出视频的大小。

视频的大小=时长(秒) * 帧率(FPS)* 图片大小;

那么1920×1280分辨率, 30FPS,时长 1 秒的视频的大小就是:1920 * 1280 * 24 / 8 * 30 / 1024 / 1024=210.9375 M,那么 1 小时的影片需要:210.9 * 60 * 60 / 1024=741.4453125 G,不禁产生疑问,为啥我下载的大片才 1G 多?莫慌,视频要是这么简单,那我们就太天真了,所以就有了下文 「视频编码」

2. 视频是怎么来的?

几个概念

  • 帧(Frame):就是一张静止的画面, 是视频的最小单位。
  • 帧速率(FPS):每秒播放图片的数量。
  • 码率(Bit Rate):视频文件在单位时间内使用的数据流量,决定视频的质量和大小,单位是 kb/s 或者 Mb/s。一般来说同样分辨率下,视频文件的码流越大,压缩比就越小,画面质量就越高。码流越大,说明单位时间内取样率越大,数据流,精度就越高,处理出来的文件就越接近原始文件,图像质量越好,画质越清晰,要求播放设备的解码能力也越高。码率的常见三种模式:- CBR - 全程码率恒定 - 文件大小可预测 - 编码压力小,直播常用- VBR - 码率可变 - 简单场景码率低,复杂场景码率高- CRF - 固定质量模式 - CRF值越低,视频看起来质量越高

视频构成

视频和音频就像是饭和菜,封装格式就相当于碗。

注意: 下文所有视频均代表包含音频的视频。

1. 视频封装格式

常见封装格式有 MP4、AVI、FLV、mov、RMVB、MKV、WMV、3GP、ASF 等。

2. 编码格式

视频编码是对采用视频压缩算法将一种视频格式转换成另一种视频格式的描述,音频编码同理。

常见的视频编码格式有:AC-1、MPEG2/H.262、VP8、MPEG4、VP9、H.261、H.263、H.264、H.265 等。

常见的音频编码格式有:WMA、MP3、AC-3、AAC、APE、FLAC、WAV 等。

视频压缩原理

主要是将视频像素数据(RGB,YUV 等)压缩成为视频码流,从而降低视频的数据量,也就是处理像素。

YUV: 和RGB一样是一种颜色编码格式,相比RGB更利于压缩。其中"Y"表示明亮度(Lumina nce 或 Luma),也就是灰阶值;而"U"和"V"表示的则是色度(Chrominance 或 Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。

视频压缩分为下面两种类型

1. 帧内压缩

也叫空间压缩,类似于图像压缩,属于有损压缩算法,达不到很高的压缩比。

2. 帧间压缩

主要是通过记录关键帧,通过压缩关键帧之间连续帧的冗余信息(连续帧内相同的像素区域)的过程。

为了记录关键帧,将视频的画面帧分为三类:

  • I 帧:帧内编码帧(intra picture),能展示最完整的画面, 可压缩的空间小,编码过程属于帧内编码。
  • P 帧:前向预测编码帧(predictive-frame),需要参考前面的 I 帧或者 P 帧来找出不同部分进行编码,压缩比比较高。
  • B 帧 双向预测,也就是 B 帧记录的是本帧与前后帧的差别。也就是说要解码 B 帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B 帧压缩率高,但是对解码性能要求较高。

GOP(Group of Pictures)值

编码器将多张图像进行编码后生产成一段一段的 GOP ,每一组 IPB 帧的序列包含多少帧,也就是一个 I 帧结束后需要经过多少帧才能出现下一个 I 帧。所以同码率下 GOP 值越大,B 帧和 P 帧越多,视频质量越高。

在压缩或者解压缩视频的过程用到编解码器(Codec)。总的过程可以:

视频的编码的过程:

下图来源于即时通讯网2。

视频解码的过程:

音频压缩原理

音频压缩是在保证信号在听觉方面不产生失真的前提下,对音频数据信号进行尽可能大的压缩, 去除冗余信息。冗余信号包含人耳听觉范围外的音频信号以及被掩蔽掉的音频信号等。例如,人耳所能察觉的声音信号的频率范围为 20Hz ~ 20KHz,除此之外的其它频率人耳无法察觉,都可视为冗余信号。此外,根据人耳听觉的生理和心理声学现象,当一个强音信号与一个弱音信号同时存在时,弱音信号将被强音信号所掩蔽而听不见,这样弱音信号就可以视为冗余信号而不用传送。

音频压缩不是今天的主角,想深入学习可参考如下链接:

https://baike.baidu.com/item/%E9%9F%B3%E9%A2%91%E5%8E%8B%E7%BC%A9/392863

https://www.kamilet.cn/how-audio-compression-works-and-can-you-really-tell-the-difference/

FFmpeg

1. FFmpeg 什么?

FFmpeg is a collection of libraries and tools to process multimedia content such as audio, video, subtitles and related metadata.

简单说就是一个跨平台的视频处理的程序。

2. FFmpeg 的原理

整个过程基本可以说成:解复用=> 解码=> 编码=> 复用器。

_______ ______________

| | | |

| input | demuxer | encoded data | decoder

| file | ---------> | packets | -----+

|_______| |______________| |

v

_________

| |

| decoded |

| frames |

|_________|

________ ______________ |

| | | | |

| output | <-------- | encoded data | <----+

| file | muxer | packets | encoder

|________| |______________|

3.FFmpeg 安装

FFmpeg 分为 3 个版本:Static、 Shared、 Dev

Mac 安装:

brew install ffmpeg

其他安装请参考官网3。

4. FFmpeg 用法

它能分别对视频的的各个组成进行编码,它对音视频的编码格式支持也比较全面。例如:对视频容器的转换、音视频的压缩、视频截取、截图、滤镜、音频提取等等,非常强大。

命令行语法:

ffmpeg [全局参数] [输入文件参数] -i [输入文件] [输出文件参数] [输出文件]

视频信息:

// 获取视频信息

ffmpeg -i input.mp4


Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'input2.mp4':

Metadata:

major_brand : isom

minor_version : 512

compatible_brands: isomiso2mp41

encoder : Lavf58.29.100

description : Packed by Bilibili XCoder v2.0.2

Duration: 00:08:24.45, start: 0.000000, bitrate: 2180 kb/s // 时长,码率

Stream #0:0(und): Video: hevc (Main) (hev1 / 0x31766568), yuv420p(tv), 1920x1080 [SAR 1:1 DAR 16:9], 2046 kb/s, 25 fps, 25 tbr, 16k tbn, 25 tbc (default) // 第一个流是视频流,编码格式是hevc(封装格式为hev1),每一帧表示为yuv420p,分辨率1920*1080,码率2046kb/s, fps为25。

Metadata:

handler_name : VideoHandler

Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 128 kb/s (default) // 第二个流是音频流,编码格式是aac(封装格式为mp4a)采样率是44100 Hz,声道是立体声,码率92Kbit/s。

Metadata:

handler_name : SoundHandler

码率的转换:

ffmpeg -i input.mp4 -b:v 64k -bufsize 64k output.mp4

帧率转换:

ffmpeg -i input.mp4 -r 5 output.mp4

分辨率转换:

ffmpeg -i input.mp4 -vf scale=480:-1 output.mp4 // 1080p 转为 480p

视频倍速:

ffmpeg -i test1 "setpts=PTS/5" test4.mp4 // 视频5倍速转换

fmpeg -i input.mp4 -filter:a "atempo=2.0" 4s.mp4 // 音频2倍速播放

ffmpeg -i input.mp4 -filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]" -map "[v]" -map "[a]" -vn 4s.mp4 // 音视频同时2倍速

提取音视频:

ffmpeg -i input.mp4 -an output.mp4 //提取视频

ffmpeg -i input.mp4 -vn output.mp3 //提取音频

视频比例转换:

ffmpeg -i input.mp4 -aspect 21:9 output.mp4

视频容器转换:

ffmpeg -i input.mp4 output.avi

视频截图:

ffmpeg -ss 00:00:05 -i input.mp4 -vframes 1 -q:v 5 -f image2 pic-%03d.jpeg

// -ss 00:00:05 从第五秒开始 -vframes 1 只截取1帧 -q:v 5 图片质量1-5

视频截取:

ffmpeg -ss 00:00:02 -i input.mp4 -t 6.5 -c copy cut.mp4

ffmpeg -ss 00:00:02 -i input.mp4 -to 00:00:10 -c copy cut.mp4

连续图片或视频生成 gif 图:

ffmpeg -i output.mp4 -to 10 -r 30 -vf scale=100:-1 gg.gif // 截取视频某个部分生成gif 100:-1 指定宽度,高度保持原始比例


ffmpeg -r 5 -i pic-%03d.jpeg 11.gif // 多图生成gif


// 图片还可生成视频

ffmpeg -r 20 -i pic-%03d.jpeg gif.mp4


ffmpeg -f concat -i "concat:part1.mp4|part2.mp4|3.mp4|part4.mp4" -c copy output.mp4 // 多个视频拼接成一个

图片或视频加滤镜:

// 模糊滤镜

ffmpeg -y -i pic-012.jpeg -vf boxblur=7 blur.jpeg

// 变色

ffmpeg -i pic-012.jpeg -vf colorbalance=rm=1 colorbalance1.jpg // 调整rgb某个维度的权重实现变色。

ffmpeg -i pic-012.jpeg -vf colorchannelmixer=.3:.4:.3:0:.3:.4:.3:0:.3:.4:.3 colorchannelmixer1.jpg // 对rgba四个通道进行重新计算,并分别给定权重比例。

ffmpeg -i pic-012.jpeg -vf hue=h=30:s=1 hue1.jpg // 改变色调,相当在调色板上调色

ffmpeg -i pic-012.jpeg -vf lutyuv="y=negval:u=negval:v=negval" lutyuv1.jpg // lutyuv用于yuv颜色空间

ffmpeg -i pic-012.jpeg -vf negate=0 negate1.jpg // 反转

ffmpeg -i pic-012.jpeg -vf swapuv swapuv1.jpg // UV 互换

ffmpeg -i pic-012.jpeg -vf crop=w=200:h=300:x=500:y=800 crop1.jpg // 裁剪

添加水印:

ffmpeg -i input.mp4 -i pic-012.jpeg -filter_complex "[1:v] scale=176:144 [logo];[0:v][logo]overlay=x=0:y=0" out.mp4 //给视频添加图片水印


ffmpeg -i input.mp4 -vf "drawtext=fontsize=100:fontcolor=white:alpha=0.3:text='%{localtime\:%Y\-%m\-%d %H-%M-%S}':y=h-line_h-100:x=(w-text_w)/2" output22.mp4// 添加文字水印


ffmpeg -i input.mp4 -i pic-012.jpeg -filter_complex "[1:v] scale=176:144 [logo];[0:v][logo]overlay=x=0:y=0" out.mp4


ffmpeg -i input.mp4 -vf drawtext="fontsize=100:text='我是水印':fontcolor=green:enable=lt(mod(t\,3)\,1)" interval-sy.mp4

// t 时间,s

// mod(t\,2) 计算t%2

// lt(mod(t\,2)\,1) 如果mod(t\,2)<1,返回1,否则返回0

// enable=lt(mod(t\,2)\,1) 每隔1s显示一次水印,enable=lt(mod(t\,3)\,1) 每隔3s.

添加字幕:

// 第一步 用you-get下载B站视频

// 第二步 用 danmaku2ass.py 转换弹幕 https://github.com/m13253/danmaku2ass

// 第三步 可以用ffmpeg转换弹幕

ffpmeg -i input.ass input.srt


// 第四步 给视频添加字幕或弹幕 字幕可添加多个

ffmpeg -i input.mp4 -vf subtitles=input.ass output.mp4

为音频添加封面:

ffmpeg -loop 1 -i cover.jpg -i input.mp3 -c:v libx264 -c:a aac -b:a 192k -shortest output.mp4


// -loop 1表示一直循环, -shortest 音频结束视频输出就结束

视频画中画:

ffmpeg -re -i input.mp4 -vf "movie=output.mp4,scale=480*320[test]; [in][test] overlay [out]" -vcodec libx264 videoInvideo.mp4

多宫格:

ffmpeg -y -i input.mp4 -i input.mp4 \

-i input.mp4 -i input.mp4 \

-filter_complex "nullsrc=size=640x480[base]; \

[0:v]scale=320x240[topleft]; \

[1:v]scale=320x240[topright]; \

[2:v]scale=320x240[bottomleft]; \

[3:v]scale=320x240[bottomright]; \

[base][topleft]overlay=shortest=1[tmp1]; \

[tmp1][topright]overlay=shortest=1:x=320[tmp2]; \

[tmp2][bottomleft]overlay=shortest=1:y=240[tmp3]; \

[tmp3][bottomright]overlay=shortest=1:x=320:y=240" \

-vcodec libx264 9_video_filtered.flv

// nullsrc创建画布

视频压缩:

ffmpeg -i input.mp3 -ab 128 output.mp3 // 压缩音频


ffmpeg -i input.mp4 -vf scale=1280:-1 -c:v libx264 -preset veryslow -crf 24 output.mp4 // 压缩视频

视频直播推流:

// 录制视频保存在本地

ffmpeg -f avfoundation -i "1" -vcodec libx264 -preset ultrafast -f h264 -r 30 ~/Downloads/test.h264


// 推送已下载在文件夹的视频

ffmpeg -re -i ~/Downloads/xxx.mp4 -vcodec libx264 -acodec aac -strict -2 -f flv rtmp://localhost:1935/live


// 录制桌面

ffmpeg -f avfoundation -i "1" -vcodec libx264 -preset ultrafast -acodec libfaac -f flv rtmp://localhost:1935/rtmplive/room


// 录制桌面和麦克风

ffmpeg -f avfoundation -i "1:0" -vcodec libx264 -preset ultrafast -acodec libmp3lame -ar 44100 -ac 1 -f flv rtmp://localhost:1935/live/room


// 录制桌面和麦克风,并打开摄像头拍摄

ffmpeg -f avfoundation -framerate 30 -i "1:0" \-f avfoundation -framerate 30 -video_size 640x480 -i "0" \-c:v libx264 -preset ultrafast \-filter_complex 'overlay=main_w-overlay_w-10:main_h-overlay_h-10' -acodec libmp3lame -ar 44100 -ac 1 -f flv rtmp://localhost:2016/rtmplive/room

直播 DEMO:

  1. 安装支持rtmp的 docker 镜像:docker pull tiangolo/nginx-rtmp
  2. 启动tiangolo/nginx-rtmp容器:docker run -d -p 1935:1935 --name nginx-rtmp tiangolo/nginx-rtmp查看nginx配置: docker exec -it nginx-rtmp /bin/bash推流地址:rtmp://10.17.8.189:1935/live
  3. Ffmpeg 推流:ffmpeg -f avfoundation -i "1:0" -vcodec libx264 -preset ultrafast -acodec libmp3lame -ar 44100 -ac 1 -f flv rtmp://localhost:1935/live/room
  4. 用支持支持rtmp的播放器(IINA)或者ffplay打开:ffplay rtmp://10.17.8.189:1935/live

一个简单的直播 demo 就跑起来了。

5. FFmpeg 在 Node 中的用法

Fluent-ffmpeg4

Fluent-ffmpeg 是将复杂的 ffmpeg 命令抽象成 NodeJS 的模块,前提是系统已安装 FFmpeg

一些简单的用法

// 视频信息

ffmpeg.ffprobe(input, function (err, metadata) {

console.dir(metadata);

});


// 提取音频

ffmpeg(input)

.audioCodec("libmp3lame")

.on("error", function (err) {

console.log("发生错误: " + err.message);

})

.on("end", function () {

console.log("提取音频完成 !");

})

.save(resOut);


// 提取视频

ffmpeg(input)

.noAudio()

.on("error", function (err) {

console.log("发生错误: " + err.message);

})

.on("end", function () {

console.log("提取视频完成 !");

})

.save(resOut);

总结

回顾一下,广义上的视频:由音频视频两部分组成,它们分别有对应的各自的编码规范视频容器是将不同编码格式的音、视频组合在一起的一种封装格式。视频编码格式主要是对视频的大小进行压缩,分为帧内压缩帧间压缩,帧间压缩主要是通过记录关键帧形式来进行压缩。

FFmpeg 是处理音视频编码的一种程序,主要原理:demuxer=> decoder=> encoder=> muxer。

Fluent-ffmpeg 是将复杂的 ffmpeg 命令抽象成 nodeJs 的模块,前提是系统已安装 FFmpeg,这对于前端工程师来说,可以用它处理众多音视频操作。

挖坑

下一篇预告:FFmpeg 和 wasm 在浏览器中的碰撞

本文参考文章

1. 即时通讯网-史上最通俗视频编码技术入门:http://www.52im.net/thread-2840-1-1.html

2. 简书-音视频基础知识:https://www.jianshu.com/p/614b3e6e641a

3. 滤镜实现各种图片效果 | Video-Filters | avfilter | 变色:https://www.geek-share.com/detail/2763908000.html

4. 阮一峰:FFmpeg 视频处理入门教程:http://www.ruanyifeng.com/blog/2020/01/ffmpeg.html

文内链接

  1. https://baike.baidu.com/item/像素深度
  2. http://www.52im.net/thread-2840-1-1.html
  3. https://ffmpeg.org/
  4. https://github.com/fluent-ffmpeg/node-fluent-ffmpeg

.环境准备

需要在执行路径下,准备一个xxx.yuv的文件。然后在qt的命令行这里,输入如下:


点击运行,编码出h264文件。看出来这个压缩比达到1:87,效果还是不错的。


下面是一些发帧的时间,编码时间等信息。

发了很多次数据,才开始编码成正真的数据packet。

当文件读取完时,必须要去冲刷编码器,把更多的缓存packet取出来。

2.FFmpeg编码视频流程

从本地读取YUV数据编码为h264格式的数据,然后再存?到本地,编码后的数据有带startcode。

与FFmpeg ?频编码的流程基本?致。

主要API说明:

(1)avcodec_find_encoder_by_name:根据指定的编码器名称查找注册的编码器。

(2)avcodec_alloc_context3:为AVCodecContext分配内存。

(3)avcodec_open2:打开编解码器。

(4)avcodec_send_frame:将AVFrame?压缩数据给编码器。

(5)avcodec_receive_packet:获取到编码后的AVPacket数据。

(6)av_frame_get_buffer: 为?频或视频数据分配新的buffer。在调?这个函数之前,必须在AVFame上设置好以下属性:format(视频为像素格式,?频为样本格式)、nb_samples(样本个数,针对?频)、channel_layout(通道类型,针对?频)、width/height(宽?,针对视频)。因为这个Buf是根据这些参数来计算。

(7)av_frame_make_writable:确保AVFrame是可写的,尽可能避免数据的复制。如果AVFrame不是可写的,将分配新的buffer和复制数据,避免数据冲突。

(8)av_image_fill_arrays:存储?帧像素数据存储到AVFrame对应的data buffer。

(9)av_image_get_buffer_size,通过指定像素格式、图像宽、图像?来计算所需的内存??。

int av_image_get_buffer_size(enum AVPixelFormat pix_fmt, int width, int height, int align)

重点说明?个参数align:

此参数是设定内存对?的对?数,也就是按多?的字节进?内存对?。

?如设置为1,表示按1字节对?,那么得到的结果就是与实际的内存???样。

再?如设置为4,表示按4字节对?。也就是内存的起始地址必须是4的整倍数。

(9)av_image_alloc

av_image_alloc()是这样定义的。此函数的功能是按照指定的宽、?、像素格式来分配图像内存。

int av_image_alloc(uint8_t *pointers[4], int linesizes[4], int w, int h, enum AVPixelFormat pix_fmt,

int align);

pointers[4]:保存图像通道的地址。如果是RGB,则前三个指针分别指向R,G,B的内存地址。第四个指针保留不?。

linesizes[4]:保存图像每个通道的内存对?的步?,即??的对?内存的宽度,此值??等于图像宽度。

w: 要申请内存的图像宽度。

h: 要申请内存的图像?度。

pix_fmt: 要申请内存的图像的像素格式。

align: ?于内存对?的值。

返回值:所申请的内存空间的总??。如果是负值,表示申请失败。

(10)av_image_fill_arrays

int av_image_fill_arrays(uint8_t *dst_data[4], int dst_linesize[4], const uint8_t *src, enum

AVPixelFormat pix_fmt, int width, int height, int align);

av_image_fill_arrays()

函数?身不具备内存申请的功能,此函数类似于格式化已经申请的内存,即通过av_malloc()函数申请的内存空间,或者av_frame_get_buffer()函数申请的内存空间。

dst_data[4]: [out]对申请的内存格式化为三个通道后,分别保存其地址。

dst_linesize[4]: [out]格式化的内存的步?(即内存对?后的宽度)。

*src: [in]av_alloc()函数申请的内存地址。

pix_fmt: [in] 申请 src内存时的像素格式

width: [in]申请src内存时指定的宽度

height: [in]申请scr内存时指定的?度

align: [in]申请src内存时指定的对?字节数


H.264 码率设置

一般在实际的直播项目中,如果为了匹配不同分辨率,一般都是设置动态缓冲区适配,这样可以把不同分辨率,不同码率的延迟降到最低。这就是一个实战中得出的优化经验。

什么是视频码率?

视频码率是视频数据(包含视频?彩量、亮度量、像素量)每秒输出的位数。?般?的单位是kbps。

设置视频码率的必要性

在?络视频应?中,视频质量和?络带宽占?是相?盾的。通常情况下,视频流占?的带宽越?则视频质量也越?,需要的?络带宽也越?,解决这??盾的钥匙当然是视频编解码技术。评判?种视频编解码技术的优劣,是?较在相同的带宽条件下,哪个视频质量更好;在相同的视频质量条件下,哪个占?的?络带宽更少(?件体积?)

是不是视频码率越?,质量越好呢?

理论上是这样的。然?在我们?眼分辨的范围内,当码率?到?定程度时,就没有什么差别了。所以码率设置有它的最优值,H.264(也叫AVC或X264)的?件中,视频的建议码率如下:

?机设置码率建议

通过上?的介绍,结合我做过的?些?机项?,我总结了?套设置码率的公式,分享给?家如下:

如果1080P,码率低于2M,实际效果很差。不建议这么做,那样没什么意义。

FFmpeg与H264编码指南

鉴于x264的参数众多,各种参数的配合复杂,为了使?者?便,x264建议如?特别需要可使?preset和tune设置。这套开发者推荐的参数较为合理,可在此基础上在调整?些具体参数以符合??需要,?动设定的参数会覆盖preset和tune?的参数。

使?ffmpeg -h encoder=libx264 命令查询相关?持的参数。







英?地址:https://trac.ffmpeg.org/wiki/Encode/H.264。内容有?定出?,但是可以借鉴学习。

x264是?个 H.264/MPEG4 AVC 编码器,本指南将指导新?如何创建?质量的H.264视频。

对于普通?户通常有两种码率控制模式:CRF(Constant Rate Factor)和Two pass ABR。码率控制是?种决定为每?个视频帧分配多少?特数的?法,它将决定?件的??和质量的分配

如果你在编译和安装libx264 ??需要帮助,请查看ffmpeg和x264编译指南:

http://ffmpeg.org/trac/ffmpeg/wiki/CompilationGuide

CRF

量化?例的范围为0~51,其中0为?损模式,23为缺省值,51可能是最差的。该数字越?,图像质量越好。从主观上讲,18~28是?个合理的范围。18往往被认为从视觉上看是?损的,它的输出视频质量和输?视频?模?样或者说相差??。但从技术的?度来讲,它依然是有损压缩。

若CRF值加6,输出码率?概减少?半;若CRF值减6,输出码率翻倍。通常是在保证可接受视频质量的前提下选择?个最?的CRF值,如果输出视频质量很好,那就尝试?个更?的值,如果看起来很糟,那就尝试?个??点值。

注意:本?所提到的量化?例只适?于8-bit x264(10-bit x264的量化?例 为0~63),你可以使?x264--help命令在Output bit depth选项查看输出位深,在各种版本中,8bit是最常?的

preset

预设是?系列参数的集合,这个集合能够在编码速度和压缩率之间做出?个权衡?个编码速度稍慢的预设会提供更?的压缩效率(压缩效率是以?件??来衡量的)。这就是说,假如你想得到?个指定??的?件或者采?恒定?特率编码模式,你可以采??个较慢的预设来获得更好的质量。同样的,对于恒定质量编码模式,你可以通过选择?个较慢的预设轻松地节省?特率。这里后面会根据代码来设置。

通常的建议是使?最慢的预设。?前所有的预设按照编码速度降序排列为:

ultrafast

superfast

veryfast

faster

fast

medium – default preset

slow

slower

veryslow

placebo - ignore this as it is not useful (see FAQ)


默认为medium级别。

你可以使?--preset来查看预设列表,也可以通过x264 --fullhelp查看预设所采?的参数配置

tune

tune是x264中重要性仅次于preset的选项,它是视觉优化的参数,tune可以理解为视频偏好(或者视频类型),tune不是?个单?的参数,?是由?组参数构成-tune来改变参数设置。当前的 tune包括:

film:电影类型,对视频的质量?常严格时使?该选项

animation:动画?,压缩的视频是动画?时使?该选项

grain:颗粒物很重,该选项适?于颗粒感很重的视频

stillimage:静态图像,该选项主要?于静?画??较多的视频

psnr:提?psnr,该选项编码出来的视频psnr?较?

ssim:提?ssim,该选项编码出来的视频ssim?较?

fastdecode:快速解码,该选项有利于快速解码

zerolatency:零延迟,该选项主要?于视频直播

如果你不确定使?哪个选项或者说你的输?与所有的tune皆不匹配,你可以忽略--tune 选项。你可以使?-tune来查看tune列表,也可以通过x264 --fullhelp来查看tune所采?的参数配置。

profile

另外?个可选的参数是-profile:v,它可以将你的输出限制到?个特定的 H.264 profile。?些?常?的或者要被淘汰的设备仅?持有限的选项,?如只?持baseline或者main。

所有的profile 包括:

baseline profile:基本画质。?持I/P 帧,只?持?交错(Progressive)和CAVLC

extended profile:进阶画质。?持I/P/B/SP/SI 帧,只?持?交错(Progressive)和CAVLC

main profile:主流画质。提供I/P/B 帧,?持?交错(Progressive)和交错(Interlaced),也?持CAVLC 和CABAC 的?持。

high profile:?级画质。在main Profile 的基础上增加了8x8内部预测、?定义量化、 ?损视频编码和更多的YUV 格式。

想要说明H.264 high profile与H.264 main profile的区别就要讲到H.264的技术发展了。JVT于2003年完成H.264基本部分标准制定?作,包含baseline profile、extended profile和main profile,分别包括不同的编码?具。之后JVT?完成了H.264 FRExt(即:Fidelity Range Extensions)扩展部分(Amendment)的制定?作,包括high profile(HP)、high 10 profile(Hi10P)、high 4:2:2profile(Hi422P)、high 4:4:4 profile(Hi444P)4个profile。

H.264 baseline profile、extended profile和main profile都是针对8位样本数据、4:2:0格式的视频序列,FRExt将其扩展到8~12位样本数据,视频格式可以为4:2:0、4:2:2、4:4:4,设?了highprofile(HP)、high 10 profile(Hi10P)、high 4:2:2 profile(Hi422P)、high 4:4:4profile(Hi444P) 4个profile,这4个profile都以main profile为基础

在相同配置情况下,High profile(HP)可以?Main profile(MP)节省10%的码流量,编码时间可能更长,?MPEG-2MP节省60%的码流量,具有更好的编码性能。根据应?领域的不同:

baseline profile:多应?于实时通信领域

main profile:多应?于流媒体领域

high profile:则多应?于?电和存储领域

关于profile和level控制,可以参考这篇文章:

https://www.jianshu.com/p/48d723bb2740


低延迟

x264提供了?个 -tune zerolatency 选项。

兼容性

如果你想让你的视频最?化的和?标播放设备兼容(?如?版本的的ios或者所有的android 设备),那么你可以这做

-profile:v baseline

这将会关闭很多?级特性,但是它会提供很好的兼容性。也许你可能不需要这些设置,因为?旦你?了这些设置,在同样的视频质量下与更?的编码档次相?会使?特率稍有增加

关于profile列表和关于它们的描述,你可以运?x264 --fullhelp

要牢记apple quick time 对于x264编码的视频只?持 YUV 420颜?空间,?且不?持任何?于 mian profile编码档次。这样对于quick time 只留下了两个兼容选项baselinemain。其他的编码档次qucik time均不?持,虽然它们均可以在其它的播放设备上回放。

有些问题,通过解答的形式给出。

两遍编码模式能够?CRF模式提供更好的质量吗?

不能,但它可以更加精确地控制?标?件??。

为什么 placebo 是?个浪费时间的玩意??

与 veryslow相?,它以极?的编码时间为代价换取了?概1%的视频质量提升,这是?种收益递减准则,veryslow 与 slower相?提升了3%;slower 与 slow相?提升了5%;slow 与 medium相?提升了5%~10%。

为什么我的?损输出看起来是?损的?

这是由于rgb->yuv的转换,如果你转换到yuv444,它依然是?损的。

显卡能够加速x264的编码吗?

不,x264没有使?(?少现在没有),有?些私有编码器使?了GPU加快了编码速度,但这并不意味着它们经过良好的优化。也有可能还不如x264,或许速度更慢。总的来说,ffmpeg到?前为?还不?持GPU。

翻译注释:x264在2013版中已经开始?持基于opencl的显卡加速,?于帧类型的判定。

为Quick time 播放器压制视频

你需要使?-pix_fmt yuv420p来是你的输出?持QT 播放器。这是因为对于H.264视频剪辑苹果的Quicktime只?持 YUV420颜?空间。否则ffmpeg会根据你的视频源输出与Quick time 不兼容的视频格式或者不是基于ffmpeg的视频。

X264参数之zerolatency的分析

加?zerolatency之后,转码路数会明显降低。

我们都知道,加?zerolatency的?的是为了降低在线转码的编码延迟,那么,该参数是如何影响到x264的转码性能了呢?

?先,先来看看代码中编码延迟的影响因素:


设置zerolatency后,相应的参数配置如下:


下?我们来看?下zerolatency设置中各个参数的意义:

rc_lookahead: Set number of frames to look ahead for frametype and ratecontrol.

该参数为mb-tree码率控制和vbv-lookahead设置可?的帧数量,最?值为250。对于mbi-tree来说,rc_lookahead值越?,会得到更准确的结果,但编码速度也会更慢,因为编码器需要缓存慢rc_lookahead帧数据后,才会开始第?帧编码,增加编码延时,因此在实时视频通信中将其设置为0。

sync_lookahead: 设置?于线程预测的帧缓存??,最?值为250。在第?遍及更多遍编码或基于分?线程时?动关闭。sync_lookahead=0为关闭线程预测,可减?延迟,但也会降低性能。

bframes: I帧和P帧或者两个P帧之间可?的最?连续B帧数量,默认值为3。B帧可使?双向预测,从?显著提?压缩率,但由于需要缓存更多的帧数以及重排序的原因,会降低编码速度,增加编码延迟,因此在实时编码时也建议将该值设置为0。

sliced_threads: 基于分?的线程,默认值为off,开启该?法在压缩率和编码效率上都略低于默认?法,但没有编码延时。除?在编码实时流或者对低延迟要求较?的场合开启该?法,?般情况下建议设为off。

vfr_input: 与force-cfr选项相对应:


vfr_input=1时,为可变帧率,使?timebase和timestamps做码率控制;vfr_input=0时,为固定帧率,使?fps做码率控制。

mb_tree: 基于宏块树的码率控制。对于每个MB,向前预测?定数量的帧(该数量由rc_lookahead和keyint中的较?值决定),计算该MB被引?的次数,根据被引?次数的多少决定为该MB分配的量化QP值。该?法会?成?个临时stats?件,记录了每个P帧中每个MB被参考的情况。使?mb_tree的?法能够节约?概30%的码率,但同时也会增加编码延迟,因此实时流编码时也将其关闭。

详细可以参考这个:https://slhck.info/video/2017/02/24/crf-guide.html

3.源码解读

根据传参,查找指定编码器。并分配编码器上下文。

给编码器上下文设置相关信息,如,宽高,time_base,B帧个数,gop等。

如果想一直保持I帧进行编码,就需要设置frame->pict_type设置为AV_PICTURE_TYPE_I, 则忽略gop_size的设置。

这里的time_base与帧率设置为倒数关系即可。一般做直播都是把B帧设置为0,因为b帧会缓存较多,然后会带来延时。

设置0延迟,画质效果一般,精细度不够好。

一般编码时间与画质是成一个反比关系,就是说如果使用ultrafast,那画质可能就差点,使用veryslow,那画质可能就好点。代码设置如下:

根据debug结果显示,ultrafast模式,编码时间为2270ms,medium模式,编码时间为5815ms,veryslow模式,编码时间为19836ms。直播时,一般都设置zerolatency,为0延迟。

从图中可以看出,当其他参数固定时,选择不同的preset,对应的码率和编码时间都不?样。

可以使?--preset来查看预设列表,也可以通过x264 --fullhelp来查看预设所采?的参数配置。一般在做直播,可以使用ultrafast、superfast、veryfast,但是就一般不会有特别复杂的算法。

码率设置,一般需要根据分辨率去配置,也会有一般值,最小值等,当然这跟动态码率,固定码率这些有关系。编码器上下文还可以设置线程数,编码速度会快,开了多线程后也会导致帧输出延迟, 需要缓存thread_count帧后再编程,所以直播为了降低延迟,一般是不去开启多线程。设置AV_CODEC_FLAG_GLOBAL_HEADER,表示只有第一个I帧,具有SPS/PPS信息,此时sps pps需要从codec_ctx->extradata读取,如果不设置,那就是每个I帧都有sps、pps 、sei。是否设置,根据自己情况去看,一般存储本地文件时,不要去设置。


如果没有设置AV_CODEC_FLAG_GLOBAL_HEADER,就可以看到每个I帧都有SPS和PPS,SEI信息。

这个就是SEI信息。

如果设置AV_CODEC_FLAG_GLOBAL_HEADER,存放到本地,PKT里没有SPS和PPS信息,但是SEI信息。

这时的SPS和PPS信息,是在codec_ctx->extradata,从这里面去获取。

注意,这个时候,是不会写到文件里的。

打开输入和输出文件。


分配PKT和frame

根据设置的参数,给frame分配buffer。

按照设置的字节对齐,计算出每一帧的数据=像素格式 * 宽 * 高

分配了一个yuv_buf,用来读取文件,存储使用。

当缓存B帧时,为了防止外面写数据与缓存冲突, 如果编码器内部保持了内存参考计数,则需要重新拷贝一个备份。一般如果是X264编码,一般是不会冲突,这里是为了起一个保险作用。

再把文件中读取的数据,存到 yuv_buf,然后再去填充到frame里去。这里也有字节对齐设置。

这里就开始设置pts,获取开始时间。一般pts还是要根据duration去计算比较好,如果实时流,直接从采集端拿到,然后配置下去。

编码视频。通过查阅代码,使用x264进行编码时,具体缓存帧是在x264源码进行,不会增加avframe对应buffer的reference。一般能够播放的h264是带有startcode,但是从flv里抽出的h264前,是要添加startcode才行。

注意:编码时,不管是编码还是解码,如果有B帧,都是要相互参考,可能输入多几帧进去,才会编码出来一帧。


本篇文章就分析到这里,欢迎关注,点赞,收藏,转发。如果需要测试代码,那需要私信。

. 前言

WebAssembly 就是运行在 Web 平台上的 Assembly。Assembly 是指汇编代码,是直接操作 CPU 的指令代码,比如 x86 指令集上的汇编代码有指令集、寄存器、栈等等设计,CPU 根据汇编代码的指导进行运算。汇编代码相当于 CPU 执行的机器码能够转换成的人类适合读的一种语言。

Wasm的技术优势: 性能高效:WASM采用二进制编码,在程序执行过程中的性能优越; 存储成本低:相对于文本格式,二进制编码的文本占用的存储空间更小; 多语言支持:用户可以使用 C/C++/RUST/Go等多种语言编写智能合约并编译成WASM格式的字节码;

2. emcc编译的ffmpeg静态库

(1)CSDN上的下载地址

下载地址: https://download.csdn.net/download/xiaolong1126626497/82868215

(2)GitHub仓库下载地址

https://github.com/wang-bin/avbuild

https://sourceforge.net/projects/avbuild/files/

https://sourceforge.net/projects/avbuild/files/wasm/

(3)这里有编译好的ffmpeg.wasm文件,前端JS可以直接调用完成视频转码等功能 https://github.com/ffmpegwasm/ffmpeg.wasm

const fs=require('fs');
const { createFFmpeg, fetchFile }=require('@ffmpeg/ffmpeg');

const ffmpeg=createFFmpeg({ log: true });

(async ()=> {
  await ffmpeg.load();
  ffmpeg.FS('writeFile', 'test.avi', await fetchFile('./test.avi'));
  await ffmpeg.run('-i', 'test.avi', 'test.mp4');
  await fs.promises.writeFile('./test.mp4', ffmpeg.FS('readFile', 'test.mp4'));
  process.exit(0);
})();

(4)ffmpeg编译wasm文件的源码,可以自行编译wasm文件: https://github.com/ffmpegwasm/ffmpeg.wasm-core

3. 调用ffmpeg库-打印版本号

3.1 准备ffmpeg库文件

3.2 编写C语言代码

下面只是编写了一个打印版本号的函数,用于测试ffmpeg的库和相关函数是否可以正常调用。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswresample/swresample.h>
#include <emscripten/emscripten.h>
#include <libavcodec/version.h>

//获取版本号
void print_version()
{
    unsigned codecVer=avcodec_version();
    int ver_major, ver_minor, ver_micro;
    ver_major=(codecVer >> 16) & 0xff;
    ver_minor=(codecVer >> 8) & 0xff;
    ver_micro=(codecVer) & 0xff;
    printf("当前ffmpeg的版本:avcodec version is: %d=%d.%d.%d\n", codecVer, ver_major, ver_minor, ver_micro);

}

3.3 编译生成wasm和js文件

emcc wasm_ffmpeg/wasm_ffmpeg.c ffmpeg-4.4-wasm/lib/libavformat.a ffmpeg-4.4-wasm/lib/libavcodec.a  ffmpeg-4.4-wasm/lib/libswresample.a ffmpeg-4.4-wasm/lib/libavutil.a -I "ffmpeg-4.4-wasm/include" -s EXPORTED_FUNCTIONS="['_malloc','_free','ccall','allocate','UTF8ToString','_print_version']" -s WASM=1 -s ASSERTIONS=0 -s TOTAL_MEMORY=167772160 -s ALLOW_MEMORY_GROWTH=1 -o out/ffmpeg_decoder.js

编译成功后生成的wasm和js文件:

3.3 编写index.html代码

编写HTML文件调用js文件里的接口。

<!doctype html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>js调用c语言函数示例</title>
  </head>
  
  <body>  
  
    <script type='text/javascript'>   
      function run1()
      {
        _print_version();
      }
      
    </script>
    
    <input type="button" value="打印版本号" onclick="run1()" />
    <script async type="text/javascript" src="ffmpeg_decoder.js"></script>
  </body>
</html>

3.4 开启服务器

cmd命令行运行python,开启http服务器。

python -m http.server

3.5 访问测试

打开谷歌浏览器,输入http://127.0.0.1:8000/index.html地址,按下F12打开控制台,点击页面上的按钮看控制台输出。

完成调用,已成功打印版本号。

4. 调用ffmpeg库-解码视频信息

wasm编译的ffmpeg代码,不能使用avformat_open_input 直接打开文件地址,打开网络地址,只能从内存中读取数据进行解码。前端js加载了本地磁盘文件后,需要通过内存方式传递给wasm-ffmpeg接口里,然后ffmpeg再进行解码。

下面C语言代码里演示了调用ffmpeg解码内存里视频文件过程,解码读取分辨率、总时间,解帧数据等。代码只是为了演示如何调用ffmpeg的测试代码,代码比较简单,只是解码了第一帧数据,得到了YUV420P数据,然后保存在文件中。

4.1 编写C语言代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswresample/swresample.h>
#include <emscripten/emscripten.h>
#include <libavcodec/version.h>

//EMSCRIPTEN_KEEPALIVE

/*
存储视频文件到磁盘
参数:
char *name  文件名称
char *buf    写入的数据
unsigned int len  写入长度
*/
int write_file(char *name, char *buf, unsigned int len)
{
    //创建文件
    FILE *new_fp=fopen(name, "wb");
    if (new_fp==NULL)
    {
        printf("%s 文件创建失败.\n", name);
        return -1;
    }
    else
    {
        printf("%s 文件创建成功.\n", name);
    }
    //写入磁盘
    int cnt=fwrite(buf, 1, len, new_fp);
    printf("成功写入=%d 字节\n", cnt);

    //关闭文件
    fclose(new_fp);

    return cnt;
}


/*
获取文件大小
*/
long get_FileSize(char *name)
{
    /*1. 打开文件*/
    FILE *fp=fopen(name, "rb");
    if (fp==NULL)
    {
        printf("% 文件不存在.\n", name);
        return -1;
    }
    /*2. 将文件指针偏移到文件结尾*/
    fseek(fp, 0, SEEK_END);

    /*3. 获取当前文件指针距离文件头的字节偏移量*/
    long byte=ftell(fp);

    /*4. 关闭文件*/
    fclose(fp);

    return byte;
}


/*
读文件
char *buf
*/
unsigned char *read_file(char *name)
{
    //创建文件
    FILE *fp=fopen(name, "rb");
    if (fp==NULL)
    {
        printf("%s 文件打开失败.\n", name);
        return -1;
    }

    //获取文件大小
    int size=get_FileSize(name);

    //申请空间
    unsigned char *buf=(unsigned char *)malloc(size);
    if (buf==NULL)
    {
        printf("空间申请失败:%d byte.\n", size);
        return NULL;
    }
    //读取文件到内存
    int cnt=fread(buf, 1, size, fp);
    printf("成功读取=%d 字节\n", cnt);

    //关闭文件
    fclose(fp);

    return buf;
}


//获取版本号
void print_version()
{
    unsigned codecVer=avcodec_version();
    int ver_major, ver_minor, ver_micro;
    ver_major=(codecVer >> 16) & 0xff;
    ver_minor=(codecVer >> 8) & 0xff;
    ver_micro=(codecVer) & 0xff;
    printf("当前ffmpeg的版本:avcodec version is: %d=%d.%d.%d\n", codecVer, ver_major, ver_minor, ver_micro);

}


int ffmpeg_laliu_run_flag=1;

/*
功能: 这是FFMPEG回调函数,返回1表示超时  0表示正常
ffmpeg阻塞完成一些任务的时候,可以快速强制退出.
*/
static int interrupt_cb(void *ctx)
{
    if (ffmpeg_laliu_run_flag==0)return 1;
    return 0;
}


//存放视频解码的详细信息
struct M_VideoInfo
{
    int64_t duration;
    int video_width;
    int video_height;
};

struct M_VideoInfo m_VideoInfo;


//读取数据的回调函数-------------------------  
//AVIOContext使用的回调函数!  
//注意:返回值是读取的字节数  
//手动初始化AVIOContext只需要两个东西:内容来源的buffer,和读取这个Buffer到FFmpeg中的函数  
//回调函数,功能就是:把buf_size字节数据送入buf即可  
//第一个参数(void *opaque)一般情况下可以不用  

/*正确方式*/
struct buffer_data
{
    uint8_t *ptr; /* 文件中对应位置指针 */
    size_t size;  ///< size left in the buffer /* 文件当前指针到末尾 */
};

// 重点,自定的buffer数据要在外面这里定义
struct buffer_data bd={ 0 };

//用来将内存buffer的数据拷贝到buf
int read_packet(void *opaque, uint8_t *buf, int buf_size)
{

    buf_size=FFMIN(buf_size, bd.size);

    if (!buf_size)
        return AVERROR_EOF;
    printf("ptr:%p size:%zu bz%zu\n", bd.ptr, bd.size, buf_size);

    /* copy internal buffer data to buf */
    memcpy(buf, bd.ptr, buf_size);
    bd.ptr +=buf_size;
    bd.size -=buf_size;

    return buf_size;
}


//ffmpeg解码使用的全局变量
unsigned char * iobuffer;
AVFormatContext * format_ctx;
int video_width=0;
int video_height=0;
int video_stream_index=-1;
char* video_buffer;


/*
函数功能: 初始化解码环境
函数参数:
unsigned char *buf  视频文件的内存地址
unsigned int len   视频文件长度
*/ 
int initDecoder(unsigned char *buf,unsigned int len)
{
    int ret=0;

    bd.ptr=buf;  /* will be grown as needed by the realloc above */
    bd.size=len; /* no data at this point */

    //注册ffmpeg
    av_register_all();

    unsigned int version=avformat_version();
    
    printf("ffmpeg版本: %d\r\n",version);

    // Allocate an AVFormatContext
    format_ctx=avformat_alloc_context();
    if (format_ctx==NULL)
    {
        printf("avformat_alloc_context 失败.\n");
        return -1;
    }

    iobuffer=(unsigned char *)av_malloc(32768);
    AVIOContext *avio=avio_alloc_context(iobuffer, 32768, 0, NULL, read_packet, NULL, NULL);
    format_ctx->pb=avio;
    ret=avformat_open_input(&format_ctx, "nothing", NULL, NULL);

    format_ctx->interrupt_callback.callback=interrupt_cb; //--------注册回调函数

    AVDictionary* options=NULL;

    //ret=avformat_open_input(&format_ctx, url, NULL, NULL);
    
    if (ret !=0)
    {
        char buf[1024];
        av_strerror(ret, buf, 1024);
        printf("无法打开视频内存,return value: %d \n",ret);
        return -1;
    }

    printf("正在读取媒体文件的数据包以获取流信息.\n");

    // 读取媒体文件的数据包以获取流信息
    ret=avformat_find_stream_info(format_ctx, NULL);
    if (ret < 0)
    {
        printf("无法获取流信息: %d\n",ret);
        return -1;
    }

    AVCodec  *video_pCodec;
    // audio/video stream index
    printf("视频中流的数量: %d\n",format_ctx->nb_streams);
    printf("视频总时间:%lld 秒\n",format_ctx->duration / AV_TIME_BASE);


    //得到秒单位的总时间
    m_VideoInfo.duration=format_ctx->duration / AV_TIME_BASE;

    for (int i=0; i < format_ctx->nb_streams; ++i)
    {
        const AVStream* stream=format_ctx->streams[i];
        printf("编码数据的类型: %d\n",stream->codecpar->codec_id);

        if (stream->codecpar->codec_type==AVMEDIA_TYPE_VIDEO)
        {
            //查找解码器
            video_pCodec=avcodec_find_decoder(AV_CODEC_ID_H264);
            //打开解码器
            int err=avcodec_open2(stream->codec, video_pCodec, NULL);
            if (err !=0)
            {
                printf("H264解码器打开失败.\n");
                return 0;
            }
            video_stream_index=i;
            
            //得到视频帧的宽高
            video_width=stream->codecpar->width;
            video_height=stream->codecpar->height;
            
            //保存宽和高
            m_VideoInfo.video_height=video_height;
            m_VideoInfo.video_width=video_width;

            //解码后的YUV数据存放空间
            video_buffer=malloc(video_height * video_width * 3 / 2);

            printf("视频帧的尺寸(以像素为单位): (宽X高)%dx%d 像素格式: %d\n",
                stream->codecpar->width,stream->codecpar->height,stream->codecpar->format);
        }
        else if (stream->codecpar->codec_type==AVMEDIA_TYPE_AUDIO)
        {
            
        }
    }

    if (video_stream_index==-1)
    {
        printf("没有检测到视频流.\n");
        return -1;
    }

    printf("初始化成功.\n");
    return 0;
}


//获取视频总时长
int64_t GetVideoDuration()
{
    return m_VideoInfo.duration;
}

//获取视频宽
int64_t GetVideoWidth()
{
    return m_VideoInfo.video_width;
}

//获取视频高
int64_t GetVideoHeight()
{
    return m_VideoInfo.video_height;
}


//获取视频帧
//传入参数时间单位--秒
unsigned char *GetVideoFrame(int time)
{
    AVPacket pkt;
    double video_clock;
    AVFrame *SRC_VIDEO_pFrame=av_frame_alloc();

    printf("开始解码.\n");

    printf("跳转状态:%d\n",av_seek_frame(format_ctx, -1, time*AV_TIME_BASE, AVSEEK_FLAG_ANY));

    while (1)
    {
        int var=av_read_frame(format_ctx, &pkt);
        //读取一帧数据
        if (var < 0)
        {
            printf("数据读取完毕:%d\n", var);
            break;
        }

        printf("开始..\n");
        //如果是视频流节点
        if (pkt.stream_index==video_stream_index)
        {
            //当前时间
            video_clock=av_q2d(format_ctx->streams[video_stream_index]->time_base) * pkt.pts;
            printf("pkt.pts=%0.2f,video_clock=%0.2f\n", pkt.pts, video_clock);

            //解码视频 frame
            //发送视频帧
            if (avcodec_send_packet(format_ctx->streams[video_stream_index]->codec, &pkt) !=0)
            {
                av_packet_unref(&pkt);//不成功就释放这个pkt
                continue;
            }

            //接受后对视频帧进行解码
            if (avcodec_receive_frame(format_ctx->streams[video_stream_index]->codec, SRC_VIDEO_pFrame) !=0)
            {
                av_packet_unref(&pkt);//不成功就释放这个pkt
                continue;
            }

            //转格式
        /*  sws_scale(img_convert_ctx,
                (uint8_t const **)SRC_VIDEO_pFrame->data,
                SRC_VIDEO_pFrame->linesize, 0,video_height, RGB24_pFrame->data,
                RGB24_pFrame->linesize);*/

            memset(video_buffer, 0, video_height * video_width * 3 / 2);
            int height=video_height;
            int width=video_width;

            printf("decode video ok\n");
            int a=0, i;
            for (i=0; i < height; i++)
            {
                memcpy(video_buffer + a, SRC_VIDEO_pFrame->data[0] + i * SRC_VIDEO_pFrame->linesize[0], width);
                a +=width;
            }
            for (i=0; i < height / 2; i++)
            {
                memcpy(video_buffer + a, SRC_VIDEO_pFrame->data[1] + i * SRC_VIDEO_pFrame->linesize[1], width / 2);
                a +=width / 2;
            }
            for (i=0; i < height / 2; i++)
            {
                memcpy(video_buffer + a, SRC_VIDEO_pFrame->data[2] + i * SRC_VIDEO_pFrame->linesize[2], width / 2);
                a +=width / 2;
            }

        
            //保存在文件中: 
            //write_file("./666.yuv", video_buffer, video_height * video_width * 3 / 2);

            printf("退出成功....\n");
            break;
        }

        //释放包
        av_packet_unref(&pkt);
    }
    av_free(SRC_VIDEO_pFrame);
    return video_buffer;
}


//销毁内存
void DeleteMemory()
{
    //释放空间
    av_free(iobuffer);
}

4.2 编译生成wasm和js文件

emcc wasm_ffmpeg/wasm_ffmpeg.c ffmpeg-4.4-wasm/lib/libavformat.a ffmpeg-4.4-wasm/lib/libavcodec.a  ffmpeg-4.4-wasm/lib/libswresample.a ffmpeg-4.4-wasm/lib/libavutil.a -I "ffmpeg-4.4-wasm/include" -s EXPORTED_FUNCTIONS="['_malloc','_free','ccall','allocate','UTF8ToString','_initDecoder','_write_file','_print_version','_get_FileSize','_read_file','_GetVideoFrame','_GetVideoWidth','_GetVideoDuration','_GetVideoHeight','_DeleteMemory']" -s WASM=1 -s ASSERTIONS=0 -s TOTAL_MEMORY=167772160 -s ALLOW_MEMORY_GROWTH=1 -o out/ffmpeg_decoder.js

编译成功后生成的wasm和js文件:

4.3 编写index.html代码

完成了视频选择,播放,调用了C语言编写的接口完成解码返回,但是没有渲染。

<!doctype html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>js调用c语言函数示例</title>
  </head>
  
  <body>  
    
    <input id="myfile" type="file"/>
    <video id="output-video" width="300" controls></video>
     <div><canvas id="glcanvas" width="640" height="480"></canvas></div>
       <script>
        //代码摘自:https://github.com/ivan-94/video-push/blob/master/yuv/index.html#L312
          const video=document.getElementById('glcanvas');
          let renderer;
            
          class WebglScreen {
              constructor(canvas) {
              this.canvas=canvas;
              this.gl=          canvas.getContext('webgl') ||
                  canvas.getContext('experimental-webgl');
              this._init();
              }

              _init() {
              let gl=this.gl;
              if (!gl) {
                  console.log('gl not support!');
                  return;
              }

              // 图像预处理
              gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
              // GLSL 格式的顶点着色器代码
              let vertexShaderSource=`
                      attribute lowp vec4 a_vertexPosition;
                      attribute vec2 a_texturePosition;
                      varying vec2 v_texCoord;
                      void main() {
                          gl_Position=a_vertexPosition;
                          v_texCoord=a_texturePosition;
                      }
                  `;

              let fragmentShaderSource=`
                      precision lowp float;
                      uniform sampler2D samplerY;
                      uniform sampler2D samplerU;
                      uniform sampler2D samplerV;
                      varying vec2 v_texCoord;
                      void main() {
                          float r,g,b,y,u,v,fYmul;
                          y=texture2D(samplerY, v_texCoord).r;
                          u=texture2D(samplerU, v_texCoord).r;
                          v=texture2D(samplerV, v_texCoord).r;
                          fYmul=y * 1.1643828125;
                          r=fYmul + 1.59602734375 * v - 0.870787598;
                          g=fYmul - 0.39176171875 * u - 0.81296875 * v + 0.52959375;
                          b=fYmul + 2.01723046875 * u - 1.081389160375;
                          gl_FragColor=vec4(r, g, b, 1.0);
                      }
                  `;

                  let vertexShader=this._compileShader(
                      vertexShaderSource,
                      gl.VERTEX_SHADER,
                  );
                  let fragmentShader=this._compileShader(
                      fragmentShaderSource,
                      gl.FRAGMENT_SHADER,
                  );

              let program=this._createProgram(vertexShader, fragmentShader);

              this._initVertexBuffers(program);

              // 激活指定的纹理单元
              gl.activeTexture(gl.TEXTURE0);
              gl.y=this._createTexture();
              gl.uniform1i(gl.getUniformLocation(program, 'samplerY'), 0);

              gl.activeTexture(gl.TEXTURE1);
              gl.u=this._createTexture();
              gl.uniform1i(gl.getUniformLocation(program, 'samplerU'), 1);

              gl.activeTexture(gl.TEXTURE2);
              gl.v=this._createTexture();
              gl.uniform1i(gl.getUniformLocation(program, 'samplerV'), 2);
          }
          /**
           * 初始化顶点 buffer
           * @param {glProgram} program 程序
           */

          _initVertexBuffers(program) 
          {
              let gl=this.gl;
              let vertexBuffer=gl.createBuffer();
              let vertexRectangle=new Float32Array([
                  1.0,
                  1.0,
                  0.0,
                  -1.0,
                  1.0,
                  0.0,
                  1.0,
                  -1.0,
                  0.0,
                  -1.0,
                  -1.0,
                  0.0,
              ]);
              gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
              // 向缓冲区写入数据
              gl.bufferData(gl.ARRAY_BUFFER, vertexRectangle, gl.STATIC_DRAW);
              // 找到顶点的位置
              let vertexPositionAttribute=gl.getAttribLocation(
                  program,
                  'a_vertexPosition',
              );
              // 告诉显卡从当前绑定的缓冲区中读取顶点数据
              gl.vertexAttribPointer(
                  vertexPositionAttribute,
                  3,
                  gl.FLOAT,
                  false,
                  0,
                  0,
              );
              // 连接vertexPosition 变量与分配给它的缓冲区对象
              gl.enableVertexAttribArray(vertexPositionAttribute);

              let textureRectangle=new Float32Array([
                  1.0,
                  0.0,
                  0.0,
                  0.0,
                  1.0,
                  1.0,
                  0.0,
                  1.0,
              ]);
              let textureBuffer=gl.createBuffer();
              gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
              gl.bufferData(gl.ARRAY_BUFFER, textureRectangle, gl.STATIC_DRAW);
              let textureCoord=gl.getAttribLocation(program, 'a_texturePosition');
              gl.vertexAttribPointer(textureCoord, 2, gl.FLOAT, false, 0, 0);
              gl.enableVertexAttribArray(textureCoord);
          }

          /**
           * 创建并编译一个着色器
           * @param {string} shaderSource GLSL 格式的着色器代码
           * @param {number} shaderType 着色器类型, VERTEX_SHADER 或 FRAGMENT_SHADER。
           * @return {glShader} 着色器。
           */
          _compileShader(shaderSource, shaderType) 
          {
              // 创建着色器程序
              let shader=this.gl.createShader(shaderType);
              // 设置着色器的源码
              this.gl.shaderSource(shader, shaderSource);
              // 编译着色器
              this.gl.compileShader(shader);
              const success=this.gl.getShaderParameter(
                  shader,
                  this.gl.COMPILE_STATUS,
              );
              if (!success) {
                  let err=this.gl.getShaderInfoLog(shader);
                  this.gl.deleteShader(shader);
                  console.error('could not compile shader', err);
                  return;
              }

              return shader;
          }

          /**
           * 从 2 个着色器中创建一个程序
           * @param {glShader} vertexShader 顶点着色器。
           * @param {glShader} fragmentShader 片断着色器。
           * @return {glProgram} 程序
           */
          _createProgram(vertexShader, fragmentShader) 
          {
              const gl=this.gl;
              let program=gl.createProgram();

              // 附上着色器
              gl.attachShader(program, vertexShader);
              gl.attachShader(program, fragmentShader);

              gl.linkProgram(program);
              // 将 WebGLProgram 对象添加到当前的渲染状态中
              gl.useProgram(program);
              const success=this.gl.getProgramParameter(
                  program,
                  this.gl.LINK_STATUS,
              );

              if (!success) {
                  console.err(
                  'program fail to link' + this.gl.getShaderInfoLog(program),
                  );
                  return;
              }

              return program;
          }

          /**
           * 设置纹理
           */
          _createTexture(filter=this.gl.LINEAR) 
          {
              let gl=this.gl;
              let t=gl.createTexture();
              // 将给定的 glTexture 绑定到目标(绑定点
              gl.bindTexture(gl.TEXTURE_2D, t);
              // 纹理包装 参考https://github.com/fem-d/webGL/blob/master/blog/WebGL基础学习篇(Lesson%207).md -> Texture wrapping
              gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
              gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
              // 设置纹理过滤方式
              gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
              gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
              return t;
          }

          /**
           * 渲染图片出来
           * @param {number} width 宽度
           * @param {number} height 高度
           */
          renderImg(width, height, data) 
          {
              let gl=this.gl;
              // 设置视口,即指定从标准设备到窗口坐标的x、y仿射变换
              gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
              // 设置清空颜色缓冲时的颜色值
              gl.clearColor(0, 0, 0, 0);
              // 清空缓冲
              gl.clear(gl.COLOR_BUFFER_BIT);

              let uOffset=width * height;
              let vOffset=(width >> 1) * (height >> 1);

              gl.bindTexture(gl.TEXTURE_2D, gl.y);
              // 填充纹理
              gl.texImage2D(
                  gl.TEXTURE_2D,
                  0,
                  gl.LUMINANCE,
                  width,
                  height,
                  0,
                  gl.LUMINANCE,
                  gl.UNSIGNED_BYTE,
                  data.subarray(0, uOffset),
              );

              gl.bindTexture(gl.TEXTURE_2D, gl.u);
              gl.texImage2D(
                  gl.TEXTURE_2D,
                  0,
                  gl.LUMINANCE,
                  width >> 1,
                  height >> 1,
                  0,
                  gl.LUMINANCE,
                  gl.UNSIGNED_BYTE,
                  data.subarray(uOffset, uOffset + vOffset),
              );

              gl.bindTexture(gl.TEXTURE_2D, gl.v);
              gl.texImage2D(
                  gl.TEXTURE_2D,
                  0,
                  gl.LUMINANCE,
                  width >> 1,
                  height >> 1,
                  0,
                  gl.LUMINANCE,
                  gl.UNSIGNED_BYTE,
                  data.subarray(uOffset + vOffset, data.length),
              );

              gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
          }

          /**
           * 根据重新设置 canvas 大小
           * @param {number} width 宽度
           * @param {number} height 高度
           * @param {number} maxWidth 最大宽度
           */
          setSize(width, height, maxWidth) 
          {
              let canvasWidth=Math.min(maxWidth, width);
              this.canvas.width=canvasWidth;
              this.canvas.height=(canvasWidth * height) / width;
          }

          destroy() 
          {
              const { gl }=this;

              gl.clear(
                  gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT,
              );
          }
      } // end of webgl
      
      const initialCanvas=(canvas, width, height)=> {
          canvas.width=width;
          canvas.height=height;
          return new WebglScreen(canvas);
       };

      const render=(buff,width,height)=>
      {
          if (renderer==null) {
          return;
          }
          renderer.renderImg(width, height, buff);
      };
    </script>
    <script type='text/javascript'>   
      function run1()
      {
      
      }
      
      function run2()
      {
         
      }
     
     
       //加载本地文件
        var file=document.getElementById("myfile");
        file.onchange=function(event){  
            let fileReader=new FileReader();
            fileReader.onload=function(){
            // 当 FileReader 读取文件时候,读取的结果会放在 FileReader.result 属性中
                var fileArray=this.result;
                console.log(fileArray);
                let fileBuffer=new  Uint8Array(this.result);
                console.log(fileBuffer);
                //申请空间
                var fileBufferPtr=_malloc(fileBuffer.length)
                //将fileBuffer里的内容拷贝到fileBufferPtr里
                Module.HEAP8.set(fileBuffer,fileBufferPtr)
                
                //1. 写文件
                //申请空间,存放字符串
                //var name=allocate(intArrayFromString("./tmp.mp4"), ALLOC_NORMAL);
                //var run_var=_write_file(name,fileBufferPtr,fileBuffer.length);
                //console.log('写文件成功字节数:',run_var);
                
                //2. 获取文件大小
                //var file_size=_get_FileSize(name);
                //console.log('获取文件大小:',file_size);
                
               //const data=ffmpeg.FS('readFile', 'output.mp4');

                //3. 读取文文件
               //const data=_read_file(name);
              // const video=document.getElementById('output-video');
               //video.src=URL.createObjectURL(new Blob([fileBuffer.buffer], { type: 'video/mp4' }));
            
               //加载内存数据
              // Module.HEAPU8.subarray(imgBufferPtr, data);  

                //4. 初始化解码器,加载文件
                _initDecoder(fileBufferPtr,fileBuffer.length);
                
                //5. 获取总时间
                var time=_GetVideoDuration();
                console.log('视频总时间:'+time);
                
                
                //6. 获取视频宽
                var Width=_GetVideoWidth();
                console.log('视频宽:'+Width);
                
                //7. 获取视频高
                var Height=_GetVideoHeight();
                console.log('视频高:'+Height);
                
                renderer=initialCanvas(video,Width,Height);
                
                //申请空间,存放字符串
                //var name_file=allocate(intArrayFromString("./666.yuv"), ALLOC_NORMAL);

                //读取文件
                //var yuv_wasm_data=_read_file(name_file);
                
               //8. 获取视频帧
               var yuv_wasm_data=_GetVideoFrame(10);
                
               var renderlength=Width*Height*3/2;
               var RenderBuffer=new Uint8Array (Module.HEAPU8.subarray(yuv_wasm_data,yuv_wasm_data + renderlength + 1) ); 
               
               console.log(RenderBuffer);
               
               render(RenderBuffer,Width,Height); 
 
            };
            fileReader.readAsArrayBuffer(this.files[0]);
        }
        
    </script>

    <input type="button" value="载入文件初始化解码器" onclick="run1()" />
    <script async type="text/javascript" src="ffmpeg_decoder.js"></script>
  </body>
</html>

4.4 开启服务器

命令行运行命令,开启HTTP服务器,方便测试:

python -m http.server

4.5 测试效果

打开谷歌浏览器,输入http://127.0.0.1:8000/index.html地址,按下F12打开控制台,点击页面上的按钮看控制台输出。

(1)输入地址,打开网页

(2)按下F12,打开控制台

(3)选择一个MP4文件载入测试。获取一帧图片。