音视频开发10. 使用ffmpeg 流媒体视频流截图jpg实践

一、准备环境

  • CentOS 安装ffmpeg开发库
  • 可播放的视频流媒体地址
  • 安装好C++编译环境,cmake/g++等

二、准备知识

1. RGB

使用红绿蓝来表示颜色,较为常见,这里不作太多解释。

2. YUV

YUV也是一种颜色编码方法,主要用在电视系统和模拟视频领域,3个分量分表表示:

  • Y:明亮度(Luminance或Luma),也就是灰度;
  • U,V: 表示色度(Chrominance或Chroma),描述影像色彩及饱和度,用于指定像素的颜色。

YUV把亮度与色彩信息分离,如果没有UV信息图像就是黑白的,最初便于解决彩色电视机与黑白电视的兼容问题。

YUV格式占用的空间比RGB少,比如:

  • RGB24一帧的大小=宽×高×3 Byte
  • RGB32一帧的大小=宽×高×4 Byte
  • YUV420一帧的大小=宽×高×1.5 Byte

3. FFmpeg解码时的视频格式

FFmpeg视频解码后帧格式一般是:AV_PIX_FMT_YUV420P,数据结构是AVFrame,其中的data[]数组存放YUV数据:

  • data[0]——-Y分量
  • data[1]——-U分量
  • data[2]——-V分量
    在linesize[]数组中保存对应通道的数据宽度 :
  • linesize[0]——-Y分量的宽度
  • linesize[1]——-U分量的宽度
  • linesize[2]——-V分量的宽度

4. YUV转RGB代码示例

通过下面示例可以更清楚了解AVFrame中存放的YUV数据格式。

uint8_t *AVFrame2Img(AVFrame *pFrame) {
    int frameHeight = pFrame->height;
    int frameWidth = pFrame->width;
    int channels = 3;

    //反转图像
    pFrame->data[0] += pFrame->linesize[0] * (frameHeight - 1);
    pFrame->linesize[0] *= -1;
    pFrame->data[1] += pFrame->linesize[1] * (frameHeight / 2 - 1);
    pFrame->linesize[1] *= -1;
    pFrame->data[2] += pFrame->linesize[2] * (frameHeight / 2 - 1);
    pFrame->linesize[2] *= -1;

    //创建保存yuv数据的buffer
    uint8_t *pDecodedBuffer = (uint8_t *) malloc(
            frameHeight * frameWidth * sizeof(uint8_t) * channels);

    //从AVFrame中获取yuv420p数据,并保存到buffer
    int i, j, k;
    //拷贝y分量
    for (i = 0; i < frameHeight; i++) {
        memcpy(pDecodedBuffer + frameWidth * i,
               pFrame->data[0] + pFrame->linesize[0] * i,
               frameWidth);
    }
    //拷贝u分量
    for (j = 0; j < frameHeight / 2; j++) {
        memcpy(pDecodedBuffer + frameWidth * i + frameWidth / 2 * j,
               pFrame->data[1] + pFrame->linesize[1] * j,
               frameWidth / 2);
    }
    //拷贝v分量
    for (k = 0; k < frameHeight / 2; k++) {
        memcpy(pDecodedBuffer + frameWidth * i + frameWidth / 2 * j + frameWidth / 2 * k,
               pFrame->data[2] + pFrame->linesize[2] * k,
               frameWidth / 2);
    } 
    return pDecodedBuffer;
}

使用ffmpeg的 sws_scale 可以实现格式转换:

sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);

三、流程

1. 整体流程

  1. 打开输入流
  2. 找到视频流信息
  3. 创建解码器
  4. 创建图像转换上下文 SwsContext img_convert_ctx
  5. 分配 AVPacket
  6. 读取流
  7. 发送到解码器
  8. 读取解码结果
  9. 图片类型YUV转换为RGB
  10. 调用保存jpeg函数
  11. 释放资源

2. 保存图片过程

  1. 创建 AVFormatContext上下文
  2. 构建 AVFrame
  3. 创建编码器
  4. 复制编码器参数
  5. 写入jpeg头
  6. 创建 AVPacket
  7. 解码
  8. 得到编码数据
  9. 写入一帧
  10. 释放资源

四、实现

1. CMakeLists.txt

cmake_minimum_required(VERSION 3.17)
project(ffmpeg_demo)

