ffmpeg播放器实现详解 - 视频显示

ffplay是ffmpeg源码中一个自带的开源播放器实例,同时支持本地视频文件的播放以及在线流媒体播放,功能非常强大。

FFplay: FFplay is a very simple and portable media player using the FFmpeg libraries and the SDL library. It is mostly used as a testbed for the various FFmpeg APIs.

ffplay中的代码充分调用了ffmpeg中的函数库,因此,想学习ffmpeg的使用,或基于ffmpeg开发一个自己的播放器,ffplay都是一个很好的切入点

由于ffmpeg本身的开发文档比较少,且ffplay播放器源码的实现相对复杂,除了基础的ffmpeg组件调用外,还包含视频帧的渲染、音频帧的播放、音视频同步策略及线程调度等问题。

因此,这里我们以ffmpeg官网推荐的一个ffplay播放器简化版本的开发例程为基础,在此基础上循序渐进由浅入深,最终探讨实现一个视频播放器的完整逻辑

在上篇文章中介绍了如果搭建一个基于ffmpeg的播放器框架
本文在上篇文章的基础上,继续讨论如何将ffmpeg解码出的视频帧进行渲染显示

公众号:断点实验室 音视频开发系列文章
ffmpeg源码编译环境搭建
ffplay源码编译
ffmpeg播放器实现详解 - 框架搭建
ffmpeg播放器实现详解 - 视频显示
ffmpeg播放器实现详解 - 音频播放
ffmpeg播放器实现详解 - 创建线程

1、视频帧渲染

上篇文章中介绍了如何基于ffmpeg搭建一个视频播放器框架,运行程序后可以看到,除了生成几张图片外,程序好像什么也做不了。

这是因为ffmpeg通过其封装的api及组件,为我们屏蔽了不同视频封装格式及编码格式的差异,以统一的api接口提供给开发者使用,开发者不需要了解每种编码方式及封装方式具体的技术细节,只需要调用ffmpeg提供的api就可以完成解封装和解码的操作了

至于视频帧的渲染及音频帧的播放,ffmpeg就无能为力了,因此需要借助类似sdl库等其他第三方组件来完成。

这里讲述如何使用sdl库完成视频帧的渲染,sdl在底层封装了opengl图形库,sdl提供的api简化了opengl的绘图操作,为开发者提供了很多便利的操作,当然,你也可以采用其他系统支持的图形库来绘制视频帧。

sdl库的编译安装详见[公众号:断点实验室]的前述文章 [ffmpeg播放器实现详解 - 框架搭建]。

1.1 渲染环境搭建

一个视频帧在显示前,需要准备一个用于显示视频的窗口对象,以及附着在窗口上的画布对象

创建SDL窗口,并指定图像尺寸

// 创建SDL窗口,并指定图像尺寸
screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);

创建画布对象

// 创建画布对象
bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height, SDL_YV12_OVERLAY, screen);

1.2 视频帧渲染

在窗口和画布对象创建完成后,就可以开始视频帧的渲染显示了。

在对画布对象操作前,需要对其加线程锁保护,避免其他线程对画布中的内容进行竞争性访问(后面的内容很快会涉及到多线程环境的开发)。对线程操作不熟悉的同学可以了解一下在多线程环境下,多个线程对临界区资源的竞争性访问与线程同步操作。

SDL_LockYUVOverlay(bmp);//locks the overlay for direct access to pixel data

向画布注入解码后的视频帧

sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pict.data, pict.linesize);

在画布对象的视频帧填充操作完成后,释放sdl线程锁。

//Unlocks a previously locked overlay. An overlay must be unlocked before it can be displayed
SDL_UnlockYUVOverlay(bmp);

对视频帧的渲染

SDL_DisplayYUVOverlay(bmp, &rect);//图像渲染

可以看到,由于借助了sdl封装的api绘图接口,视频帧的渲染还是非常容易的,如果直接采用opengl绘图,绘制过程会相对复杂些,例程主要的目的是为了介绍ffmpeg的使用,因此,这里采用sdl简化了渲染流程。

1.3 项目源码编译

本例程和上篇文章中用到的编译方法完全一样

tutorial02: tutorial02.c
    gcc -o tutorial02 -g3 tutorial02.c -I${FFMPEG_INCLUDE} -I${SDL_INCLUDE}  \
    -L${FFMPEG_LIB} -lavutil -lavformat -lavcodec -lswscale -lswresample -lz -lm \
    `sdl-config --cflags --libs`

clean:
    rm -rf tutorial02

执行make命令开始编译,编译完成后,可在源码目录生成名为[tutorial02]的可执行文件。

可通过ldd命令查询当前可执行文件所有依赖的动态库。

1.4 验证

执行[tutorial02 url]命令,可以看到有画面输出了。

./tutorial02 rtmp://58.200.131.2:1935/livetv/hunantv
s1.png

虽然画面已经有了,但还缺少声音,下篇文章会继续完善我们的播放器开发,讨论如何播放声音。

2、视频播放中可能出现的问题

视频播放中可能会出现以下两个问题

