音视频-FFmpeg视频录制、播放、编码和解码(下)

前言:上篇 音视频-FFmpeg音频录制、播放、编码和解码(上) 已经介绍了在跨平台开发工具QT(跨平台C++图形用户界面应用程序开发框架)上使用 FFmpeg 进行音频的录制、播放、编码和解码。
这篇介绍在QT上使用 FFmpeg 进行视频的录制、播放、编码和解码。

视频(Video)泛指将一系列静态影像以电信号的方式加以捕捉、记录、处理、储存、传送与重现的各种技术。连续的图像变化每秒超过24帧(frame)画面以上时,根据视觉暂留原理,人眼无法辨别单幅的静态画面;看上去是平滑连续的视觉效果,这样连续的画面叫做视频。视频技术最早是为了电视系统而发展,但现在已经发展为各种不同的格式以利消费者将视频记录下来。网络技术的发达也促使视频的纪录片段以串流媒体的形式存在于因特网之上并可被电脑接收与播放。视频与电影属于不同的技术,后者是利用照相术将动态的影像捕捉为一系列的静态照片。

一、视频-每帧图片介绍

1、图片大小

1个字节byte等于8位bit,1个像素等于3个字节:

一个像素占几个字节,要看这个像素用多少位二进制数来表示,若是真彩色,则一个像素点用24位来表示,那么一个像素占三个字节。

如果是非黑即白的二值图像,不压缩的情况下一个像素只需要1个bit。

图片大小计算:比如60x50,像素深度24(一个像素点用24位来表示,即三个8位二进制),则大小为 60x50x(24/3) = 9000b = 8.77kb

2、图片压缩

二、YUV

1、YUV介绍

YUV,是一种颜色编码方法,跟RGB是同一个级别的概念,广泛应用于多媒体领域中。

也就是说,图像中每1个像素的颜色信息,除了可以用RGB的方式表示,也可以用YUV的方式表示。

2、对比RGB

2.1、如果使用RGB

  • 比如RGB888(R、G、B每个分量都是8bit)
  • 1个像素占用24bit(3字节)

2.2、如果使用YUV

  • 1个像素可以减小至平均只占用12bit(1.5字节)
  • 体积为RGB888的一半

3、组成

RGB数据由R、G、B三个分量组成。

YUV数据由Y、U、V三个分量组成,现在通常说的YUV指的是YCbCr。

  • Y:表示亮度(Luminance、Luma),占8bit(1字节)

  • Cb、Cr:表示色度(Chrominance、Chroma)

  • Cb(U):蓝色色度分量,占8bit(1字节)

  • Cr(V):红色色度分量,占8bit(1字节)

根据上面的图片,不难看出:

  • Y分量对呈现出清晰的图像有着很大的贡献
  • Cb、Cr分量的内容不太容易识别清楚

此外,你是否感觉:Y分量的内容看着有点眼熟?其实以前黑白电视的画面就是长这样子的。

YUV的发明处在彩色电视与黑白电视的过渡时期。

  • YUV将亮度信息(Y)与色度信息(UV)分离,没有UV信息一样可以显示完整的图像,只不过是黑白的
  • 这样的设计很好地解决了彩色电视与黑白电视的兼容性问题,使黑白电视也能够接收彩色电视信号,只不过它只显示了Y分量
  • 彩色电视有Y、U、V分量,如果去掉UV分量,剩下的Y分量和黑白电视相同

4、转换公式

  • RGB的取值范围是[0,255]
  • Y的取值范围是[16,235]
  • UV的取值范围是[16,239]
Y = 0.257R + 0.504G + 0.098B + 16
U = -0.148R - 0.291G + 0.439B + 128
V = 0.439R - 0.368G - 0.071B + 128
 
R = 1.164(Y - 16) + 2.018(U - 128)
G = 1.164(Y - 16) - 0.813(V - 128) - 0.391(U - 128)
B = 1.164(Y - 16) + 1.596(V - 128)

5、色度二次采样

采样格式通常用A:B:C的形式来表示,比如4:4:4、4:2:2、4:2:0等,其中我们最需要关注的是4:2:0。

  • A:一块A*2个像素的概念区域,一般都是4
  • B:第1行的色度采样数目
  • C:第2行的色度采样数目,C的值一般要么等于B,要么等于0

6、命令行进行格式转换

6.1、其他图片格式转YUV

ffmpeg -i in.png -s 408x410 -pix_fmt yuv420p out.yuv

输出:

Input #0, png_pipe, from 'in.png':
  Duration: N/A, bitrate: N/A
    Stream #0:0: Video: png, rgba(pc), 408x410 [SAR 5669:5669 DAR 204:205], 25 tbr, 25 tbn, 25 tbc
Stream mapping:
  Stream #0:0 -> #0:0 (png (native) -> rawvideo (native))
Press [q] to stop, [?] for help
Output #0, rawvideo, to 'out.yuv':
  Metadata:
    encoder         : Lavf58.45.100
    Stream #0:0: Video: rawvideo (I420 / 0x30323449), yuv420p, 408x410 [SAR 1:1 DAR 204:205], q=2-31, 50184 kb/s, 25 fps, 25 tbn, 25 tbc
    Metadata:
      encoder         : Lavc58.91.100 rawvideo
frame=    1 fps=0.0 q=-0.0 Lsize=     245kB time=00:00:00.04 bitrate=50184.0kbits/s speed=6.86x    
video:245kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.000000%

6.2、YUV转其他图片格式

ffmpeg -s 408x410 -pix_fmt yuv420p -i out.yuv out.jpg

输出

Input #0, rawvideo, from 'out.yuv':
  Duration: 00:00:00.04, start: 0.000000, bitrate: 50184 kb/s
    Stream #0:0: Video: rawvideo (I420 / 0x30323449), yuv420p, 408x410, 50184 kb/s, 25 tbr, 25 tbn, 25 tbc
Stream mapping:
  Stream #0:0 -> #0:0 (rawvideo (native) -> mjpeg (native))
Press [q] to stop, [?] for help
[swscaler @ 0x7fcf13c3f000] deprecated pixel format used, make sure you did set range correctly
[swscaler @ 0x7fcf13c3f000] Warning: data is not aligned! This can lead to a speed loss
Output #0, image2, to 'out.jpg':
  Metadata:
    encoder         : Lavf58.45.100
    Stream #0:0: Video: mjpeg, yuvj420p(pc), 408x410, q=2-31, 200 kb/s, 25 fps, 25 tbn, 25 tbc
    Metadata:
      encoder         : Lavc58.91.100 mjpeg
    Side data:
      cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: N/A
frame=    1 fps=0.0 q=7.5 Lsize=N/A time=00:00:00.04 bitrate=N/A speed=2.95x    
video:37kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown

6.3、三种格式图片对比

image.png

7、显示YUV

可以通过ffplay显示YUV数据。

  • YUV中直接存储的是所有像素的颜色信息(可以理解为是图像的一种原始数据)
  • 必须得设置YUV的尺寸(-s)、像素格式(-pix_fmt)才能正常显示
  • 这就类似于:播放pcm时,必须得设置采样率(-ar)、声道数(-ac)、采样格式(-f)

在ffplay中
-s已经过期,建议改为:-video_size
-pix_fmt已经过期,建议改为:-pixel_format

ffplay -video_size 408x410 -pixel_format yuv420p out.yuv

输出

Input #0, rawvideo, from 'out.yuv':
  Duration: 00:00:00.04, start: 0.000000, bitrate: 50184 kb/s
    Stream #0:0: Video: rawvideo (I420 / 0x30323449), yuv420p, 408x410, 50184 kb/s, 25 tbr, 25 tbn, 25 tbc
   6.30 M-V: -0.000 fd=   0 aq=    0KB vq=    0KB sq=    0B f=0/0  

三、命令行录制视频

1、avfoundation支持的设备

ffmpeg -f avfoundation -list_devices true -i ''

输出

[AVFoundation indev @ 0x7fc719005300] AVFoundation video devices:
[AVFoundation indev @ 0x7fc719005300] [0] FaceTime高清摄像头(内建)
[AVFoundation indev @ 0x7fc719005300] [1] Capture screen 0
[AVFoundation indev @ 0x7fc719005300] AVFoundation audio devices:
[AVFoundation indev @ 0x7fc719005300] [0] MacBook Pro麦克风

2、avfoundation支持的参数

ffmpeg -h demuxer=avfoundation

输出

AVFoundation indev AVOptions:
  -list_devices          .D........ list available devices (default false)
  -pixel_format          .D........ set pixel format (default yuv420p)
  -framerate          .D........ set frame rate (default "ntsc")
  -video_size         .D........ set video size
  • -video_size:分辨率

  • -pixel_format:像素格式

  • 默认是yuv420p

  • -framerate:帧率(每秒采集多少帧画面)

  • 默认是ntsc,也就是30000/1001,约等于29.970030

  • -list_devices:true表示列出avfoundation支持的所有设备

比如:像素格式:uyvy422 分辨率:1280x720 帧率:30