# 设置ffmpeg依赖库及头文件所在目录,并存进指定变量
set(ffmpeg_libs_DIR /home/xundh/ffmpeg_sources/ffmpeg-4.2.2)
set(ffmpeg_headers_DIR /home/xundh/ffmpeg_sources/ffmpeg-4.2.2)

#对于find_package找不到的外部依赖库,可以用add_library添加
# SHARED表示添加的是动态库
# IMPORTED表示是引入已经存在的动态库

add_library( avcodec SHARED IMPORTED)
add_library( avfilter SHARED IMPORTED )
add_library( swresample SHARED IMPORTED )
add_library( swscale SHARED IMPORTED )
add_library( avformat SHARED IMPORTED )
add_library( avutil SHARED IMPORTED )


#指定所添加依赖库的导入路径
set_target_properties( avcodec PROPERTIES IMPORTED_LOCATION ${ffmpeg_libs_DIR}/libavcodec/libavcodec.so )
set_target_properties( avfilter PROPERTIES IMPORTED_LOCATION ${ffmpeg_libs_DIR}/libavfilter/libavfilter.so )
set_target_properties( swresample PROPERTIES IMPORTED_LOCATION ${ffmpeg_libs_DIR}/libswresample/libswresample.so )
set_target_properties( swscale PROPERTIES IMPORTED_LOCATION ${ffmpeg_libs_DIR}/libswscale/libswscale.so )
set_target_properties( avformat PROPERTIES IMPORTED_LOCATION ${ffmpeg_libs_DIR}/libavformat/libavformat.so )
set_target_properties( avutil PROPERTIES IMPORTED_LOCATION ${ffmpeg_libs_DIR}/libavutil/libavutil.so )


# 添加头文件路径到编译器的头文件搜索路径下,多个路径以空格分隔
include_directories( ${ffmpeg_headers_DIR} )
link_directories(${ffmpeg_libs_DIR} )
link_directories(/usr/lib)


set(CMAKE_CXX_STANDARD 14)
add_executable(ffmpeg_demo main.cpp)
target_link_libraries(${PROJECT_NAME}  avcodec avformat avutil swresample swscale swscale avfilter )

2. main.cpp

#include <stdio.h>
#include <iostream>
#ifdef __cplusplus
extern "C"
{
#endif
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libavutil/log.h"
#include "libswscale/swscale.h"
#ifdef __cplusplus
}
#endif
using namespace std;

#define CAPTURE_COUNT 1
/**
 *
 * ffmpeg -i "rtsp://admin123:tang3shan@112.31.211.240:554/cam/realmonitor?channel=7&subtype=1" -y -f image2 -ss 00:00:03 -vframes 1 -s 640x360 1.jpg
 */
// 回调函数的参数,时间和有无流的判断
typedef struct
{
    time_t lasttime;
    bool connected;
} Runner;

// 回调函数
int interrupt_callback(void *p)
{
    Runner *r = (Runner *)p;
    if (r->lasttime > 0)
    {
        if (time(NULL) - r->lasttime > 10 && !r->connected)
        {
            // 等待超过1s则中断
            return 1;
        }
    }
    return 0;
}

/**
 * 写入YUV的灰度图片
 */
