FFmpeg简易播放器的实现2-视频播放

简介: 基于 FFmpeg 和 SDL 实现的简易视频播放器,主要分为读取视频文件解码和调用 SDL 播放两大部分。本实验仅研究视频播放的实现方式。

作者:叶余

来源:https://www.cnblogs.com/leisure_chn/p/10047035.html


基于 FFmpeg 和 SDL 实现的简易视频播放器,主要分为读取视频文件解码和调用 SDL 播放两大部分。本实验仅研究视频播放的实现方式。

FFmpeg 简易播放器系列文章如下:

[1]. FFmpeg简易播放器的实现1-最简版

[2]. FFmpeg简易播放器的实现2-视频播放

[3]. FFmpeg简易播放器的实现3-音频播放

[4]. FFmpeg简易播放器的实现4-音视频播放

[5]. FFmpeg简易播放器的实现5-音视频同步

1. 视频播放器基本原理

下图引用自 “雷霄骅,视音频编解码技术零基础学习方法”,因原图太小,看不太清楚,故重新制作了一张图片。

image.png

如下内容引用自 “雷霄骅,视音频编解码技术零基础学习方法”:

解协议

将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如 HTTP,RTMP,或是 MMS 等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用 RTMP 协议传输的数据,经过解协议操作后,输出 FLV 格式的数据。

解封装

将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如 MP4,MKV,RMVB,TS,FLV,AVI 等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV 格式的数据,经过解封装操作后,输出 H.264 编码的视频码流和 AAC 编码的音频码流。

解码

将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含 AAC,MP3,AC-3 等等,视频的压缩编码标准则包含 H.264,MPEG2,VC-1 等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如 YUV420P,RGB 等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如 PCM 数据。

音视频同步

根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。

2. 简易播放器的实现-视频播放

2.1 实验平台

实验平台:  openSUSE Leap 42.3  
FFmpeg版本:4.1  
SDL版本:   2.0.9

FFmpeg 开发环境搭建可参考 “FFmpeg开发环境构建

2.2 源码清单

使用如下命令下载源码:

svn checkout https://github.com/leichn/exercises/trunk/source/ffmpeg/player_video

2.3 源码流程简述

流程比较简单,不画流程图了,简述如下:

media file --[decode]--> raw frame --[scale]--> yuv frame --[SDL]--> display  
media file ------------> p_frm_raw -----------> p_frm_yuv ---------> sdl_renderer

加上相关关键函数后,流程如下:

media_file ---[av_read_frame()]----------->  
p_packet   ---[avcodec_send_packet()]----->  
decoder    ---[avcodec_receive_frame()]--->  
p_frm_raw  ---[sws_scale()]--------------->  
p_frm_yuv  ---[SDL_UpdateYUVTexture()]---->  
display

2.4 解码及显示过程

2.4.1 读取视频数据

调用 av_read_frame() 从输入文件中读取视频数据包

// A8. 从视频文件中读取一个packet
//     packet可能是视频帧、音频帧或其他数据,解码器只会解码视频帧或音频帧,非音视频数据并不会被
//     扔掉、从而能向解码器提供尽可能多的信息
//     对于视频来说,一个packet只包含一个frame
//     对于音频来说,若是帧长固定的格式则一个packet可包含整数个frame,
//                   若是帧长可变的格式则一个packet只包含一个frame
while (av_read_frame(p_fmt_ctx, p_packet) == 0)
{
    if (p_packet->stream_index == v_idx)  // 取到一帧视频帧,则退出
    {
        break;
    }
}

2.4.2 视频数据解码

调用 avcodec_send_packet() 和 avcodec_receive_frame() 对视频数据解码

// A9. 视频解码:packet ==> frame
// A9.1 向解码器喂数据,一个packet可能是一个视频帧或多个音频帧,此处音频帧已被上一句滤掉
ret = avcodec_send_packet(p_codec_ctx, p_packet);
if (ret != 0)
{
    printf("avcodec_send_packet() failed %d\n", ret);
    res = -1;
    goto exit8;
}
// A9.2 接收解码器输出的数据,此处只处理视频帧,每次接收一个packet,将之解码得到一个frame
ret = avcodec_receive_frame(p_codec_ctx, p_frm_raw);
if (ret != 0)
{
    if (ret == AVERROR_EOF)
    {
        printf("avcodec_receive_frame(): the decoder has been fully flushed\n");
    }
    else if (ret == AVERROR(EAGAIN))
    {
        printf("avcodec_receive_frame(): output is not available in this state - "
                "user must try to send new input\n");
        continue;
    }
    else if (ret == AVERROR(EINVAL))
    {
        printf("avcodec_receive_frame(): codec not opened, or it is an encoder\n");
    }
    else
    {
        printf("avcodec_receive_frame(): legitimate decoding errors\n");
    }
    res = -1;
    goto exit8;
}