3、录制YUV

ffmpeg -f avfoundation -framerate 30 -i 0 out.yuv

输出

Input #0, avfoundation, from '0':
  Duration: N/A, start: 34518.710233, bitrate: N/A
    Stream #0:0: Video: rawvideo (UYVY / 0x59565955), uyvy422, 1280x720, 6.33 tbr, 1000k tbn, 1000k tbc
File 'out.yuv' already exists. Overwrite? [y/N] y
Stream mapping:
  Stream #0:0 -> #0:0 (rawvideo (native) -> rawvideo (native))
Press [q] to stop, [?] for help
Output #0, rawvideo, to 'out.yuv':
  Metadata:
    encoder         : Lavf58.45.100
    Stream #0:0: Video: rawvideo (UYVY / 0x59565955), uyvy422, 1280x720, q=2-31, 93388 kb/s, 6.33 fps, 6.33 tbn, 6.33 tbc
    Metadata:
      encoder         : Lavc58.91.100 rawvideo
frame=   67 fps= 12 q=-0.0 Lsize=  120600kB time=00:00:10.57 bitrate=93388.8kbits/s dup=25 drop=130 speed=1.89x    
video:120600kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.000000%
Exiting normally, received signal 2.

可以指定具体的参数去录制

ffplay -video_size 1280x720 -pixel_format uyvy422 -framerate 30 out_new.yuv

4、播放录制好的YUV

ffplay -video_size 1280x720 -pixel_format uyvy422 -framerate 30 out.yuv

输出

Input #0, rawvideo, from 'out.yuv':
  Duration: 00:00:02.23, start: 0.000000, bitrate: 442368 kb/s
    Stream #0:0: Video: rawvideo (UYVY / 0x59565955), uyvy422, 1280x720, 442368 kb/s, 30 tbr, 30 tbn, 30 tbc
  17.23 M-V:  0.001 fd=   0 aq=    0KB vq=    0KB sq=    0B f=0/0 

5、录制mp4

ffmpeg -f avfoundation -i 0:0 -framerate 60 -c:v  libx264 -video_size 1280x720 -pixel_format yuv420p -framerate 30 in.mp4

6、裁剪视频

-ss 从哪里开始,传秒
-t 裁剪多少秒,传秒

ffmpeg -ss 0 -t 10 -i in.mp4 -vcodec copy -acodec copy out.mp4

输出

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'in.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf58.45.100
  Duration: 00:00:08.83, start: 0.000000, bitrate: 1624 kb/s
    Stream #0:0(und): Video: h264 (High 4:2:2) (avc1 / 0x31637661), yuv422p, 1280x720, 1557 kb/s, 30 fps, 30 tbr, 15360 tbn, 60 tbc (default)
    Metadata:
      handler_name    : VideoHandler
    Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, mono, fltp, 56 kb/s (default)
    Metadata:
      handler_name    : SoundHandler
Output #0, mp4, to 'out.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf58.45.100
    Stream #0:0(und): Video: h264 (High 4:2:2) (avc1 / 0x31637661), yuv422p, 1280x720, q=2-31, 1557 kb/s, 30 fps, 30 tbr, 15360 tbn, 15360 tbc (default)
    Metadata:
      handler_name    : VideoHandler
    Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, mono, fltp, 56 kb/s (default)
    Metadata:
      handler_name    : SoundHandler
Stream mapping:
  Stream #0:0 -> #0:0 (copy)
  Stream #0:1 -> #0:1 (copy)
Press [q] to stop, [?] for help
frame=  176 fps=0.0 q=-1.0 Lsize=    1146kB time=00:00:04.96 bitrate=1890.5kbits/s speed=1.57e+03x    
video:1097kB audio:41kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.712026%

四、编码录制视频

整体的流程跟《音频录制02_编程》类似。

1、依赖库

extern "C" {
#include 
#include 
#include 
#include 
#include 
}

2、宏定义

#ifdef Q_OS_WIN
    // 格式名称
    #define FMT_NAME "dshow"
    // 设备名称
    #define DEVICE_NAME "video=Integrated Camera"
    // YUV文件名
    #define FILENAME "F:/out.yuv"
#else
    #define FMT_NAME "avfoundation"
    #define DEVICE_NAME "0"
    #define FILENAME "/Users/mj/Desktop/out.yuv"
#endif
 
#define ERROR_BUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf));

3、权限申请

在Mac平台,有2个注意点:

  • 需要在Info.plist中添加摄像头的使用说明,申请摄像头的使用权限
  • 使用Debug模式运行程序
  • 不然会出现闪退的情况




        NSCameraUsageDescription
        使用摄像头采集您的靓照


