当前位置: 首页 > 业余研究, 我的研究, 流媒体学习 > 正文

FFMPEG基于内存的转码实例——输入输出视频均在内存

我在6月份写了篇文章《FFMPEG基于内存的转码实例》,讲如何把视频转码后放到内存,然后通过网络发送出去。但该文章只完成了一半,即输入的数据依然是从磁盘文件中读取。在实际应用中,有很多数据是放到内存的,比如播放从服务器接收到的视频,就是在内存中的。时隔2个月,项目终于完成了,虽然在收尾阶段会花费大量时间,但也算空闲了点。于是就继续完善。

本文中,假定的使用场合是,有一个已经放到内存的视频,需要将它转码成另一种封装格式,还是放到内存中。由于是测试,首先将视频从文件中读取到内存,最后会将转换好的视频写入到另一个文件以检查是否正常。当然,限于能力,代码不可能适合于所有情况,但却可以清楚演示出自定义的IO输入输出的用法。

技术要点简述如下:

1、用户自定义的操作

对于内存的操作使用结构体封装:

typedef struct AVIOBufferContext {
    unsigned char* ptr;
    int pos;
    int totalSize;
    int realSize;
}AVIOBufferContext;

输入、输出均使用该结构体:

AVIOBufferContext g_avbuffer_in;
AVIOBufferContext g_avbuffer_out;

实现,read、write、seek函数。其中read为读取时使用到的,其它2个是写入内存要使用的。以read为例:

static int my_read(void *opaque, unsigned char *buf, int size)
{
    AVIOBufferContext* op = (AVIOBufferContext*)opaque;
    int len = size;
    if (op->pos + size > op->totalSize)
    {
        len = op->totalSize - op->pos;
    }
    memcpy(buf, op->ptr + op->pos, len);
    if (op->pos + len >= op->realSize)
    op->realSize += len;
    
    op->pos += len;

    return len;
}

实质进行的是读取已有内存的size数据,拷贝到buf中。opaque方便参数传递。注意,在拷贝时要对pos累加。

其它函数类似。

2、输出配置关键代码:

    avio_out =avio_alloc_context((unsigned char *)g_ptr_out, IO_BUFFER_SIZE, 1,
                &g_avbuffer_out, NULL, my_write, my_seek); 

    avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);
    if (!ofmt_ctx)
    {
        printf( "Could not create output context\n");
        ret = AVERROR_UNKNOWN;
        goto end;
    }
    ofmt_ctx->pb=avio_out; // 赋值自定义的IO结构体
    ofmt_ctx->flags=AVFMT_FLAG_CUSTOM_IO; // 指定为自定义

这个跟上述提到的文章是一致的。只是多了个自定义的结构体。

3、输入配置关键代码:

    avio_in =avio_alloc_context((unsigned char *)g_ptr_in, IO_BUFFER_SIZE, 0,
                &g_avbuffer_in, my_read, NULL, NULL); 
    if (!avio_in)
    {
        printf( "avio_alloc_context for input failed\n");
        ret = AVERROR_UNKNOWN;
        goto end;
    }
    // 分配输入的AVFormatContext
    ifmt_ctx=avformat_alloc_context();
    if (!ifmt_ctx)
    {
        printf( "Could not create output context\n");
        ret = AVERROR_UNKNOWN;
        goto end;
    }
    ifmt_ctx->pb=avio_in; // 赋值自定义的IO结构体
    ifmt_ctx->flags=AVFMT_FLAG_CUSTOM_IO; // 指定为自定义
    if ((ret = avformat_open_input(&ifmt_ctx, "wtf", NULL, NULL)) < 0)
    {
        printf("Cannot open input file\n");
        return ret;
    }
    if ((ret = avformat_find_stream_info(ifmt_ctx, NULL)) < 0)
    {
        printf("Cannot find stream information\n");
        return ret;
    }

对于avio_alloc_context的赋值和输出一样,只是没有了write和seek。对于输入所用的AVFormatContext变量,用avformat_alloc_context来分配。由于是读内存的数据,因此avformat_open_input就不用指定文件名了。

我在代码中尽量加了注释,下面是代码:

/**
他山之石,学习为主,版权所无,翻版不究,有错无责
                  Late Lee  2015.08
基于内存的格式封装测试(从内存视频转换到另一片内存视频)
使用
./a.out a.avi a.mkv

支持的:
avi mkv mp4 flv ts ...

参考:

http://blog.csdn.net/leixiaohua1020/article/details/25422685

log
新版本出现:
Using AVStream.codec.time_base as a timebase hint to the muxer is 
deprecated. Set AVStream.time_base instead.

test passed!!

mp4->avi failed
出现:
H.264 bitstream malformed, no startcode found, use the h264_mp4toannexb bitstream filter 
解决见:

http://blog.chinaunix.net/uid-11344913-id-4432752.html

官方解释:

https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb

ts -> avi passed

其它:

1、传递给ffmpeg的avio_alloc_context中的内存p和大小size,可以使用32768。
如果转换后的数据保存在内存p1,这个内存p1一定要和前面所说的p不同。因为
在自定义的write中的buf参数,就是p,所以要拷贝到其它内存。
如定义p为32768,但定义p1为50MB,可以转换50MB的视频
测试:
p为32768时,需调用write 1351次
2倍大小时,调用write 679次
p越大,调用次数最少,内存消耗越大
(用time测试,时间上没什么变化,前者为4.7s,后者为4.6s,但理论上内存大一点好)

2、优化:
   转换功能接口封装为类,把write、seek等和内存有关的操作放到类外部实现,
   再传递到该类中,该类没有内存管理更好一些。

todo
   重复读文件,如何做?
*/

#include 
#include 
#include 

#include 
#include 
#include 

extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
}

#include "file_utils.h"

#ifndef min
#define min(a,b) ((a) > (b) ? (b) : (a))
#endif

#define _LL_DEBUG_

// low level debug
#ifdef _LL_DEBUG_
    #define debug(fmt, ...) printf(fmt, ##__VA_ARGS__)
    #define LL_DEBUG(fmt, ...) printf("[DEBUG %s().%d @ %s]: " fmt, \
    __func__, __LINE__, P_SRC, ##__VA_ARGS__)
#else
    #define debug(fmt, ...)
    #define LL_DEBUG(fmt, ...)
#endif

#define DEFAULT_MEM (10*1024*1024)

//参考file协议的内存,使用大小32768,大一点也可以
#define IO_BUFFER_SIZE (32768*1)

typedef struct AVIOBufferContext {
    unsigned char* ptr;
    int pos;
    int totalSize;
    int realSize;
}AVIOBufferContext;

// note 这两个是用户视频数据,
// g_avbuffer_in为已经读取的视频
// g_avbuffer_out是ffmpeg转换后的视频,直接将该内存写入文件即可
AVIOBufferContext g_avbuffer_in;
AVIOBufferContext g_avbuffer_out;

// note这两个是FFMPEG内部使用的IO内存,与AVIOBufferContext的ptr不同
// 在测试时,发现直接定义为数组,会有错误,故使用malloc
static char *g_ptr_in = NULL;
static char *g_ptr_out = NULL;

// 每次read_frame时,就会调用到这个函数,该函数从g_avbuffer_in读数据
static int my_read(void *opaque, unsigned char *buf, int size)
{
    AVIOBufferContext* op = (AVIOBufferContext*)opaque;
    int len = size;
    if (op->pos + size > op->totalSize)
    {
        len = op->totalSize - op->pos;
    }
    memcpy(buf, op->ptr + op->pos, len);
    if (op->pos + len >= op->realSize)
    op->realSize += len;
    
    op->pos += len;

    return len;
}