2.4.3 图像格式转换

图像格式转换的目的,是为了解码后的视频帧能被 SDL 正常显示。因为 FFmpeg 解码后得到的图像格式不一定就能被 SDL 支持,这种情况下不作图像转换是无法正常显示的。

图像转换初始化相关:

// A7. 初始化SWS context,用于后续图像转换
//     此处第6个参数使用的是FFmpeg中的像素格式,对比参考注释B4
//     FFmpeg中的像素格式AV_PIX_FMT_YUV420P对应SDL中的像素格式SDL_PIXELFORMAT_IYUV
//     如果解码后得到图像的不被SDL支持,不进行图像转换的话,SDL是无法正常显示图像的
//     如果解码后得到图像的能被SDL支持,则不必进行图像转换
//     这里为了编码简便,统一转换为SDL支持的格式AV_PIX_FMT_YUV420P==>SDL_PIXELFORMAT_IYUV
sws_ctx = sws_getContext(p_codec_ctx->width,    // src width
                         p_codec_ctx->height,   // src height
                         p_codec_ctx->pix_fmt,  // src format
                         p_codec_ctx->width,    // dst width
                         p_codec_ctx->height,   // dst height
                         AV_PIX_FMT_YUV420P,    // dst format
                         SWS_BICUBIC,           // flags
                         NULL,                  // src filter
                         NULL,                  // dst filter
                         NULL                   // param
                         );
// B4. 创建SDL_Texture
//     一个SDL_Texture对应一帧YUV数据,同SDL 1.x中的SDL_Overlay
//     此处第2个参数使用的是SDL中的像素格式,对比参考注释A7
//     FFmpeg中的像素格式AV_PIX_FMT_YUV420P对应SDL中的像素格式SDL_PIXELFORMAT_IYUV
sdl_texture = SDL_CreateTexture(sdl_renderer, 
                                SDL_PIXELFORMAT_IYUV, 
                                SDL_TEXTUREACCESS_STREAMING,
                                p_codec_ctx->width,
                                p_codec_ctx->height
                                );

图像格式转换过程调用 sws_scale() 实现:

// A10. 图像转换:p_frm_raw->data ==> p_frm_yuv->data
// 将源图像中一片连续的区域经过处理后更新到目标图像对应区域,处理的图像区域必须逐行连续
// plane: 如YUV有Y、U、V三个plane,RGB有R、G、B三个plane
// slice: 图像中一片连续的行,必须是连续的,顺序由顶部到底部或由底部到顶部
// stride/pitch: 一行图像所占的字节数,Stride=BytesPerPixel*Width+Padding,注意对齐
// AVFrame.*data[]: 每个数组元素指向对应plane
// AVFrame.linesize[]: 每个数组元素表示对应plane中一行图像所占的字节数
sws_scale(sws_ctx,                                  // sws context
          (const uint8_t *const *)p_frm_raw->data,  // src slice
          p_frm_raw->linesize,                      // src stride
          0,                                        // src slice y
          p_codec_ctx->height,                      // src slice height
          p_frm_yuv->data,                          // dst planes
          p_frm_yuv->linesize                       // dst strides
          );

2.4.4 显示

调用 SDL 相关函数将图像在屏幕上显示:

// B7. 使用新的YUV像素数据更新SDL_Rect
SDL_UpdateYUVTexture(sdl_texture,                   // sdl texture
                     &sdl_rect,                     // sdl rect
                     p_frm_yuv->data[0],            // y plane
                     p_frm_yuv->linesize[0],        // y pitch
                     p_frm_yuv->data[1],            // u plane
                     p_frm_yuv->linesize[1],        // u pitch
                     p_frm_yuv->data[2],            // v plane
                     p_frm_yuv->linesize[2]         // v pitch
                     );
// B8. 使用特定颜色清空当前渲染目标
SDL_RenderClear(sdl_renderer);
// B9. 使用部分图像数据(texture)更新当前渲染目标
SDL_RenderCopy(sdl_renderer,                        // sdl renderer
               sdl_texture,                         // sdl texture
               NULL,                                // src rect, if NULL copy texture
               &sdl_rect                            // dst rect
               );
// B10. 执行渲染,更新屏幕显示
SDL_RenderPresent(sdl_renderer);

2.5 帧率控制-定时刷新机制

上一版源码存在的两个问题:

[1]. 以固定 25 FPS 的帧率播放视频文件,对于帧率不是 25 FPS 的视频文件,播放是不正常的

[2]. 即使对于帧率是 25 FPS 的文件来说,帧率控制仍然较不准确,因为未考虑解码视频帧消耗的时间