4、注册设备

在整个程序的运行过程中,只需要执行1次注册设备的代码。

// 初始化libavdevice并注册所有输入和输出设备
avdevice_register_all();

5、获取输入格式对象

// 获取输入格式对象
AVInputFormat *fmt = av_find_input_format(FMT_NAME);
if (!fmt) {
    qDebug() << "av_find_input_format error" << FMT_NAME;
    return;
}

6、打开输入设备

// 格式上下文
AVFormatContext *ctx = nullptr;
 
// 传递给输入设备的参数
AVDictionary *options = nullptr;
av_dict_set(&options, "video_size", "640x480", 0);
av_dict_set(&options, "pixel_format", "yuyv422", 0);
av_dict_set(&options, "framerate", "30", 0);
 
// 打开输入设备
int ret = avformat_open_input(&ctx, DEVICE_NAME, fmt, &options);
if (ret < 0) {
    ERROR_BUF(ret);
    qDebug() << "avformat_open_input error" << errbuf;
    return;
}

7、打开输出文件

// 打开文件
QFile file(FILENAME);
if (!file.open(QFile::WriteOnly)) {
    qDebug() << "file open error" << FILENAME;
 
    // 关闭输入设备
    avformat_close_input(&ctx);
    return;
}

8、采集视频数据

// 计算每一帧的大小
AVCodecParameters *params = ctx->streams[0]->codecpar;
int imageSize = av_image_get_buffer_size(
                    (AVPixelFormat) params->format,
                    params->width, params->height,
                    1);
 
// 数据包
AVPacket *pkt = av_packet_alloc();
while (!isInterruptionRequested()) {
    // 不断采集数据
    ret = av_read_frame(ctx, pkt);
 
    if (ret == 0) { // 读取成功
        // 将数据写入文件
        file.write((const char *) pkt->data, imageSize);
        /*
         这里要使用imageSize,而不是pkt->size。
         pkt->size有可能比imageSize大(比如在Mac平台),
         使用pkt->size会导致写入一些多余数据到YUV文件中,
         进而导致YUV内容无法正常播放
        */
 
        // 释放资源
        av_packet_unref(pkt);
    } else if (ret == AVERROR(EAGAIN)) { // 资源临时不可用
        continue;
    } else { // 其他错误
        ERROR_BUF(ret);
        qDebug() << "av_read_frame error" << errbuf;
        break;
    }
}

9、释放资源

// 释放资源
av_packet_free(&pkt);
// 关闭文件
file.close();
// 关闭设备
avformat_close_input(&ctx);

五、显示BMP图片

为什么是显示BMP图片?而不是显示JPG或PNG图片?

  • 因为SDL内置了加载BMP的API,使用起来会更加简单,便于初学者学习使用SDL
  • 如果想要轻松加载JPG、PNG等其他格式的图片,可以使用第三方库:SDL_image

1、宏定义

#include 
#include 
 
// 出错了就执行goto end
#define END(judge, func) \
    if (judge) { \
        qDebug() << #func << "Error" << SDL_GetError(); \
        goto end; \
    }

2、变量定义

// 窗口
SDL_Window *window = nullptr;
// 渲染上下文
SDL_Renderer *renderer = nullptr;
// 像素数据
SDL_Surface *surface = nullptr;
// 纹理(直接跟特定驱动程序相关的像素数据)
SDL_Texture *texture = nullptr;

3、初始化子系统

// 初始化Video子系统
END(SDL_Init(SDL_INIT_VIDEO), SDL_Init);

4、加载BMP

// 加载BMP
surface = SDL_LoadBMP("F:/in.bmp");
END(!surface, SDL_LoadBMP);

5、创建窗口

// 创建窗口
window = SDL_CreateWindow(
             // 窗口标题
             "SDL显示BMP图片",
             // 窗口X(未定义)
             SDL_WINDOWPOS_UNDEFINED,
             // 窗口Y(未定义)
             SDL_WINDOWPOS_UNDEFINED,
             // 窗口宽度(跟图片宽度一样)
             surface->w,
             // 窗口高度(跟图片高度一样)
             surface->h,
             // 显示窗口
             SDL_WINDOW_SHOWN
         );
END(!window, SDL_CreateWindow);

6、创建渲染上下文

// 创建渲染上下文(默认的渲染目标是window)
renderer = SDL_CreateRenderer(window, -1,
                              SDL_RENDERER_ACCELERATED |
                              SDL_RENDERER_PRESENTVSYNC);
