FFMPEG基于内存的转码实例

前面有文章写道,尝试在FFMPEG里面直接使用FTP协议将H.264祼码流封装为AVI视频格式存储到FTP服务器上,遗憾的是以失败告终。然此事须解决,在多方考虑之后,决定使用内存临时存储,而不是写入磁盘文件,一来不必要,二来对磁盘有损耗。

FFMPEG支持内存转码的,但没有现成可用的封装得很好的接口。几经翻阅网络文章,找到了些许方法,结合自己的实验,完成既定目标。网络上暂未发现有此类应用,写出来,尽一人绵薄之力。至于经常偷窃他人成果,亦于此略表心意。
在FFMPEG中,转换视频有一套固定的方式,初始化好参数后,就是调用avformat_write_header写头部信息,调用av_write_frame写一帧数据,调用av_write_trailer写尾部信息,它们都使用AVFormatContext结构体作为参数。
对于使用文件名(file协议)的情况,初始化参数后,调用avio_open来打开文件。后面可以不用管文件名称了。
对于使用ftp协议的情况,除了在协议路径上加ftp://ip/,其它的步骤和上述情况一样。但必须注意的是,编译FFMPEG时要添加对应的协议,否则会不成功。另外还要在代码加上avformat_network_init函数进行初始化。
说回本文重点。本文关注的是使用FFMPEG将转换好的视频数据放到内存中,该内存中的数据是完全的视频,直接保存成文件即可播放而不用。很多年以前搞AVI时研究过其格式,知道在写AVI文件时要将文件指针定位到文件头部,写文件总大小,会使用seek函数。有的FTP服务是不支持回写和sekk功能的,这也是为什么我之前的尝试会失败。
鉴于此,想到了将所有的视频数据放到开辟好的内存,而seek则直接用指针来运算。网络有这方面的文章,请参考文后地址。
FFMPEG支持自定义AVIOContext,使用avio_alloc_context返回AVIOContext结构体,同时该函数会传递自定义的read、write、seek函数。再使用avformat_alloc_output_context2函数分配AVFormatContext结构体,再将上面分配的AVIOContext结构体赋值给AVFormatContext的pb成员。

下面说说我所做的步骤。
1、定义好write、seek或read。具体参考文后源码。要注意的是,这里没有读操作,因此不实现,关注的是转码后的数据,因而实现了write和seek。在write过程中,会出现内存不够的情况所以我使用av_realloc函数来扩展内存,每次只扩展一半,不够再扩。
2、创建AVIOContext,关赋值,示例如下:

1
2
3
4
5
6
7
AVFormatContext  *ofmt_ctx = NULL;
AVIOContext *avio_out = NULL;
avio_out =avio_alloc_context((unsigned char *)g_ptr1, IO_BUFFER_SIZE, 1,
NULL, NULL, my_write, my_seek);
avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);
ofmt_ctx->pb=avio_out; // 赋值自定义的IO结构体
ofmt_ctx->flags=AVFMT_FLAG_CUSTOM_IO; // 指定为自定义