void save_gray_frame(unsigned char *buf, int wrap, int xsize, int ysize, char *filename)
{
    FILE *grayFile;
    int i;
    grayFile = fopen(filename, "w");
    // 写入一个pgm文件最小头部,便携灰度图格式: https://en.wikipedia.org/wiki/Netpbm_format#PGM_example
    fprintf(grayFile, "P5\n%d %d\n%d\n", xsize, ysize, 255);

    // 逐行写入
    for (i = 0; i < ysize; i++)
        fwrite(buf + i * wrap, 1, xsize, grayFile);
    // 关闭文件
    fclose(grayFile);
}
/**
 * 将AVFrame(YUV420格式)保存为JPEG格式的图片
*/
int savePicture(AVFrame *pFrame, char *out_name)
{
    //编码保存图片
    int width = pFrame->width;
    int height = pFrame->height;
    AVCodecContext *pAVCodecContext = NULL;

    AVFormatContext *pAVFormatContext = avformat_alloc_context();
    // 设置输出文件格式
    pAVFormatContext->oformat = av_guess_format("mjpeg", NULL, NULL);
    cout << "打开文件" << out_name << endl;
    // 创建并初始化输出 AVIOContext
    int ret1 = avio_open(&pAVFormatContext->pb, out_name, AVIO_FLAG_READ_WRITE);
    if (ret1 < 0)
    {
        cout << "打开输出文件失败, errorCode" << ret1 << endl;
        return -1;
    }

    // 构建一个新stream
    AVStream *pAVStream = avformat_new_stream(pAVFormatContext, 0);
    if (pAVStream == NULL)
    {
        return -1;
    }

    AVCodecParameters *parameters = pAVStream->codecpar;
    parameters->codec_id = pAVFormatContext->oformat->video_codec;
    parameters->codec_type = AVMEDIA_TYPE_VIDEO;
    parameters->format = AV_PIX_FMT_YUVJ420P;
    parameters->width = pFrame->width;
    parameters->height = pFrame->height;

    AVCodec *pCodec = avcodec_find_encoder(pAVStream->codecpar->codec_id);

    if (!pCodec)
    {
        cout << "找不到jpeg编码器" << endl;
        return -1;
    }

    pAVCodecContext = avcodec_alloc_context3(pCodec);
    if (!pAVCodecContext)
    {
        cout << "获取编码器上下文失败" << endl;
        exit(1);
    }

    if ((avcodec_parameters_to_context(pAVCodecContext, pAVStream->codecpar)) < 0)
    {
        cout << "复制编码器参数发生错误" << endl;
        return -1;
    }

    pAVCodecContext->time_base = (AVRational){1, 25};

    if (avcodec_open2(pAVCodecContext, pCodec, NULL) < 0)
    {
        cout << "打开编码器上下文失败" << endl;
        return -1;
    }

    int ret = avformat_write_header(pAVFormatContext, NULL);
    if (ret < 0)
    {
        cout << "写入图片头失败" << endl;
        return -1;
    }

    int y_size = width * height;

    // Encode
    //  给AVPacket分配足够大的空间
    AVPacket pkt;
    av_new_packet(&pkt, y_size * 3);

    // 解码数据
    ret = avcodec_send_frame(pAVCodecContext, pFrame);
    if (ret < 0)
    {
        cout << "解码失败" << endl;
        return -1;
    }

    // 得到编码后数据
    ret = avcodec_receive_packet(pAVCodecContext, &pkt);
    if (ret < 0)
    {
        cout << "编码失败" << endl;
        return -1;
    }

    ret = av_write_frame(pAVFormatContext, &pkt);

    if (ret < 0)
    {
        cout << "写入帧失败" << endl;
        return -1;
    }

    av_packet_unref(&pkt);

    // 写入索引
    av_write_trailer(pAVFormatContext);

    // 释放资源
    avcodec_close(pAVCodecContext);
    avio_close(pAVFormatContext->pb);
    avformat_free_context(pAVFormatContext);

    return 0;
}