if (!renderer) { // 说明开启硬件加速失败
    renderer = SDL_CreateRenderer(window, -1, 0);
}
END(!renderer, SDL_CreateRenderer);

7、创建纹理

// 创建纹理
texture = 
SDL_CreateTextureFromSurface
(

              renderer,

              surface);

END
(!texture, SDL_CreateTextureFromSurface);

8、渲染

// 设置绘制颜色(这里随便设置了一个颜色:黄色)
END(SDL_SetRenderDrawColor(renderer,
                             255, 255, 0,
                             SDL_ALPHA_OPAQUE),
      SDL_SetRenderDrawColor);
 
// 用DrawColor清除渲染目标
END(SDL_RenderClear(renderer),
      SDL_RenderClear);
 
// 复制纹理到渲染目标上
END(SDL_RenderCopy(renderer, texture, nullptr, nullptr),
      SDL_RenderCopy);
 
// 将此前的所有需要渲染的内容更新到屏幕上
SDL_RenderPresent(renderer);

9、延迟退出

// 延迟3秒退出
SDL_Delay(3000);

10、释放资源

end:
    // 释放资源
    SDL_FreeSurface(surface);
    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

五、显示YUV图片

使用SDL显示一张YUV图片,整体过程跟《显示BMP图片》比较像。

1、宏定义

#include 
#include 
#define END(judge, func) \
    if (judge) { \
        qDebug() << #func << "error" << SDL_GetError(); \
        goto end; \
    }
#define FILENAME "F:/res/in.yuv"
#define PIXEL_FORMAT SDL_PIXELFORMAT_IYUV
#define IMG_W 512
#define IMG_H 512

2、变量定义

// 窗口
SDL_Window *window = nullptr;
 
// 渲染上下文
SDL_Renderer *renderer = nullptr;
 
// 纹理(直接跟特定驱动程序相关的像素数据)
SDL_Texture *texture = nullptr;
 
// 文件
QFile file(FILENAME);

3、初始化子系统

// 初始化Video子系统
END(SDL_Init(SDL_INIT_VIDEO), SDL_Init);

4、创建窗口

// 创建窗口
window = SDL_CreateWindow(
             // 窗口标题
             "SDL显示YUV图片",
             // 窗口X(未定义)
             SDL_WINDOWPOS_UNDEFINED,
             // 窗口Y(未定义)
             SDL_WINDOWPOS_UNDEFINED,
             // 窗口宽度(跟图片宽度一样)
             surface->w,
             // 窗口高度(跟图片高度一样)
             surface->h,
             // 显示窗口
             SDL_WINDOW_SHOWN
         );
END(!window, SDL_CreateWindow);

5、创建渲染上下文

// 创建渲染上下文(默认的渲染目标是window)
renderer = SDL_CreateRenderer(window, -1,
                              SDL_RENDERER_ACCELERATED |
                              SDL_RENDERER_PRESENTVSYNC);
if (!renderer) { // 说明开启硬件加速失败
    renderer = SDL_CreateRenderer(window, -1, 0);
}
END(!renderer, SDL_CreateRenderer);

6、创建纹理

// 创建纹理
texture = SDL_CreateTexture(renderer,
                            PIXEL_FORMAT,
                            SDL_TEXTUREACCESS_STREAMING,
                            IMG_W, IMG_H);
END(!texture, SDL_CreateTexture);

7、打开文件

// 打开文件
if (!file.open(QFile::ReadOnly)) {
    qDebug() << "file open error" << FILENAME;
    goto end;
}

8、渲染

// 将YUV的像素数据填充到texture
END(SDL_UpdateTexture(texture, nullptr, file.readAll().data(), IMG_W),
    SDL_UpdateTexture);
 
// 设置绘制颜色(画笔颜色)
END(SDL_SetRenderDrawColor(renderer,
                           0, 0, 0, SDL_ALPHA_OPAQUE),
    SDL_SetRenderDrawColor);
 
// 用绘制颜色(画笔颜色)清除渲染目标
END(SDL_RenderClear(renderer),
    SDL_RenderClear);
 
// 拷贝纹理数据到渲染目标(默认是window)
END(SDL_RenderCopy(renderer, texture, nullptr, nullptr),
    SDL_RenderCopy);
 
// 更新所有的渲染操作到屏幕上
SDL_RenderPresent(renderer);

9、延迟退出

// 延迟3秒退出 
SDL_Delay(3000);

10、释放资源

end:
    file.close();
    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

六、H.264编码介绍

1、前言

主要介绍一种非常流行的视频编码:H.264