在这里要注意avformat_alloc_output_context2函数,它会分配AVOutputFormat(如果不指定的话),out_filename是根据文件名来判断是哪一种格式,因为我没有限制使用什么格式,这和前面提到不使用文件是两回事。详见代码。
3、由于不使用文件了,所以不需要调用avio_open了。我在这里也纠结好久,一时没转过弯来。 其它就和普通文件的操作类似。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
/**
他山之石,学习为主,版权所无,翻版不究,有错无责

基于内存的格式封装测试
使用
./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、自定USE_MEM,则转换后的数据在内存中,要用write将其写回到文件
   如果不定义,则每一次write都会写到文件,seek也是文件操作

2、传递给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)

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

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.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

// 自定义的IO全部在内存
#define USE_MEM

#define DEFAULT_MEM (10*1024*1024)

#define IO_BUFFER_SIZE (32768*1)

static char g_ptr1[IO_BUFFER_SIZE] = {0};

static char* g_ptr = NULL;
//static char g_ptr[DEFAULT_MEM] = {0};
static int g_nPos = 0;
static int g_nTotalLen = DEFAULT_MEM;

static int g_nRealLen = 0;

static int g_fd = 0;

int my_read(void *opaque, unsigned char *buf, int buf_size)
{
    // no
    return 0;
}

int my_write(void *opaque, unsigned char *buf, int size)
{
    if (g_nPos + size > g_nTotalLen)
    {
        // 重新申请
        // 根据数值逐步加大
        int nTotalLen = g_nTotalLen*sizeof(char) * 3 / 2;
        char* ptr = (char*)av_realloc(g_ptr, nTotalLen);
        if (ptr == NULL)
        {
// if (g_ptr) av_free(g_ptr);
            return -1;
        }
        debug("org ptr: %p new ptr: %p size: %d(%0.fMB) ", g_ptr, ptr, 
                    nTotalLen, nTotalLen/1024.0/1024.0);
        g_nTotalLen = nTotalLen;
        g_ptr = ptr;
        debug(" realloc!!!!!!!!!!!!!!!!!!!!!!!\n");
        // todo
        //memcpy(g_ptr + g_nPos, buf, size);
    }
    memcpy(g_ptr + g_nPos, buf, size);

    if (g_nPos + size >= g_nRealLen)
        g_nRealLen += size;
    
    static int cnt = 1;
    debug("%d write %p %p pos: %d len: %d\n", cnt++, g_ptr, buf, g_nPos, size);
    //dump("org ", (char*)buf, 32);
    //dump("g_ptr ", g_ptr, 32);
    
    g_nPos += size;
    return 0;
}

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

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

int my_write1(void *opaque, unsigned char *buf, int size)
{
    debug("write %p len: %d\n",  buf, size);
    //dump("org ", (char*)buf, 32);
    //dump("g_ptr ", (char*)g_ptr, 32);
    return write(g_fd, buf, size);
}

int64_t my_seek1(void *opaque, int64_t offset, int whence)
{
    debug("seek pos: %d whence: %d\n", offset, whence);
    return lseek(g_fd, offset, whence);
}

// 定义2个函数指针,免得后面改
static int (*write_packet)(void *opaque, uint8_t *buf, int buf_size) = 
#ifdef USE_MEM
my_write
#else
my_write1
#endif
;

static int64_t (*seek)(void *opaque, int64_t offset, int whence) =
#ifdef USE_MEM
my_seek
#else
my_seek1
#endif
;

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

    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];

    // 分配空间
    g_ptr = (char*)av_realloc(NULL, DEFAULT_MEM*sizeof(char));  // new
    if (g_ptr == NULL)
    {
        debug("alloc mem failed.\n");
        return -1;
    }
    printf("------alloc ptr: %p\n", g_ptr);
    memset(g_ptr, '\0', DEFAULT_MEM);

    av_register_all();

    //输入(Input)
    if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0)
    {
        printf( "Could not open input file '%s': %s\n", in_filename, strerror(errno));
        goto end;
    }
    if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0)
    {
        printf( "Failed to retrieve input stream information\n");
        goto end;
    }

    printf("input format:\n");
    av_dump_format(ifmt_ctx, 0, in_filename, 0);

    //输出(Output)
    
    // 先打开要保存的文件
    g_fd = open(out_filename, O_CREAT|O_RDWR, 0666);
    
    // 分配自定义的AVIOContext
    // 注意,参考file协议的内存,使用大小32768,而g_ptr是不同一片内存的
    avio_out =avio_alloc_context((unsigned char *)g_ptr1, IO_BUFFER_SIZE, 1,
                NULL, NULL, write_packet, seek); 
    if (!avio_out)
    {
        printf( "avio_alloc_context failed\n");
        ret = AVERROR_UNKNOWN;
        goto end;
    }
    // 分配AVFormatContext
    // out_filename 根据输出文件扩展名来判断格式
    // 注意该函数会分配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);

    for (int i = 0; i < (int)ifmt_ctx->nb_streams; i++)
    {
        //根据输入流创建输出流(Create output AVStream according to input AVStream)
        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的设置(Copy the settings of 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);

    //打开输出文件(Open output file)
    // !!!! 存在于内存,不需要明确指定打开,这种形式是针对文件或网络协议的
    /*
    if (!(ofmt->flags & AVFMT_NOFILE))
    {
        ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
        if (ret < 0)
        {
            printf( "Could not open output file '%s': %s\n", out_filename, strerror(errno));
            goto end;
        }
    }
    */

    //写文件头(Write file header)
    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(Get an AVPacket)
        ret = av_read_frame(ifmt_ctx, &pkt);
        if (ret < 0)
            break;
        in_stream  = ifmt_ctx->streams[pkt.stream_index];
        out_stream = ofmt_ctx->streams[pkt.stream_index];

        /* copy packet */
        //转换PTS/DTS(Convert 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;

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

    //写文件尾(Write file trailer)
    av_write_trailer(ofmt_ctx);

end:
    avformat_close_input(&ifmt_ctx);

    avformat_free_context(ofmt_ctx);

    #ifdef USE_MEM
    write(g_fd, g_ptr, g_nRealLen);
    #endif
    av_freep(&avio_out);
    av_freep(&g_ptr);

    close(g_fd);
    return ret;
}

参考资料:
http://blog.csdn.net/leixiaohua1020/article/details/25422685
http://stackoverflow.com/questions/13838841/decode-a-video-file-from-memory-using-libav
http://www.codeproject.com/Tips/489450/Creating-Custom-FFmpeg-IO-Context

李迟 2015.6.9 周二 中午