先吐一下槽,Android下的多媒体框架真不好搞,感觉还是微软的DirectShow框架更容易扩展。如果有人知道其它更好的完成下面任务的方法,请告知我。
任务是在现有的音视频格式上做一层包装/加密,打开这种新格式文件时文件流要先经过一层自定义的解包处理,再把解包后的文件流交由解码器处理。
任务看似简单,在DirectShow框架中只需要做个源过滤器,随后即可调用现有的解码器等设施成功解码。但Android上就挣扎了。现有的App框架完全满足不了需求(或许是我没找到),使用opencore又似乎太庞大而且工作量不小,最终选择了FFMPEG。
通过FFMPEG完成上面的任务主要途径有两种:一是修改打开文件读文件流识别解码器部分和解码器读文件流部分;二是重新写一个解码器(这似乎和在opencore中实现一个解码器相当)。
分析一下打开文件读文件流识别解码器的部分吧,入口函数是av_open_input_file,其最重要的作用就是填充好AVFormatContext结构。
AVFormatContext结构是解析文件格式、找到对应的解码器的重要的结构之一。其定义略长,在此简述。
struct AVInputFormat *iformat;
struct AVOutputFormat *oformat;
ByteIOContext *pb;
iformat和oformat两者只有一个会生效,如果是播放iformat生效,录制则oformat生效。当然还有转码的过程中,输入iformat生效,输出oformat生效。
pb是对文件流操作的一个抽象,接下来讲解。
FFMPEG打开文件后会读一段文件流,根据文件流中的内容来找到一个对应AVInputFormat,即一种文件格式的抽象。比如mp4,它的AVInputFormat定义在/libffmpeg/libavformat/mov.c中:
AVInputFormat mov_demuxer = {
"mov,mp4,m4a,3gp,3g2,mj2",
NULL_IF_CONFIG_SMALL("QuickTime/MPEG-4/Motion JPEG 2000 format"),
sizeof(MOVContext),
mov_probe,
mov_read_header,
mov_read_packet,
mov_read_close,
mov_read_seek,
};
AVInputFormat定义在/libffmpeg/libavformat/avformat.h中,其定义略长,在此简述。
const char *name; // 格式别名
const char *long_name; // 格式的全名,方便人阅读
int priv_data_size; // 额外数据长度
int (*read_probe)(AVProbeData *); // 读取适配数据的函数指针
... // 其它函数指针
FFMPEG努力用C来实现面向对象,其中结构体中含函数指针的写法非常常见。所有格式的AVInputFormat(还有AVOutput协议)都会在/libffmpeg/libavformat/Allformats.c的av_register_all函数中注册,形成一个以
/** head of registered input format linked list */
AVInputFormat *first_iformat = NULL;
为链表头的链表。ff_probe_input_buffer会通过ByteIOContext读取一段适配数据然后调用av_probe_input_format2找到正确的AVInputFormat(即通过遍历first_iformat)。
那么实现上面的任务通过修改ff_probe_input_buffer,在其中先解包,再让其调用av_probe_input_format2即可以达到部分欺骗FFMPEG的目的。但事实并不那么容易,找到正确的AVInputFormat后FFMPEG还会在av_open_input_stream中调用它的read_header来读取文件头部信息。这下麻烦大了,如果包装的文件原来是mp4格式的,那么得改mp4的AVInputFormat的read_header函数,如果是rmvb的……这个复杂性就不用说了。
回过头来看FFMPEG读文件流的过程,依赖于ByteIOContext,在url_fopen--->url_fdopen中生成了该对象并通过
if (init_put_byte(*s, buffer, buffer_size,
(h->flags & URL_WRONLY || h->flags & URL_RDWR), h,
url_read, url_write, url_seek) < 0)
设置了其内容。ByteIOContext定义如下:
/**
* Bytestream IO Context.
* New fields can be added to the end with minor version bumps.
* Removal, reordering and changes to existing fields require a major
* version bump.
* sizeof(ByteIOContext) must not be used outside libav*.
*/
typedef struct {
unsigned char *buffer;
int buffer_size;
unsigned char *buf_ptr, *buf_end;
void *opaque;
int (*read_packet)(void *opaque, uint8_t *buf, int buf_size);
int (*write_packet)(void *opaque, uint8_t *buf, int buf_size);
int64_t (*seek)(void *opaque, int64_t offset, int whence);
int64_t pos; /**< position in the file of the current buffer */
int must_flush; /**< true if the next seek should flush */
int eof_reached; /**< true if eof reached */
int write_flag; /**< true if open for writing */
int is_streamed;
int max_packet_size;
unsigned long checksum;
unsigned char *checksum_ptr;
unsigned long (*update_checksum)(unsigned long checksum, const uint8_t *buf, unsigned int size);
int error; ///< contains the error code or 0 if no error happened
int (*read_pause)(void *opaque, int pause);
int64_t (*read_seek)(void *opaque, int stream_index,
int64_t timestamp, int flags);
void *URLContext;
} ByteIOContext;
通过分析可以看出,url_read就是其读取文件流的方式。这里注意到url_read的实现:
int url_read(URLContext *h, unsigned char *buf, int size)
{
int ret;
if (h->flags & URL_WRONLY)
return AVERROR(EIO);
ret = h->prot->url_read(h, buf, size);
return ret;
}
可见读取文件流的过程又与具体的协议内容(URLContext)有关。其实在这里已经能够完成任务了,如下:
int fd = h->prot->url_get_file_handle(h); // 其实就是h->priv_data
long pos = lseek(fd, 0, SEEK_CUR);
__android_log_print(ANDROID_LOG_INFO, TAG, "url_read ftell pos: %d", pos);
pos代表文件流当前的位置,这样在哪个位置如何解包就一清二楚了。
下面还是简单说说URLContext与URLProtolcol。URLContext的定义如下:
/**
* URL Context.
* New fields can be added to the end with minor version bumps.
* Removal, reordering and changes to existing fields require a major
* version bump.
* sizeof(URLContext) must not be used outside libav*.
*/
typedef struct URLContext {
#if LIBAVFORMAT_VERSION_MAJOR >= 53
const AVClass *av_class; ///< information for av_log(). Set by url_open().
#endif
struct URLProtocol *prot;
int flags;
int is_streamed; /**< true if streamed (no seek possible), default = false */
int max_packet_size; /**< if non zero, the stream is packetized with this max packet size */
void *priv_data;
char *filename; /**< specified URL */
} URLContext;
其中最重要的就是URLProtocol了,打开文件的过程中会根据文件的URL生成对应的URLProtocol及URLContext,才有了后面url_read中的ret = h->prot->url_read(h, buf, size);这儿的调用。URLProtocol才是真正负责协议中文件流数据读取的底层数据结构。下面举file协议(即本地文件)的例子。
URLProtocol file_protocol = {
"file",
file_open,
file_read,
file_write,
file_seek,
file_close,
.url_get_file_handle = file_get_handle,
};
其中file_read:
static int file_read(URLContext *h, unsigned char *buf, int size)
{
int fd = (intptr_t) h->priv_data;
return read(fd, buf, size);
}
可以看到了,就是Linux中read读文件。