计算一下:10秒钟1080p(1920x1080)、30fps的YUV420P原始视频,需要占用多大的存储空间?

  • (10 * 30) * (1920 * 1080) * 1.5 = 933120000字节 ≈ 889.89MB
  • 可以看得出来,原始视频的体积是非常巨大的

由于网络带宽和硬盘存储空间都是非常有限的,因此,需要先使用视频编码技术(比如H.264编码)对原始视频进行压缩,然后再进行存储和分发。H.264编码的压缩比可以达到至少是100:1

2、介绍

H.264,又称为MPEG-4 Part 10,Advanced Video Coding。

  • 译为:MPEG-4第10部分,高级视频编码
  • 简称:MPEG-4 AVC

H.264是迄今为止视频录制、压缩和分发的最常用格式。截至2019年9月,已有91%的视频开发人员使用了该格式。H.264提供了明显优于以前任何标准的压缩性能。H.264因其是蓝光盘的其中一种编解码标准而著名,所有蓝光盘播放器都必须能解码H.264。

3、编码器

H.264标准允许制造厂商自由地开发具有竞争力的创新产品,它并没有定义一个编码器,而是定义了编码器应该产生的输出码流。

x264是一款免费的高性能的H.264开源编码器。x264编码器在FFmpeg中的名称是libx264。

AVCodec *codec = avcodec_find_encoder_by_name("libx264");

4、解码器

H.264标准中定义了一个解码方法,但是制造厂商可以自由地开发可选的具有竞争力的、新的解码器,前提是他们能够获得与标准中采用的方法同样的结果。

FFmpeg默认已经内置了一个H.264的解码器,名称是h264。

AVCodec *codec1 = avcodec_find_decoder_by_name("h264");
 // 或者 
AVCodec *codec2 = avcodec_find_decoder(AV_CODEC_ID_H264);

5、编码过程与原理

H.264的编程过程比较复杂,本文只介绍大体的框架和脉络,具体细节就不展开了。

大体可以归纳为以下几个主要步骤:

  • 划分帧类型
  • 帧内/帧间编码
  • 变换 + 量化
  • 滤波
  • 熵编码

七、H.264编码实战

1、命令行编码

ffmpeg -s 640x480 -pix_fmt yuv420p -i in.yuv -c:v libx264 out.h264 
# -c:v libx264是指定使用libx264作为编码器

接下来主要讲解如何通过代码的方式使用H.264编码,用到了avcodec、avutil两个库,整体过程跟《AAC编码实战》类似。

2、类的声明

extern "C" {
#include 
}
 
typedef struct {
    const char *filename;
    int width;
    int height;
    AVPixelFormat pixFmt;
    int fps;
} VideoEncodeSpec;
 
class FFmpegs {
public:
    FFmpegs();
 
    static void h264Encode(VideoEncodeSpec &in,
                           const char *outFilename);
};

3、类的使用

VideoEncodeSpec in;
in.filename = "F:/res/in.yuv";
in.width = 640;
in.height = 480;
in.fps = 30;
in.pixFmt = AV_PIX_FMT_YUV420P;

FFmpegs::h264Encode(in, "F:/res/out.h264");

4、宏定义

extern "C" {
#include 
#include 
#include 
}

#define ERROR_BUF(ret) \   
 char errbuf[1024]; \    
 av_strerror(ret, errbuf, sizeof (errbuf));

5、变量定义

// 文件
QFile inFile(in.filename);
QFile outFile(outFilename);
 
// 一帧图片的大小
int imgSize = av_image_get_buffer_size(in.pixFmt, in.width, in.height, 1);
 
// 返回结果
int ret = 0;

// 编码器
AVCodec *codec = nullptr;
 
// 编码上下文
AVCodecContext *ctx = nullptr;
 
// 存放编码前的数据(yuv)
AVFrame *frame = nullptr;
 
// 存放编码后的数据(h264)
AVPacket *pkt = nullptr;

6、初始化

// 获取编码器
codec = avcodec_find_encoder_by_name("libx264");
if (!codec) {
    qDebug() << "encoder not found";
    return;
}
 
// 检查输入数据的采样格式
if (!check_pix_fmt(codec, in.pixFmt)) {
    qDebug() << "unsupported pixel format"
             << av_get_pix_fmt_name(in.pixFmt);
    return;
}
 
// 创建编码上下文
ctx = avcodec_alloc_context3(codec);
if (!ctx) {
    qDebug() << "avcodec_alloc_context3 error";
    return;
}
 
// 设置yuv参数
ctx->width = in.width;
ctx->height = in.height;
ctx->pix_fmt = in.pixFmt;
// 设置帧率(1秒钟显示的帧数是in.fps)
ctx->time_base = {1, in.fps};
 