static int my_write(void *opaque, unsigned char *buf, int size)
{
    AVIOBufferContext* op = (AVIOBufferContext*)opaque;
    if (op->pos + size > op->totalSize)
    {
        // 重新申请
        // 根据数值逐步加大
        int newTotalLen = op->totalSize*sizeof(char) * 3 / 2;
        unsigned char* ptr = (unsigned char*)av_realloc(op->ptr, newTotalLen);
        if (ptr == NULL)
        {
            // todo 是否在此处释放内存?
            return -1;
        }
        debug("org ptr: %p new ptr: %p size: %d(%0.fMB) ", op->ptr, ptr, 
                    newTotalLen, newTotalLen/1024.0/1024.0);
        op->totalSize = newTotalLen;
        op->ptr = ptr;
        debug(" realloc!!!!!!!!!!!!!!!!!!!!!!!\n");
    }
    memcpy(op->ptr + op->pos, buf, size);

    if (op->pos + size >= op->realSize)
        op->realSize += size;

    //static int cnt = 1;
    //debug("%d write %p %p pos: %d len: %d\n", cnt++, op->ptr, buf, op->pos, size);
    
    op->pos += size;

    return 0;
}

static int64_t my_seek(void *opaque, int64_t offset, int whence)
{
    AVIOBufferContext* op = (AVIOBufferContext*)opaque;
    int64_t new_pos = 0; // 可以为负数
    int64_t fake_pos = 0;

    switch (whence)
    {
        case SEEK_SET:
            new_pos = offset;
            break;
        case SEEK_CUR:
            new_pos = op->pos + offset;
            break;
        case SEEK_END: // 此处可能有问题
            new_pos = op->totalSize + offset;
            break;
        default:
            return -1;
    }
    
    fake_pos = min(new_pos, op->totalSize);
    if (fake_pos != op->pos)
    {
        op->pos = fake_pos;
    }
    //debug("seek pos: %d(%d)\n", offset, op->pos);
    return new_pos;
}

