SDL and Video

显示图像的方法有很多,这里我们使用 SDL 。SDL 英文全称为 Simple Direct Layer 。是一个跨平台的多媒体库,也就是你可以在 windonws linuxe 和 mac 里面使用这个库。所谓的多媒体库,就是提供了图像显示,声音播放,和线程相关接口的库。在 linux 上安装这个库很简单,示例代码的 README 已经有写了。

SDL 提供了很多显示图片的方法,SDL显示图片是通过把数据显示在 YUV 层实现的。YUV 是显示原始数据的一种方法,类似于 RGB,通俗地说, Y 代表亮度系数, U 和 V 代表颜色系数。SDL的工作方式是把 YUV数据传进 YUV 层并显示。它接受4种 YUV 格式,但其中 YV12 是最快的。另一种格式叫 YUV420P 和 YV12 差不多,只是 U 和 V 调转了。420 的意思是每个样本的比例是 4:2:0 ,也就是每4个亮度分量共享一个颜色分量。这样可以节省带宽,而且人类对于颜色分量不太敏感,即使去掉几个也看不出差别。“P” 的意思是数据是 Y U V 在不同的数组中。ffmpeg 能够把图像转为 YUV420P, 其实很多视频已经使用这种格式存储图像,或者很容易就可以转为到这种格式。

现在如果想要显示图像,我们要把之前的代码中的 SaveFrame 函数替换掉。但是首先,我们先要知道怎么用 SDL 。

首先需要把库包含进来并初始化 SDL:

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

if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
  fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
  exit(1);
}

SDL_Init 告诉库我们先要用什么功能。SDL_GetError 显然是用来处理 debug 信息的函数。

##Creating a Display

现在我们需要一块屏幕来显示东西。SDL 中显示图像的区域叫做 surface:

:::c
SDL_Surface *screen;

screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
if(!screen) {
  fprintf(stderr, "SDL: could not set video mode - exiting\n");
  exit(1);
}

这里给定了屏幕的宽和高。下一个参数是屏幕位数,0的意思是使用现在的显示值。

这样我们就创建了 YUV 层了,接着就可以把视频放进去了。

:::c
SDL_Overlay     *bmp;

bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height,
                           SDL_YV12_OVERLAY, screen);

就像之前说的,我们使用 YV12 来显示图像。

##Displaying the Image

是不是很简单?接着我们只需要显示图片了。一直去到得到 frame的地方。 我们可以把之前的代码都删掉,换为显示图像的代码。要显示图像,我们需要一个 AVPicture 结构体,然后设置数据指针和 linesize 到我们的 YUV 层。

:::c
if(frameFinished) {
    SDL_LockYUVOverlay(bmp);

    AVPicture pict;
    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];
    pict.linesize[2] = bmp->pitches[1];

    // Convert the image into YUV format that SDL uses
    img_convert(&pict, PIX_FMT_YUV420P,
                    (AVPicture *)pFrame, pCodecCtx->pix_fmt, 
            pCodecCtx->width, pCodecCtx->height);
    
    SDL_UnlockYUVOverlay(bmp);
  }    

首先我们需要把显示层锁上,因为我们要往里面写数据。这样做可以避免后面可能出现的问题。AVPicture 结构体,就像之前看到的,有4个指向数据的指针。由于我们处理 YUV420P 格式,所以只用到其中三个通道,因此只需其中3套数据。其他格式可能会用到第四个数据指针来存储透明值或者其他东西。linesize 就像它的名字那样。对于 YUV 层有 pitches与 linesize 对应。所以我们只要把指针指向 pitches,然后当我们往 pict 写数据时,实际上是往 overlay层写数据。

##Drawing the Image

虽然图像数据已经在 Overlay 层了,但是我们还是需要告诉 SDL 显示数据。我们还传一个矩型到函数里面告诉 SDL 图像应该显示的地方和缩放比例。在这里,SDL 帮助我们做了缩放,以获得较高的处理速度。

:::c
SDL_Rect rect;

  if(frameFinished) {
    /* ... code ... */
    // Convert the image into YUV format that SDL uses
    img_convert(&pict, PIX_FMT_YUV420P,
                    (AVPicture *)pFrame, pCodecCtx->pix_fmt, 
            pCodecCtx->width, pCodecCtx->height);
    
    SDL_UnlockYUVOverlay(bmp);
    rect.x = 0;
    rect.y = 0;
    rect.w = pCodecCtx->width;
    rect.h = pCodecCtx->height;
    SDL_DisplayYUVOverlay(bmp, &rect);
  }

这样视频就开始显示了。

下面说一下 SDL 另一个特性,它的事件处理系统。当你在 SDL 程序中打字,移动鼠标或者发送信号,它都会产生一个事件。你的程序可以截获这些事件和处理先要处理的事件。你的程序也可以向 SDL 事件系统发送事件。这个在多线程程序中很有用。这个会在教程4中有所体现。现在我们只用它来处理程序的退出事件。

:::c
SDL_Event       event;

    av_free_packet(&packet);
    SDL_PollEvent(&event);
    switch(event.type) {
    case SDL_QUIT:
      SDL_Quit();
      exit(0);
      break;
    default:
      break;
    }

暂时到这里,后面的更精彩。

源代码可以在 https://github.com/gavinlin/ffmpeg_tutorial 代码会有些出入,因为源码中使用了新版本的 ffmpeg 。