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

Posted 编程圈子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了音视频开发10. 使用ffmpeg 流媒体视频流截图jpg实践相关的知识,希望对你有一定的参考价值。

音视频开发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以上是关于音视频开发10. 使用ffmpeg 流媒体视频流截图jpg实践的主要内容,如果未能解决你的问题,请参考以下文章

流媒体开发8ffmpeg命令视频拼接图片和视频转换

[音视频处理] FFmpeg使用指北1-视频解码

javaCV开发详解之9:基于gdigrab的windows屏幕画面抓取/采集(基于javacv的屏幕截屏录屏功能)

流媒体开发9ffmpeg实现视频录制

流媒体开发9ffmpeg实现视频录制

ffmpeg音视频开发: 使用ffprobe获取媒体信息