环境:ffmpeg version3.2.2 32bit shared,Windows

使用Windows GDI进行视频画面的渲染

  1. 在视频解码线程中进行视频的解码, 得到一帧一帧的图像数据,当前格式为YUV
  2. 将原始YUV格式的帧数据转化为RGB24,用于显示,这一过程在CPU中完成
bool    VideoReader::readFrame(FrameInfor& infor) {
    AVPacket packet;
    av_init_packet(&packet);
    for (;;)
    {
        if (av_read_frame(_formatCtx, &packet))
        {
            av_free_packet(&packet);
            return false;
        }
        if (packet.stream_index != _videoIndex)
        {
            continue;
        }
        int frame_finished = 0;

        int res = avcodec_decode_video2(_codecCtx, _frame, &frame_finished, &packet);

        if (frame_finished)
        {
            AVStream* streams = _formatCtx->streams[_videoIndex];
            double      tmbase = av_q2d(streams->time_base);
            int64_t     pts = _frame->pts;
            
            int res = sws_scale(
                _convertCtx
                , (const uint8_t* const*)_frame->data
                , _frame->linesize
                , 0
                , _codecCtx->height
                , _frameRGB->data
                , _frameRGB->linesize
            );
            av_packet_unref(&packet);

            infor._data = _frameRGB->data[0];
            infor._dataSize = _imageSize;
            infor._width = _screenW;
            infor._height = _screenH;
            infor._pts = _frame->pts;
            infor._timeBase = av_q2d(streams->time_base);

            return  true;
        }
    }
    return  false;
}
  1. RGB转化为BGR格式,填充位图:转化格式的原因是Windows GDI的默认像素格式BGR。
 BYTE* data = (BYTE*)infor._data;
// Windows GDI的默认像素格式BGR RGB=>BGR
for (int i = 0; i < infor._dataSize; i += 3)
{
    // _imageBuf为指向位图实际内存的指针
    _imageBuf[i + 0] = data[i + 2];
    _imageBuf[i + 1] = data[i + 1];
    _imageBuf[i + 2] = data[i + 0];
}
  1. 触发窗口重绘
// 触发窗口重绘
InvalidateRect(_hWnd,
    0,  // 无效区域(NULL = 整个窗口
    0); // 是否擦除背景
  1. 主窗口的消息回调函数中处理窗口重绘消息:将在子线程中填充好的离屏位图一次性复制到窗口上显示
case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC         hdc;
    hdc = BeginPaint(hWnd, &ps);
    // 将离屏位图一次性复制到窗口上显示
    if (g_decode._hMem)
    {
        BITMAPINFO  bmpInfor;
        GetObject(g_decode._hBmp, sizeof(bmpInfor), &bmpInfor);
        BitBlt(hdc, 0, 0, bmpInfor.bmiHeader.biWidth, bmpInfor.bmiHeader.biHeight, g_decode._hMem, 0, 0, SRCCOPY);
    }
    EndPaint(hWnd, &ps);
}
break;

播放速率的控制

  1. 由于解码的时间很快,因此按照视频帧的显示时间戳进行播放,而不是解码的时间。解码快了就进行休眠
bool    VideoDecodeThread::run()
{
    _timestamp.update();
    while (!_exitFlag)
    {
        FrameInfor  infor;
        if (!_ffReader.readFrame(infor))
        {
            break;
        }

        BYTE* data = (BYTE*)infor._data;
        // Windows GDI的默认像素格式BGR RGB=>BGR
        for (int i = 0; i < infor._dataSize; i += 3)
        {
            _imageBuf[i + 0] = data[i + 2];
            _imageBuf[i + 1] = data[i + 1];
            _imageBuf[i + 2] = data[i + 0];
        }
        // 触发窗口重绘
        InvalidateRect(_hWnd,
            0,  // 无效区域(NULL = 整个窗口
            0); // 是否擦除背景

        // 播放速率控制

        // 帧的预定显示时间(ms)
        double      tims = infor._pts * infor._timeBase * 1000;
        // 实际经过的时间(ms)
        double      elsped = _timestamp.getElapsedTimeInMilliSec();
        double      sleeps = (tims - elsped);
        if (sleeps > 1)
        {
            Sleep((DWORD)sleeps);
        }
    }

    return  true;
}

OpenGL可编程渲染管线进行视频画面的渲染

  1. 在视频解码线程中进行视频的解码, 得到一帧一帧的原始图像数据
  2. 创建一个单通道纹理存储 Y、U、V,根据YUV坐标的不同进行采样
    image
  3. 通过Windows的消息传递机制将原始图像数据传递到主线程,作为GPU纹理输入

OpenGL可编程渲染管线进行视频画面的渲染优化

  1. RGB转换放到GPU中处理:fragment shader中进行YUV转RGB
  2. 利用双PBO进行异步上传纹理数据
帧1:PBO0写入数据 → PBO1上传到纹理
帧2:PBO1写入数据 → PBO0上传到纹理  
帧3:PBO0写入数据 → PBO1上传到纹理

参考

https://gitee.com/excelcoder/open-gl-ffmpeg-player-win32