sdl找不到音频设备 SDL_OpenAudio no such audio device

sdl无法初始化 Could not initialize SDL, no available video device

解决方法见[公众号:断点实验室]的前述文章 [ffplay源码编译]。

3、源码清单

源码非常的简单,仅在上篇的内容基础上,增加了sdl渲染环境的搭建,整个源码仍然运行在main的主线程中,后面的内容会涉及多个线程的调度及同步的场景。

// tutorial02.c
// A pedagogical video player that will stream through every video frame as fast as it can.
//
// This tutorial was written by Stephen Dranger (dranger@gmail.com).
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
//
// Updates tested on:
// Mac OS X 10.11.6
// Apple LLVM version 8.0.0 (clang-800.0.38)
//
// Use 
//
// $ gcc -o tutorial02 tutorial02.c -lavutil -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
//
// to build (assuming libavutil/libavformat/libavcodec/libswscale are correctly installed your system).
//
// Run using
//
// $ tutorial02 myvideofile.mpg
//
// to play the video stream on your screen.


#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main // Prevents SDL from overriding main().
#endif

#include <stdio.h>

int main(int argc, char *argv[]) {
/*--------------参数定义-------------*/
    AVFormatContext *pFormatCtx = NULL;//保存文件容器封装信息及码流参数的结构体
    AVCodecContext *pCodecCtx = NULL;//解码器上下文对象,解码器依赖的相关环境、状态、资源以及参数集的接口指针
    AVCodec *pCodec = NULL;//保存编解码器信息的结构体,提供编码与解码的公共接口,可以看作是编码器与解码器的一个全局变量
    AVPacket packet;//负责保存压缩编码数据相关信息的结构体,每帧图像由一到多个packet包组成
    AVFrame *pFrame = NULL;//保存音视频解码后的数据,如状态信息、编解码器信息、宏块类型表,QP表,运动矢量表等数据
    struct SwsContext *sws_ctx = NULL;//描述转换器参数的结构体
    AVDictionary *optionsDict = NULL;
 
    SDL_Surface *screen = NULL;//SDL绘图窗口,A structure that contains a collection of pixels used in software blitting
    SDL_Overlay *bmp = NULL;//SDL画布
    SDL_Rect rect;//SDL矩形对象
    SDL_Event event;//SDL事件对象

    int i, videoStream;//循环变量,视频流类型标号
    int frameFinished;//解码操作是否成功标识
 
/*-------------参数初始化------------*/
    if (argc<2) {//检查输入参数个数是否正确
        fprintf(stderr, "Usage: test <file>\n");
        exit(1);
    }
    // Register all available formats and codecs,注册所有ffmpeg支持的多媒体格式及编解码器
    av_register_all();
  
    /*-----------------------
     * Open video file,打开视频文件,读文件头内容,取得文件容器的封装信息及码流参数并存储在pFormatCtx中
     * read the file header and stores information about the file format in the AVFormatContext structure  
     * The last three arguments are used to specify the file format, buffer size, and format options
     * but by setting this to NULL or 0, libavformat will auto-detect these
     -----------------------*/
    if (avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0) {
        return -1; // Couldn't open file.
    }
  
    /*-----------------------
     * 取得文件中保存的码流信息,并填充到pFormatCtx->stream 字段
     * check out & Retrieve the stream information in the file
     * then populate pFormatCtx->stream with the proper information 
     * pFormatCtx->streams is just an array of pointers, of size pFormatCtx->nb_streams
     -----------------------*/
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        return -1; // Couldn't find stream information.
    }
  
    // Dump information about file onto standard error,打印pFormatCtx中的码流信息
    av_dump_format(pFormatCtx, 0, argv[1], 0);
  
    // Find the first video stream.
    videoStream = -1;//视频流类型标号初始化为-1
    for(i = 0; i < pFormatCtx->nb_streams; i++) {//遍历文件中包含的所有流媒体类型(视频流、音频流、字幕流等)
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {//若文件中包含有视频流
            videoStream = i;//用视频流类型的标号修改标识,使之不为-1
            break;
        }
    }
    if (videoStream == -1) {//检查文件中是否存在视频流
        return -1; // Didn't find a video stream.
    }
  
    // Get a pointer to the codec context for the video stream,根据流类型标号从pFormatCtx->streams中取得视频流对应的解码器上下文
    pCodecCtx = pFormatCtx->streams[videoStream]->codec;

    /*-----------------------
     * Find the decoder for the video stream,根据视频流对应的解码器上下文查找对应的解码器,返回对应的解码器(信息结构体)
     * The stream's information about the codec is in what we call the "codec context.
     * This contains all the information about the codec that the stream is using
     -----------------------*/
    pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (pCodec == NULL) {//检查解码器是否匹配
        fprintf(stderr, "Unsupported codec!\n");
        return -1; // Codec not found.
    }

    // Open codec,打开解码器
    if (avcodec_open2(pCodecCtx, pCodec, &optionsDict) < 0) {
        return -1; // Could not open codec.
    }

    // Allocate video frame,为解码后的视频信息结构体分配空间并完成初始化操作(结构体中的图像缓存按照下面两步手动安装)
    pFrame = av_frame_alloc();

    // Initialize SWS context for software scaling,设置图像转换像素格式为AV_PIX_FMT_YUV420P
    sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, NULL);

    //SDL_Init initialize the Event Handling, File I/O, and Threading subsystems,初始化SDL 
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {//initialize the video audio & timer subsystem 
        fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());//tell the library what features we're going to use
        exit(1);
    }

    // Make a screen to put our video,在SDL2.0中SDL_SetVideoMode及SDL_Overlay已经弃用,改为SDL_CreateWindow及SDL_CreateRenderer创建窗口及着色器
