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

本文为作者原创:https://www.cnblogs.com/leisure_chn/p/10040202.html,转载请注明出处

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

本实验仅实现最简单的视频播放流程,不考虑细节,不考虑音频。本实验主要参考如下两篇文章:
[1]. 最简单的基于FFMPEG+SDL的视频播放器ver2(采用SDL2.0)
[2]. An ffmpeg and SDL Tutorial

FFmpeg 简易播放器系列文章如下:
[1]. FFmpeg简易播放器的实现1-最简版
[2]. FFmpeg简易播放器的实现2-视频播放
[3]. FFmpeg简易播放器的实现3-音频播放
[4]. FFmpeg简易播放器的实现4-音视频播放
[5]. FFmpeg简易播放器的实现5-音视频同步

1. 视频播放器基本原理

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

图1 播放器基本原理示意图

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

解协议
将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如 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 15.6  
FFmpeg 版本:7.1.1  
SDL 版本:   2.0  

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

2.2 源码清单

下载源码:

git clone https://github.com/leichn/ffmpeg_training.git

ffmpeg_training/player_simple 目录是本文源码目录。ffmpeg_training/video 目录存放了一些视频文件可用于测试。

整份代码只有 100 行左右,源码清单如下:

/*****************************************************************
 * ffplayer.c
 *
 * history:
 *   2018-11-27 - [lei]     Create file: a simplest ffmpeg player
 *   2025-03-28 - [lei]     Update ffmpeg to 7.1.1
******************************************************************/
#include <stdio.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_rect.h>
#include <SDL2/SDL_render.h>
#include <SDL2/SDL_video.h>

int main(int argc, char *argv[])
{
    AVFormatContext* p_fmt_ctx = NULL;
    avformat_open_input(&p_fmt_ctx, argv[1], NULL, NULL);
    avformat_find_stream_info(p_fmt_ctx, NULL);
    av_dump_format(p_fmt_ctx, 0, argv[1], 0);

    int video_idx = -1;
    int frame_rate = -1;
    for (int i=0; i<p_fmt_ctx->nb_streams; i++)
    {
        if (p_fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            video_idx = i;
            printf("Find a video stream, index %d\n", video_idx);
            frame_rate = p_fmt_ctx->streams[i]->avg_frame_rate.num / 
                         p_fmt_ctx->streams[i]->avg_frame_rate.den;
            break;
        }
    }

    AVCodecParameters* p_codec_par = p_fmt_ctx->streams[video_idx]->codecpar;
    const AVCodec* p_codec = avcodec_find_decoder(p_codec_par->codec_id);
    AVCodecContext* p_codec_ctx = avcodec_alloc_context3(p_codec);
    avcodec_parameters_to_context(p_codec_ctx, p_codec_par);
    avcodec_open2(p_codec_ctx, p_codec, NULL);

    AVFrame* p_frm_raw = av_frame_alloc();
    AVFrame* p_frm_yuv = av_frame_alloc();
    int buf_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, p_codec_ctx->width, p_codec_ctx->height, 1);
    uint8_t* p_buffer = (uint8_t *)av_malloc(buf_size);
    av_image_fill_arrays(p_frm_yuv->data, p_frm_yuv->linesize, p_buffer, AV_PIX_FMT_YUV420P, 
                         p_codec_ctx->width, p_codec_ctx->height, 1);

    AVPacket* p_packet = (AVPacket *)av_malloc(sizeof(AVPacket));

    struct SwsContext* p_sws_ctx = 
    sws_getContext(p_codec_ctx->width, p_codec_ctx->height, p_codec_ctx->pix_fmt,
                   p_codec_ctx->width, p_codec_ctx->height, AV_PIX_FMT_YUV420P,
                   SWS_BICUBIC, NULL, NULL, NULL);


    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER);
    SDL_Window* p_sdl_screen = 
    SDL_CreateWindow("simple ffplayer", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
                     p_codec_ctx->width, p_codec_ctx->height, SDL_WINDOW_OPENGL);
    SDL_Renderer* p_sdl_renderer = SDL_CreateRenderer(p_sdl_screen, -1, 0);
    SDL_Texture* p_sdl_texture = 
    SDL_CreateTexture(p_sdl_renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,
                      p_codec_ctx->width, p_codec_ctx->height);

    SDL_Event sdl_event;
    SDL_Rect sdl_rect;
    sdl_rect.x = 0;
    sdl_rect.y = 0;
    sdl_rect.w = p_codec_ctx->width;
    sdl_rect.h = p_codec_ctx->height;

    while (av_read_frame(p_fmt_ctx, p_packet) == 0)
    {
        if (p_packet->stream_index == video_idx)
        {
            avcodec_send_packet(p_codec_ctx, p_packet);
            avcodec_receive_frame(p_codec_ctx, p_frm_raw);

            sws_scale(p_sws_ctx, (const uint8_t* const*)p_frm_raw->data, p_frm_raw->linesize,
                      0, p_codec_ctx->height, p_frm_yuv->data, p_frm_yuv->linesize);
            
            SDL_UpdateYUVTexture(p_sdl_texture, &sdl_rect,
                                 p_frm_yuv->data[0], p_frm_yuv->linesize[0],
                                 p_frm_yuv->data[1], p_frm_yuv->linesize[1],
                                 p_frm_yuv->data[2], p_frm_yuv->linesize[2]);
            SDL_RenderClear(p_sdl_renderer);
            SDL_RenderCopy(p_sdl_renderer, p_sdl_texture, NULL, &sdl_rect);
            SDL_RenderPresent(p_sdl_renderer);  

            SDL_Delay(1000/frame_rate);
        }

        av_packet_unref(p_packet);
    }

    SDL_Quit();
    sws_freeContext(p_sws_ctx);
    av_packet_unref(p_packet);
    av_free(p_buffer);
    av_frame_free(&p_frm_yuv);
    av_frame_free(&p_frm_raw);
    avcodec_free_context(&p_codec_ctx);
    avformat_close_input(&p_fmt_ctx);

    return 0;
}

