FFmpeg在Android上读取文件的方法

随着Android系统对文件访问权限的收紧,很多时候,不能再通过文件路径来访问文件。Android上使用FFmpeg,具体而言,使用libavformat访问本地文件,如何实现呢?

0、最简单且最低效的方式

当文件不能直接访问时,可以通过Android Java的API,把文件拷贝到App私有目录,再让FFmpeg访问拷贝后的文件。这种处理方式最简单、效率最低,but it works。

1、avio callback

AVIOContext *avio_alloc_context(
                  unsigned char *buffer,
                  int buffer_size,
                  int write_flag,
                  void *opaque,
                  int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),
                  int (*write_packet)(void *opaque, const uint8_t *buf, int buf_size),
                  int64_t (*seek)(void *opaque, int64_t offset, int whence));

通过callback方式实现自定义IO,传递给avformat,可以实现只用libavformat的muxer/demuxer能力,不依赖libavformat protocol的效果。

avio callback是最灵活的一种方式。FFmpeg自己实现的IO能力,即各种protocol,往往是有基本功能,但是实现并不完备,效率不高。和demuxer/muxer/decoder相比,FFmpeg protocol是最需要优化的部分。但因为libavformat缺少一些基础设施,比如没有event loop,IO实现方式原始,优化难度大。不少人认为,不需要优化libavformat内置的protocol了,只需要使用avio callback接口,用户可以使用专业的第三方库来实现高效的IO能力。我部分同意这种方式,需要高效IO时自己实现,但简单应用要求不高时,直接用libavformat现成的能力更方便,特别是作为测试工具。

对于Android本地文件访问,可以在C/C++层实现avio callback,再通过JNI方式把IO请求发送到Java层,Java层把数据传回来。灵活是灵活了,但十分啰嗦。

2、通过file descriptor方式访问

Android系统提供的API一直是这种风格:提供基本完备的Java API,再给C/C++开一扇窗户。

本地文件不让直接通过路径访问的时候,ParcelFileDescriptor提供了通过文件描述符file descriptor的访问方式:

public int getFd ()
public int detachFd ()

前者是获取native file descriptor,但是所有权还是属于ParcelFileDescriptor,不能直接close返回的fd。后者是取走所有权,native负责close fd。

也就是说,你可以在Java层获取到对应文件的fd,通过JNI传递到C/C++层,让FFmpeg libavformat通过fd来访问本地文件。

libavformat有提供通过fd访问文件的能力吗?上一篇我们讲到,ffmpeg可以通过pipe,跨进程传递数据给ffplay。libavformat里的pipe protocol,就是通过fd访问数据的一个实现。实现方式上,pipe protocol并不关心fd是不是真的pipe,只要能读或者能写就够了,可以是普通的fd。

但是,libavformat 的pipe protocol只考虑了pipe的形式,也就是通过fd流式的读写文件。pipe protocol缺少一个关键的能力:seek。谁也没想着有这种需求,通过fd访问文件,还要支持seek,因为很多文件离了seek无法解析,比如mp4.

所以,可以说libavformat提供了通过fd访问文件文件的能力,但又是个不完备的,没有实际使用价值的能力。

给pipe protocol加上seek的能力很简单,关键是判断传进来的fd是否支持seek。可以通过fstat来判断:

    if (fstat(c->fd, &st) < 0)
        return AVERROR(errno);
    h->is_streamed = !(S_ISREG(st.st_mode) || S_ISBLK(st.st_mode));

实现功能简单,还有一个设计问题:是给pipe protocol加上seek能力,还是加一个新的protocol?

pipe从概念上来说就是流式的,没有seek能力。给一个概念上不具备seek能力的模块加上seek功能,我认为是设计缺陷。比如,假如有人想测试下一个demuxer或muxer在non-seekable IO上的表现而选择用pipe protocol,悄悄增加的seek能力让测试无效了。

所以我选择增加一个协议,增加的协议就叫fd protocol。这个协议在2022年就合并到ffmpeg仓库中了,随着ffmpeg 6.0发布。

ffplay通过fd protocol播放视频的示例:

ffplay fd: < ~/Movies/cctv.mp4

这里有个小细节,通过 < 创建的file descriptor,是有seek能力的。Unix下没问题,不知道Windows上表现如何。

没有seek能力的示例:

cat ~/Movies/cctv.mp4 |ffplay fd:

当moov位于文件末尾时,这种方式无法播放。

综上所述,通过Java的ParcelFileDescriptor getFd/detachFd,加上libavformat的fd protocol,也就实现了本地文件访问的目的。

3、通过content protocol访问

这是为libavformat新增加的一个协议,不是我实现的,我还在review这个patch。这个patch实现的功能是:

1、通过JNI调用ContentResolver,获取ParcelFileDescriptor,再获取fd

2、剩下的部分和方法2相同

也就是说,content protocol是把一部分本来要在Java层实现的功能,通过JNI方式搬到了FFmpeg里。

关于这个patch,我很难说是赞成还是反对。一方面,它确实简化了一部分用户的工作,用户可以直接传一个content:// URL。另一方面,它在设计上看着不够优雅:fd是个高度抽象,跨平台的通用解决方案,而content protocol是一个Android特有的协议,一个通过大量JNI调用实现的协议。

基于尊重别人劳动成果、新功能没有特别的负面影响、并且有用户存在的考虑,我同意增加content protocol。如果没有其他人反对的情况,content protocol有望在FFmpeg 7.0之前合并到仓库中。

作者:quink
来源:Fun With FFmpeg
原文:https://mp.weixin.qq.com/s/9eKEtEzNQRkUeIgWxR8FEg

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论