#ifndef __DARWIN__
    screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);//创建SDL窗口,并指定图像尺寸
#else
    screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);//创建SDL窗口,并指定图像尺寸
#endif
    if (!screen) {//检查SDL窗口是否创建成功
        fprintf(stderr, "SDL: could not set video mode - exiting\n");
        exit(1);
    }
    SDL_WM_SetCaption(argv[1],0);//用输入文件名设置SDL窗口标题

    // Allocate a place to put our YUV image on that screen,创建画布对象
    bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height, SDL_YV12_OVERLAY, screen);

/*--------------循环解码-------------*/
    i = 0;// Read frames and save first five frames to disk
    /*-----------------------
     * read in a packet and store it in the AVPacket struct
     * ffmpeg allocates the internal data for us,which is pointed to by packet.data
     * this is freed by the av_free_packet()
     -----------------------*/
    while(av_read_frame(pFormatCtx, &packet) >= 0) {//从文件中依次读取每个图像编码数据包,并存储在AVPacket数据结构中
        // Is this a packet from the video stream,检查数据包类型
        if (packet.stream_index == videoStream) {
            /*-----------------------
             * Decode video frame,解码完整的一帧数据,并将frameFinished设置为true
             * 可能无法通过只解码一个packet就获得一个完整的视频帧frame,可能需要读取多个packet才行
             * avcodec_decode_video2()会在解码到完整的一帧时设置frameFinished为真
             * Technically a packet can contain partial frames or other bits of data
             * ffmpeg's parser ensures that the packets we get contain either complete or multiple frames
             * convert the packet to a frame for us and set frameFinisned for us when we have the next frame
             -----------------------*/
            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);

            // Did we get a video frame,检查是否解码出完整一帧图像
            if (frameFinished) {
                SDL_LockYUVOverlay(bmp);//locks the overlay for direct access to pixel data,原子操作,保护像素缓冲区临界资源

                AVFrame pict;//保存转换为AV_PIX_FMT_YUV420P格式的视频帧
                pict.data[0] = bmp->pixels[0];//将转码后的图像与画布的像素缓冲器关联
                pict.data[1] = bmp->pixels[2];
                pict.data[2] = bmp->pixels[1];

                pict.linesize[0] = bmp->pitches[0];//将转码后的图像扫描行长度与画布像素缓冲区的扫描行长度相关联
                pict.linesize[1] = bmp->pitches[2];//linesize-Size, in bytes, of the data for each picture/channel plane
                pict.linesize[2] = bmp->pitches[1];//For audio, only linesize[0] may be set

                // Convert the image into YUV format that SDL uses,将解码后的图像转换为AV_PIX_FMT_YUV420P格式,并赋值到pict对象
                sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pict.data, pict.linesize);

                SDL_UnlockYUVOverlay(bmp);//Unlocks a previously locked overlay. An overlay must be unlocked before it can be displayed

                //设置矩形显示区域
                rect.x = 0;
                rect.y = 0;
                rect.w = pCodecCtx->width;
                rect.h = pCodecCtx->height;
                SDL_DisplayYUVOverlay(bmp, &rect);//图像渲染
            }
        }
    
        // Free the packet that was allocated by av_read_frame,释放AVPacket数据结构中编码数据指针
        av_packet_unref(&packet);
        
        /*-------------------------
         * 在每次循环中从SDL后台队列取事件并填充到SDL_Event对象中
         * SDL的事件系统使得你可以接收用户的输入,从而完成一些控制操作
         * SDL_PollEvent() is the favored way of receiving system events 
         * since it can be done from the main loop and does not suspend the main loop
         * while waiting on an event to be posted
         * poll for events right after we finish processing a packet
         ------------------------*/
        SDL_PollEvent(&event);
        switch (event.type) {//检查SDL事件对象
            case SDL_QUIT://退出事件
                printf("SDL_QUIT\n");
                SDL_Quit();//退出操作
                exit(0);//结束进程
                break;
            default:
                break;
        }//end for switch
    }//end for while
/*--------------参数撤销-------------*/  
    // Free the YUV frame.
    av_free(pFrame);

    // Close the codec.
    avcodec_close(pCodecCtx);

    // Close the video file.
    avformat_close_input(&pFormatCtx);

    return 0;
}


// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
// 公众号:断点实验室
// 扫描二维码,关注更多优质原创,内容包括:音视频开发、图像处理、网络、
// Linux,Windows、Android、嵌入式开发等

qr.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容