// 打开编码器
ret = avcodec_open2(ctx, codec, nullptr);
if (ret < 0) {
    ERROR_BUF(ret);
    qDebug() << "avcodec_open2 error" << errbuf;
    goto end;
}
 
// 创建AVFrame
frame = av_frame_alloc();
if (!frame) {
    qDebug() << "av_frame_alloc error";
    goto end;
}
frame->width = ctx->width;
frame->height = ctx->height;
frame->format = ctx->pix_fmt;
frame->pts = 0;
 
// 利用width、height、format创建缓冲区
ret = av_image_alloc(frame->data, frame->linesize,
                     in.width, in.height, in.pixFmt, 1);
if (ret < 0) {
    ERROR_BUF(ret);
    qDebug() << "av_frame_get_buffer error" << errbuf;
    goto end;
}
 
// 创建AVPacket
pkt = av_packet_alloc();
if (!pkt) {
    qDebug() << "av_packet_alloc error";
    goto end;
}

7、编码

// 打开文件
if (!inFile.open(QFile::ReadOnly)) {
    qDebug() << "file open error" << in.filename;
    goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
    qDebug() << "file open error" << outFilename;
    goto end;
}
 
// 读取数据到frame中
while ((ret = inFile.read((char *) frame->data[0],
                          imgSize)) > 0) {
    // 进行编码
    if (encode(ctx, frame, pkt, outFile) < 0) {
        goto end;
    }
 
    // 设置帧的序号
    frame->pts++;
}
 
// 刷新缓冲区
encode(ctx, nullptr, pkt, outFile);

encode函数的实现如下所示:

// 返回负数:中途出现了错误
// 返回0:编码操作正常完成
static int encode(AVCodecContext *ctx,
                  AVFrame *frame,
                  AVPacket *pkt,
                  QFile &outFile) {
    // 发送数据到编码器
    int ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_frame error" << errbuf;
        return ret;
    }
 
    // 不断从编码器中取出编码后的数据
    while (true) {
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            // 继续读取数据到frame,然后送到编码器
            return 0;
        } else if (ret < 0) { // 其他错误
            return ret;
        }
 
        // 成功从编码器拿到编码后的数据
        // 将编码后的数据写入文件
        outFile.write((char *) pkt->data, pkt->size);
 
        // 释放pkt内部的资源
        av_packet_unref(pkt);
    }
}

8、回收资源

end:
    // 关闭文件
    inFile.close();
    outFile.close();
    // 释放资源
    if (frame) {
        av_freep(&frame->data[0]);
        av_frame_free(&frame);
    }
    av_packet_free(&pkt);
    avcodec_free_context(&ctx);

八、H.264解码实战

1、命令行解码

ffmpeg -c:v h264 -i in.h264 out.yuv# -c:v h264是指定使用h264作为解码器

接下来主要讲解如何通过代码的方式解码H.264数据,用到了avcodec、avutil两个库,整体过程跟《AAC解码实战》类似。

2、类的声明

extern "C" {
#include 
}
 
typedef struct {
    const char *filename;
    int width;
    int height;
    AVPixelFormat pixFmt;
    int fps;
} VideoDecodeSpec;
 
class FFmpegs {
public:
    FFmpegs();
 
    static void h264Decode(const char *inFilename,
                           VideoDecodeSpec &out);
};

3、类的使用

VideoDecodeSpec out;
out.filename = "F:/res/out.yuv";
FFmpegs::h264Decode("F:/res/in.h264", out);
qDebug() << out.width << out.height         << out.fps << av_get_pix_fmt_name(out.pixFmt);

4、宏定义

extern "C" {
#include 
#include 
#include 
}
 
#define ERROR_BUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf));
 
// 输入缓冲区的大小
#define IN_DATA_SIZE 4096

5、变量定义

// 返回结果
int ret = 0;
 
// 用来存放读取的输入文件数据(h264)
char inDataArray[IN_DATA_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
char *inData = inDataArray;
 
// 每次从输入文件中读取的长度(h264)
// 输入缓冲区中,剩下的等待进行解码的有效数据长度
int inLen;
// 是否已经读取到了输入文件的尾部
int inEnd = 0;
 
// 文件
QFile inFile(inFilename);
QFile outFile(out.filename);
 
// 解码器
AVCodec *codec = nullptr;
// 上下文
AVCodecContext *ctx = nullptr;
// 解析器上下文
AVCodecParserContext *parserCtx = nullptr;
 
// 存放解码前的数据(h264)
AVPacket *pkt = nullptr;
// 存放解码后的数据(yuv)
AVFrame *frame = nullptr;

6、初始化

// 获取解码器
//    codec = avcodec_find_decoder_by_name("h264");
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
    qDebug() << "decoder not found";
    return;
}
 