int main(int argc, char *argv[])
{
    AVFormatContext *pAVFormatContext;
    AVCodecContext *pAVCodecContext;
    AVCodec *pAVCodec;
    AVFrame *pAVFrame, *pAVFrameRGB;
    AVPacket *pAVPacket;

    uint8_t *out_buffer;

    static struct SwsContext *img_convert_ctx;

    int videoStream, i, numBytes;
    int ret, got_picture;
    // 注册ffmpeg
    av_register_all();
    avformat_network_init();

    // AVFormatContext 分配内存
    pAVFormatContext = avformat_alloc_context();

    /// 推流参数设置
    AVDictionary *avdic = NULL;
    char option_key[] = "rtsp_transport";
    char option_value[] = "tcp";
    av_dict_set(&avdic, option_key, option_value, 0);
    char option_key2[] = "max_delay";
    char option_value2[] = "100";

    av_dict_set(&avdic, option_key2, option_value2, 0);
    // 视频地址
    char url[] = "http://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear2/prog_index.m3u8";
    Runner input_runner = {0};
    pAVFormatContext->interrupt_callback.callback = interrupt_callback;
    pAVFormatContext->interrupt_callback.opaque = &input_runner;
    input_runner.lasttime = time(NULL);
    input_runner.connected = false;

    int avformat_ret = avformat_open_input(&pAVFormatContext, url, NULL, &avdic);
    if (avformat_ret != 0)
    {
        cout << "无法打开文件,返回值:" << avformat_ret << endl;
        return 1;
    }

    if (avformat_find_stream_info(pAVFormatContext, NULL) < 0)
    {
        cout << "无法打开文件流" << endl;
        return 1;
    }

    videoStream = -1;

    ///循环查找视频中包含的流信息,直到找到视频类型的流,保存到videoStream变量中
    for (i = 0; i < pAVFormatContext->nb_streams; i++)
    {
        if (pAVFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            videoStream = i;
        }
    }

    ///如果videoStream为-1 说明没有找到视频流
    if (videoStream == -1)
    {
        cout << "没有找到视频流" << endl;
        return -1;
    }

    cout << "查找解码器" << endl;;
    pAVCodecContext = pAVFormatContext->streams[videoStream]->codec;
    pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id);
    pAVCodecContext->bit_rate = 0;      //初始化为0
    pAVCodecContext->time_base.num = 1; //下面两行:一秒钟25帧
    pAVCodecContext->time_base.den = 10;
    pAVCodecContext->frame_number = 1; //每包一个视频帧

    if (pAVCodec == NULL)
    {
        cout << "查找解码器失败"<< endl;
        return 1;
    }
    cout << "打开解码器" << endl;;
    ///打开解码器
    if (avcodec_open2(pAVCodecContext, pAVCodec, NULL) < 0)
    {
        cout << "打开解码器失败" << endl;
        return 1;
    }

    pAVFrame = av_frame_alloc();
    pAVFrameRGB = av_frame_alloc();

    ///转换帧格式,这里将解码后的YUV数据通过转换成RGB32
    img_convert_ctx = sws_getContext(pAVCodecContext->width, pAVCodecContext->height,
                                    pAVCodecContext->pix_fmt, pAVCodecContext->width, pAVCodecContext->height,
                                    AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL);

    numBytes = avpicture_get_size(AV_PIX_FMT_RGB32, pAVCodecContext->width, pAVCodecContext->height);

    out_buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
    avpicture_fill((AVPicture *)pAVFrameRGB, out_buffer, AV_PIX_FMT_RGB32,
                   pAVCodecContext->width, pAVCodecContext->height);

    int y_size = pAVCodecContext->width * pAVCodecContext->height;

    pAVPacket = (AVPacket *)malloc(sizeof(AVPacket)); //分配一个packet
    cout<< "打印流信息" << endl;
    av_dump_format(pAVFormatContext, 0, url, 0);

    av_new_packet(pAVPacket, y_size); //分配packet的数据
    i = 0;
    const char *in_filename;
    char buf[1024];
    int frame_count = 0;
    while (av_read_frame(pAVFormatContext, pAVPacket) >= 0)
    {

        if (pAVPacket->stream_index == videoStream)
        {
            ret = avcodec_send_packet(pAVCodecContext, pAVPacket);
            if (ret < 0)
            {
                av_packet_unref(pAVPacket);
                continue;
            }

            do
            {
                // 读取帧
                ret = avcodec_receive_frame(pAVCodecContext, pAVFrame);
                if (ret < 0)
                    break;
                else if (ret == 0)
                { 
                    /* Got a frame successfully */
                    sws_scale(img_convert_ctx, pAVFrame->data, pAVFrame->linesize, 0, pAVCodecContext->height, pAVFrameRGB->data, pAVFrameRGB->linesize);

                    // 注释的这一段程序用来存储YUV的 Y 通道灰度图片
                    // char frame_filename[1024];
                    // snprintf(frame_filename, sizeof(frame_filename), "%s-%d.pgm", "frame", pCodecCtx->frame_number);
                    // save_gray_frame(pFrame->data[0], pFrame->linesize[0], pFrame->width, pFrame->height, frame_filename);
                    snprintf(buf, sizeof(buf), "picture-%d.jpg", frame_count);
                    savePicture(pAVFrame, buf);
                    frame_count++;
                }
                else if (ret == AVERROR_EOF)
                {
                    avcodec_flush_buffers(pAVCodecContext);
                    break;
                }
            } while (ret != AVERROR(EAGAIN));

            av_packet_unref(pAVPacket);

            // 截取几张
            if (frame_count >= CAPTURE_COUNT)
                break;
        }
        else
        {
            av_packet_unref(pAVPacket);
            continue;
        }
        av_free_packet(pAVPacket); //释放资源,否则内存会一直上升
    }
    av_free(out_buffer);
    av_free(pAVFrameRGB);
    avcodec_close(pAVCodecContext);
    avformat_close_input(&pAVFormatContext);

    return 0;
}

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程圈子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值