int remuxer_mem_read(int argc, char* argv[])
{
    //输入对应一个AVFormatContext,输出对应一个AVFormatContext
    AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
    AVIOContext *avio_in = NULL, *avio_out = NULL;
    const char *in_filename = NULL, *out_filename = NULL;
    AVPacket pkt;
    
    int ret = 0;

    if (argc < 3)
    {
        printf("usage: %s [input file] [output file]\n", argv[0]);
        printf("eg %s foo.avi bar.ts\n", argv[0]);
        return -1;
    }

    in_filename  = argv[1];
    out_filename = argv[2];

    memset(&g_avbuffer_in, '\0', sizeof(AVIOBufferContext));
    memset(&g_avbuffer_out, '\0', sizeof(AVIOBufferContext));

    read_file(in_filename, (char**)&g_avbuffer_in.ptr, &g_avbuffer_in.totalSize);
    
    // 分配输出视频数据空间
    g_avbuffer_out.ptr = (unsigned char*)av_realloc(NULL, DEFAULT_MEM*sizeof(char));  // new
    if (g_avbuffer_out.ptr == NULL)
    {
        debug("alloc output mem failed.\n");
        return -1;
    }
    g_avbuffer_out.totalSize = DEFAULT_MEM;
    memset(g_avbuffer_out.ptr, '\0', g_avbuffer_out.totalSize);
    
    g_ptr_in = (char*)malloc(IO_BUFFER_SIZE*sizeof(char));
    g_ptr_out = (char*)malloc(IO_BUFFER_SIZE*sizeof(char));

    // 初始化
    av_register_all();

    // 输出相关
    // note 要指定IO内存,还在指定自定义的操作函数,这里有write和seek
    avio_out =avio_alloc_context((unsigned char *)g_ptr_out, IO_BUFFER_SIZE, 1,
                &g_avbuffer_out, NULL, my_write, my_seek); 
    if (!avio_out)
    {
        printf( "avio_alloc_context failed\n");
        ret = AVERROR_UNKNOWN;
        goto end;
    }
    // 分配AVFormatContext
    // 为方便起见,使用out_filename来根据输出文件扩展名来判断格式
    // 如果要使用如“avi”、“mp4”等指定,赋值给第3个参数即可
    // 注意该函数会分配AVOutputFormat
    avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);
    if (!ofmt_ctx)
    {
        printf( "Could not create output context\n");
        ret = AVERROR_UNKNOWN;
        goto end;
    }
    ofmt_ctx->pb=avio_out; // 赋值自定义的IO结构体
    ofmt_ctx->flags=AVFMT_FLAG_CUSTOM_IO; // 指定为自定义

    debug("guess format: %s(%s) flag: %d\n", ofmt_ctx->oformat->name, 
            ofmt_ctx->oformat->long_name, ofmt_ctx->oformat->flags);


    //  输入相关
    // 分配自定义的AVIOContext 要区别于输出的buffer
    // 由于数据已经在内存中,所以指定read即可,不用write和seek
    avio_in =avio_alloc_context((unsigned char *)g_ptr_in, IO_BUFFER_SIZE, 0,
                &g_avbuffer_in, my_read, NULL, NULL); 
    if (!avio_in)
    {
        printf( "avio_alloc_context for input failed\n");
        ret = AVERROR_UNKNOWN;
        goto end;
    }
    // 分配输入的AVFormatContext
    ifmt_ctx=avformat_alloc_context();
    if (!ifmt_ctx)
    {
        printf( "Could not create output context\n");
        ret = AVERROR_UNKNOWN;
        goto end;
    }
    ifmt_ctx->pb=avio_in; // 赋值自定义的IO结构体
    ifmt_ctx->flags=AVFMT_FLAG_CUSTOM_IO; // 指定为自定义

    // 注:第二个参数本来是文件名,但基于内存,不再有意义,随便用字符串
    if ((ret = avformat_open_input(&ifmt_ctx, "wtf", NULL, NULL)) < 0)
    {
        printf("Cannot open input file\n");
        return ret;
    }
    if ((ret = avformat_find_stream_info(ifmt_ctx, NULL)) < 0)
    {
        printf("Cannot find stream information\n");
        return ret;
    }

    // 复制所有的stream
    for (int i = 0; i < (int)(ifmt_ctx->nb_streams); i++)
    {
        //根据输入流创建输出流
        AVStream *in_stream = ifmt_ctx->streams[i];
        AVStream *out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec);
        if (!out_stream)
        {
            printf( "Failed allocating output stream\n");
            ret = AVERROR_UNKNOWN;
            goto end;
        }
        //复制AVCodecContext的设置
        ret = avcodec_copy_context(out_stream->codec, in_stream->codec);
        if (ret < 0)
        {
            printf( "Failed to copy context from input to output stream codec context\n");
            goto end;
        }
        out_stream->codec->codec_tag = 0;
        if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
            out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
    }
    //输出一下格式------------------
    printf("output format:\n");
    av_dump_format(ofmt_ctx, 0, out_filename, 1);

    // 写文件头
    ret = avformat_write_header(ofmt_ctx, NULL);
    if (ret < 0)
    {
        printf( "Error occurred when opening output file\n");
        goto end;
    }

    // 帧
    while (1)
    {
        AVStream *in_stream, *out_stream;
        //获取一个AVPacket
        ret = av_read_frame(ifmt_ctx, &pkt);
        if (ret < 0)
        {
            printf("av_read_frame failed or end of stream.\n");
            break;
        }
        in_stream  = ifmt_ctx->streams[pkt.stream_index];
        out_stream = ofmt_ctx->streams[pkt.stream_index];

        //转换PTS/DTS
        pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, 
            out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
        pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base,
            out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
        pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
        pkt.pos = -1;

        // 写入一帧
        ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
        if (ret < 0) {
            printf( "Error muxing packet\n");
            break;
        }
        av_free_packet(&pkt);
    }

    //写文件尾(Write file trailer)
    printf("--------write trailer------------\n");
    av_write_trailer(ofmt_ctx);

    // 把输出的视频写到文件中
    printf("write to file: %s %p %d\n", out_filename, g_avbuffer_out.ptr, g_avbuffer_out.realSize);
    write_file(out_filename, (char*)g_avbuffer_out.ptr, g_avbuffer_out.realSize, 1);

end:
    if (avio_in != NULL)  av_freep(avio_in); 
    if (avio_out != NULL) av_freep(avio_out);
    //if (g_ptr_in != NULL) free(g_ptr_in);
    if (g_ptr_out != NULL) free(g_ptr_out);

    // 该函数会释放用户自定义的IO buffer,上面不再释放,否则会corrupted double-linked list
    avformat_close_input(&ifmt_ctx);
    avformat_free_context(ofmt_ctx);

    if (g_avbuffer_in.ptr != NULL) free(g_avbuffer_in.ptr);
    if (g_avbuffer_out.ptr != NULL) free(g_avbuffer_out.ptr);

    return ret;
}