// 初始化解析器上下文
parserCtx = av_parser_init(codec->id);
if (!parserCtx) {
    qDebug() << "av_parser_init error";
    return;
}
 
// 创建上下文
ctx = avcodec_alloc_context3(codec);
if (!ctx) {
    qDebug() << "avcodec_alloc_context3 error";
    goto end;
}
 
// 创建AVPacket
pkt = av_packet_alloc();
if (!pkt) {
    qDebug() << "av_packet_alloc error";
    goto end;
}
 
// 创建AVFrame
frame = av_frame_alloc();
if (!frame) {
    qDebug() << "av_frame_alloc error";
    goto end;
}
 
// 打开解码器
ret = avcodec_open2(ctx, codec, nullptr);
if (ret < 0) {
    ERROR_BUF(ret);
    qDebug() << "avcodec_open2 error" << errbuf;
    goto end;
}

7、解码

// 打开文件
if (!inFile.open(QFile::ReadOnly)) {
    qDebug() << "file open error:" << inFilename;
    goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
    qDebug() << "file open error:" << out.filename;
    goto end;
}
 
// 读取文件数据
do {
    inLen = inFile.read(inDataArray, IN_DATA_SIZE);
    // 设置是否到了文件尾部
    inEnd = !inLen;
 
    // 让inData指向数组的首元素
    inData = inDataArray;
 
    // 只要输入缓冲区中还有等待进行解码的数据
    while (inLen > 0 || inEnd) {
        // 到了文件尾部(虽然没有读取任何数据,但也要调用av_parser_parse2,修复bug)
        // 经过解析器解析
        ret = av_parser_parse2(parserCtx, ctx,
                               &pkt->data, &pkt->size,
                               (uint8_t *) inData, inLen,
                               AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
 
        if (ret < 0) {
            ERROR_BUF(ret);
            qDebug() << "av_parser_parse2 error" << errbuf;
            goto end;
        }
 
        // 跳过已经解析过的数据
        inData += ret;
        // 减去已经解析过的数据大小
        inLen -= ret;
 
        qDebug() << inEnd << pkt->size << ret;
 
        // 解码
        if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
            goto end;
        }
 
        // 如果到了文件尾部
        if (inEnd) break;
    }
} while (!inEnd);
 
// 刷新缓冲区
//    pkt->data = nullptr;
//    pkt->size = 0;
//    decode(ctx, pkt, frame, outFile);
decode(ctx, nullptr, frame, outFile);
 
// 赋值输出参数
out.width = ctx->width;
out.height = ctx->height;
out.pixFmt = ctx->pix_fmt;
// 用framerate.num获取帧率,并不是time_base.den
out.fps = ctx->framerate.num;
 
end:
inFile.close();
outFile.close();
av_packet_free(&pkt);
av_frame_free(&frame);
av_parser_close(parserCtx);
avcodec_free_context(&ctx);

decode函数的实现如下所示:

static int decode(AVCodecContext *ctx,
                  AVPacket *pkt,
                  AVFrame *frame,
                  QFile &outFile) {
    // 发送压缩数据到解码器
    int ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_packet error" << errbuf;
        return ret;
    }
 
    while (true) {
        // 获取解码后的数据
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            ERROR_BUF(ret);
            qDebug() << "avcodec_receive_frame error" << errbuf;
            return ret;
        }
 
        // 将解码后的数据写入文件
        // 写入Y平面
        outFile.write((char *) frame->data[0],
                      frame->linesize[0] * ctx->height);
        // 写入U平面
        outFile.write((char *) frame->data[1],
                      frame->linesize[1] * ctx->height >> 1);
        // 写入V平面
        outFile.write((char *) frame->data[2],
                      frame->linesize[2] * ctx->height >> 1);
    }
}

8、回收资源

end:    
inFile.close();    
outFile.close();
av_packet_free(&pkt);
av_frame_free(&frame);
av_parser_close(parserCtx);
avcodec_free_context(&ctx);

备注:以上是使用FFmpeg视频录制、播放、编码和解码相关介绍。

注意:本文只用于个人记录和学习,原文请参考:秒懂音视频开发
源码下载请参考:CoderMJLee/audio-video-dev-tutorial

你可能感兴趣的:(音视频-FFmpeg视频录制、播放、编码和解码(下))