源码清单中涉及的一些概念简述如下:

container:
容器,也称封装器,对应数据结构 AVFormatContext。封装是指将流数据组装为指定格式的文件。封装格式有 AVI、MP4 等。FFmpeg 可识别五种流类型:视频 video(v)、音频 audio(a)、attachment(t)、数据 data(d)、字幕 subtitle。

codec:
编解码器,对应数据结构 AVCodec。编码器将未压缩的原始图像或音频数据编码为压缩数据。解码器与之相反。

codec context:
编解码器上下文,对应数据结构 AVCodecContext。此为非常重要的一个数据结构,后文分析。各API大量使用 AVCodecContext 来引用编解码器。

codec par:
编解码器参数,对应数据结构 AVCodecParameters。新版本增加的字段。新版本建议使用 AVStream->codepar 替代 AVStream->codec。

packet:
经过编码的数据包,对应数据结构 AVPacket。通过 av_read_frame() 从媒体文件中获取得到的一个 packet 可能包含多个(整数个)音频帧或单个视频帧,或者其他类型的流数据。

frame:
未编码的原始数据帧,对应数据结构 AVFrame。解码器将 packet 解码后生成 frame。

plane:
如 YUV 有 Y、U、V 三个 plane,RGB 有 R、G、B 三个 plane。

slice:
图像中一片连续的行,必须是连续的,顺序由顶部到底部或由底部到顶部

stride/pitch:
一行图像所占的字节数,Stride = BytesPerPixel × Width,按 x 字节对齐[待确认]

sdl window:
播放视频时弹出的窗口,对应数据结构SDL_Window。在 SDL1.x 版本中,只可以创建一个窗口。在 SDL2.0 版本中,可以创建多个窗口。

sdl texture:
对应数据结构 SDL_Texture。一个SDL_Texture对应一帧解码后的图像数据。

sdl renderer:
渲染器,对应数据结构SDL_Renderer。将 SDL_Texture 渲染至 SDL_Window。

sdl rect:
对应数据结构 SDL_Rect,SDL_Rect 用于确定 SDL_Texture 显示的位置。一个 SDL_Window 上可以显示多个 SDL_Rect。这样可以实现同一窗口的分屏显示。

2.3 源码流程简述

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

media file --[decode]--> raw frame --[scale]--> yuv frame --[SDL]--> display  
media file ------------> p_frm_raw -----------> p_frm_yuv ---------> p_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.3.1 初始化

初始化解码及显示环境。

2.3.2 读取视频数据

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

2.3.3 视频数据解码

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

2.3.4 图像格式转换

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

2.3.5 显示

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

3. 编译与验证

3.1 编译

在源码目录运行:

./compile.sh

编译后在当前目录生成可执行程序 ffplayer。

3.2 验证

选用 ffmpeg_training/video/clock.avi 作为测试文件,运行测试命令:

./ffplayer clock.avi

4. 参考资料

[1] 雷霄骅,视音频编解码技术零基础学习方法
[2] 雷霄骅,FFmpeg源代码简单分析:常见结构体的初始化和销毁(AVFormatContext,AVFrame等)
[3] 雷霄骅,最简单的基于FFMPEG+SDL的视频播放器ver2(采用SDL2.0)
[4] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 01: Making Screencaps
[5] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 02: Outputting to the Screen
[6] YUV图像里的stride和plane的解释
[7] 图文详解YUV420数据格式
[8] YUVhttps://zh.wikipedia.org/wiki/YUV

5. 修改记录

2018-11-23 V1.0 初稿
2018-11-29 V1.1 增加定时刷新线程,使解码帧率更加准确
2025-03-21 V1.2 FFmpeg 版本 4.2.2 升级至 7.1.1

posted @ 2018-11-29 18:52  叶余  阅读(19140)  评论(4)    收藏  举报