本版源码针对上述问题作了改善,将上一版代码拆分为两个线程:定时刷新线程 + 解码主线程,定时刷新线程按计算出的帧率发送自定义 SDL 事件,通知解码主线程解码主线程收到 SDL 事件后,获取一个视频帧解码并显示。

3. 编译与验证

3.1 编译

在源码目录运行:

./compiler.sh

3.2 验证

选用 clock.avi 测试文件,测试文件下载(右键另存为):clock.avi

查看视频文件格式信息:

ffprobe clock.avi

打印视频文件信息如下:

[avi @ 0x9286c0] non-interleaved AVI
Input #0, avi, from 'clock.avi':
  Duration: 00:00:12.00, start: 0.000000, bitrate: 42 kb/s
    Stream #0:0: Video: msrle ([1][0][0][0] / 0x0001), pal8, 320x320, 1 fps, 1 tbr, 1 tbn, 1 tbc
    Stream #0:1: Audio: truespeech ([34][0][0][0] / 0x0022), 8000 Hz, mono, s16, 8 kb/s

运行测试命令:

./ffplayer clock.avi

可以听到每隔 1 秒时钟指针跳动一格,跳动 12 次后播放结束。播放过程只有图像,没有声音。播放正常。

4. 参考资料

[1] 雷霄骅,视音频编解码技术零基础学习方法

[2] 雷霄骅,FFmpeg源代码简单分析:常见结构体的初始化和销毁(AVFormatContext,AVFrame等)

[3] 雷霄骅,最简单的基于FFMPEG+SDL的视频播放器ver2(采用SDL2.0)

[4] 雷霄骅,最简单的视音频播放示例7:SDL2播放RGB/YUV

[5] 使用SDL2.0进行YUV显示

[6] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 01: Making Screencaps

[7] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 02: Outputting to the Screen

[8] YUV图像里的stride和plane的解释

[9] 图文详解YUV420数据格式

[10] YUVhttps://zh.wikipedia.org/wiki/YUV

5. 修改记录

2018-11-23 V1.0 初稿

2018-11-29 V1.1 增加定时刷新线程,使解码帧率更加准确

2019-01-12 V1.2 增加解码及显示过程说明


「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。

阿里云视频云@凡科快图.png

相关文章
|
6月前
|
存储 算法 C++
深入理解ffmpeg视频播放以及音视频同步:时间基与样本处理
深入理解ffmpeg视频播放以及音视频同步:时间基与样本处理
510 1
|
6月前
|
设计模式 编解码 C++
【ffmpeg 视频播放】深入探索:ffmpeg视频播放优化策略与设计模式的实践应用(一)
【ffmpeg 视频播放】深入探索:ffmpeg视频播放优化策略与设计模式的实践应用
197 0
|
6月前
|
存储 缓存 编解码
【FFmpeg 视频播放】深入理解多媒体播放:同步策略、缓冲技术与性能优化(一)
【FFmpeg 视频播放】深入理解多媒体播放:同步策略、缓冲技术与性能优化
296 0
|
存储 编解码 Linux
FFmpeg+SDL播放器开发实践:解析、解码、渲染全流程详解
FFmpeg+SDL播放器开发实践:解析、解码、渲染全流程详解
|
26天前
|
Linux 开发工具 Android开发
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
ijkplayer是由Bilibili基于FFmpeg3.4研发并开源的播放器,适用于Android和iOS,支持本地视频及网络流媒体播放。本文详细介绍如何在新版Android Studio中导入并使用ijkplayer库,包括Gradle版本及配置更新、导入编译好的so文件以及添加直播链接播放代码等步骤,帮助开发者顺利进行App调试与开发。更多FFmpeg开发知识可参考《FFmpeg开发实战:从零基础到短视频上线》。
101 2
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
|
6月前
|
设计模式 存储 缓存
【ffmpeg 视频播放】深入探索:ffmpeg视频播放优化策略与设计模式的实践应用(二)
【ffmpeg 视频播放】深入探索:ffmpeg视频播放优化策略与设计模式的实践应用
110 0
|
6月前
|
设计模式 编解码 算法
【ffmpeg 视频播放】深入探索:ffmpeg视频播放优化策略与设计模式的实践应用(三)
【ffmpeg 视频播放】深入探索:ffmpeg视频播放优化策略与设计模式的实践应用
86 0
|
6月前
|
设计模式 存储 缓存
【ffmpeg C++ 播放器优化实战】优化你的视频播放器:使用策略模式和单例模式进行视频优化
【ffmpeg C++ 播放器优化实战】优化你的视频播放器:使用策略模式和单例模式进行视频优化
206 0
|
6月前
|
存储 算法 C++
【FFmpeg 视频播放】深入理解多媒体播放:同步策略、缓冲技术与性能优化(二)
【FFmpeg 视频播放】深入理解多媒体播放:同步策略、缓冲技术与性能优化
297 0

热门文章

最新文章