PS:有人问我为什么在代码里经常看到LL_DEBUG,实际上“LL”是本人大名。至于解释为“low level”,那是骗人的。

李迟 2015.8.26 周三 晚,吃项目饭

本文固定链接: http://www.latelee.org/my-study/ffmpeg-memory-transcoder2.html

FFMPEG基于内存的转码实例——输入输出视频均在内存:目前有8 条留言

  1. 2楼
    芝麻酱(倍纯浓香):

    楼主的文章拜读了 我如获至宝,果断保存了,特别想认识你一下啊,我最近也在做类似的事情,视频文件不是在内存中,而是自己加密过的,需要用自己的方式来解密,还有也要支持url,所以也是用了avio_alloc_context来实现自己的read和seek函数,但是在读取mp3文件的时候,音频的duration 播放时间总是一个错误的很大的数值,能帮我指点一下吗,请问 你的邮箱是什么呢或者qq是什么?我的qq290795497 盼回复

    2015-11-03 上午10:23 [回复]
    • 李迟:

      音频方面我没接触过。帮不了你的忙。
      你说duration很大,你能不能用文件的方式来测试一下该值。除了该参数,其它的参数是否也正常?

      2015-11-03 下午3:17 [回复]
  2. 1楼
    xu:

    你好,这篇文章给出了一个关于内存读写的很好例子。我这里请教个问题。此代码一次把码流全部读入g_avbuffer_in指向的buffer中,再进行转码。如果读入的码流是实时产生的,只有一小部分码流存放在g_avbuffer_in中,随着时间推移,后生成的码流把g_avbuffer_in中的码流覆盖掉。这样该如何实现转码?谢谢!

    2015-10-09 下午3:25 [回复]
    • 看了你的问题,你是想把实时接收的数据都放到g_avbuffer_in吗?你的目的是什么?如果是要转换格式或编码的话,从g_avbuffer_in拿到数据转换到其它buffer,这样g_avbuffer_in覆盖掉也没什么问题。
      如有疑问,可以发我邮箱。

      2015-10-10 上午10:28 [回复]
      • 暗夜:

        您好,我想请教的问题和上面差不多,现在就是说我也想从内存中读取数据然后再去解码,但是内存中的数据并不是包含一个完整的视频文件,而是实时的以固定数据包大小发过来的,这个固定大小我测试过并不是一帧完整的视频,所以需要在内存中缓存几帧然后组包,我想请教这种实时的情况应该如何实现呢?

        2016-09-29 上午10:35 [回复]
        • 不好意思,您的问题我没考虑过。先按不完整的帧做法,在av_read_frame调试看看会有什么问题或错误。
          另外,不知道有没有可能在进行ffmpeg的read操作前就已经将几帧完成的数据缓存好再送到ffmpeg,我也没研究过。
          好久前写的东西了,只能提点思路了。

          2016-09-30 上午11:15 [回复]
          • 暗夜:

            您好,非常感谢您的回复!我测试的时候先是这样做的:(1)直接把固定数据包8K左右(非完整帧)赋值给avpacket然后送过去解码,这样解码后显示的时候发现就是只能显示屏幕的1/3左右,剩下的有拉伸的感觉,我断定是每次解码的帧非完整。(2)我使用了这种ffmpeg从内存读取数据的方式,先将我的固定数据包缓存一定量的大小,然后再回调函数去拷贝缓存里面的数据,存在的问题是:如果我设置的缓存大小是64K,回调函数中的buf_size=32K的话,它就会一直陷入回调函数的死循环中出不来了?我不太理解这是一种什么样的机制呢?是必须回调函数拷贝到ffmpeg觉得数据量足够解码一帧之后才会退出回调吗?
            如有指教,不胜感激!

            2016-10-11 下午4:24 [回复]
            • 猪八戒:

              先用av_parser_parse2判断一下你接收的buffer是否有完整的帧,然后再送入到avcodec_decode_video2解码。

              2016-11-18 下午8:55 [回复]

发表评论

*

快捷键:Ctrl+Enter