FFmpeg读取Assets资源文件

news2024/11/25 20:38:11

在Android开发中我们经常把原生资源文件放在assets目录下以供需要时读取,通过API提供的resources.assets.open(filename)/openFd(filenam)方法可以非常方便获得InputStream或FileDescriptor(文件标识符),但是在使用FFmpeg读取Assets文件时却遇到了困难。主要原因在于FFmpeg读取媒体文件时是通过传入的url来判断IO协议的,也就是说如果想要使FFmpeg能够正确读取Assets文件就需要选择合适的IO协议来构造url并传入avformat_open_input(...)方法中,然而实际操作起来貌似问题多多。

AssetFileDescriptor

调用resources.assets.openFd(filename)可以返回AssetFileDescriptor,那么FFmpeg是否可以通过文件标识符(以下简称fd)来打开媒体文件呢?答案是肯定的。但是对于assets文件会存在一个问题,assets返回的AssetFileDescriptor中带有mStartOffset需要处理,也就是说实际的有效数据需要从mStartOffset处开始读取。

Stackoverflow上有同样的提问:

How to properly pass an asset FileDescriptor to FFmpeg using JNI in Android

实现方式

  1. 在native层获取实际的fd后使用pipe协议或file协议拼装url

int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
char path[20];
sprintf(path, "/proc/self/fd/%d", fd);

在调用avformat_open_input(...)方法前手动创建AVFormatContext并设置skip_initial_bytes参数。

fmtCtx = avformat_alloc_context();
fmtCtx->skip_initial_bytes = offset;
fmtCtx->iformat = av_find_input_format("mp3");

注:avformat_open_input用于打开媒体文件并获取相关的文件信息,对于该函数更详细的分析可以参考雷神的文章FFmpeg源代码简单分析:avformat_open_input()。实际上,我们也可以不指定iformat,指定了

iformat的好处是FFmpeg将不再对媒体文件的格式进行探测。

问题

当使用上述方案时,发现并不能正常的解码mp4(mov格式)视频文件,Stackoverflow上的评论也有人遇到同样的问题,比较奇怪的是虽然解码失败但是可以获取视频文件的基本信息,查看FFmpeg输出日志:
 

//这里仅贴出较为关键的日志
[FFMPEG]--:type:'ftyp' parent:'root' sz: 32 8 15677739
[FFMPEG]--:ISO: File Type Major Brand: isom
[FFMPEG]--:type:'free' parent:'root' sz: 8 40 15677739
[FFMPEG]--:type:'mdat' parent:'root' sz: 7606147 48 15677739
[FFMPEG]--:type:'moov' parent:'root' sz: 7384 7606195 15677739
....
//sample与chunk的映射关系
[FFMPEG]--:AVIndex stream 0, sample 0, offset 30, dts 0, size 83337, distance 0, keyframe 1
[FFMPEG]--:AVIndex stream 0, sample 1, offset 1494b, dts 512, size 118, distance 1, keyframe 
....
[FFMPEG]--:nal_unit_type: 7(SPS), nal_ref_idc: 3
[FFMPEG]--:nal_unit_type: 8(PPS), nal_ref_idc: 3
[FFMPEG]--:stream 0, sample 0, dts 0
[FFMPEG]--:stream 1, sample 0, dts 0
//解析NAL报错,NAL用于存储视频流(H264)数据
[FFMPEG]--:Invalid NAL unit size (1920229220 > 83333).
[FFMPEG]--:Error splitting the input into NAL units.
....

从FFmpeg的日志中我们可以发现,FFmpeg在解析mp4文件信息时并没有出错,正确的识别了ftypmdatmoov等关键atom,在后续解析中也正常的解析了sample(采样)与chunk(数据块)的映射关系(stsc),但是在解码阶段却报出明显的Invalid NAL unit size异常。

分析

assets目录下的媒体文件是否被Android特殊处理了?

上述的现象首先想到的是assets目录下的资源文件是否被Android特殊处理过,最后导致FFmpeg在通过fd读取文件解码时发生错误。从AssetFileDescriptor的官方描述中可以获知Android好像并没有对assets下的文件做特殊的处理。

File descriptor of an entry in the AssetManager. This provides your own opened FileDescriptor that can be used to read the data, as well as the offset and length of that entry's data in the file.

然而在Android音视频开发中,我们经常使用MediaCodec的setDataSource(fd)方法来向MediaCodec传递媒体数据,MediaCodec却仍然可以正常的读取assets资源文件,难道在使用Android的AssetFileDescriptor文件标识符时需要特殊的处理?

MediaCodec是如何处理AssetFileDescriptor的?
搜索源码可以查找到MediaExtractor#setDataSource所调用的native方法为NuMediaExtractor::setDataSource,该方法最后将fd封装为FileSource对象。
NuMediaExtractor.cpp
 

status_t NuMediaExtractor::setDataSource(int fd, off64_t offset, off64_t size) {
		...
    if (mImpl != NULL) {
        return -EINVAL;
    }
  	//创建FileSource
    sp<FileSource> fileSource = new FileSource(dup(fd), offset, size);
		...
    return OK;
}

FileSource中读取数据的操作就是调用普通的linux内核函数read,这一过程并没有对fd做任何特殊的处理。

FileSource.cpp

ssize_t FileSource::readAt_l(off64_t offset, void *data, size_t size) {
  	//seek到指定的offset
    off64_t result = lseek64(mFd, offset + mOffset, SEEK_SET);
    if (result == -1) {
        ALOGE("seek to %lld failed", (long long)(offset + mOffset));
        return UNKNOWN_ERROR;
    }
    //seek后从fd中读取指定大小的数据到data中
    return ::read(mFd, data, size);
}

【免费分享】音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击788280672加群免费领取~

那么FFmpeg是如何处理AssetFileDescriptor的?是否与MediaCodec不同?

FFmpeg对应的file协议处理在libavforamt/file.c中:

//4.3.1源码,109行
static int file_read(URLContext *h, unsigned char *buf, int size)
{
    FileContext *c = h->priv_data;
    int ret;
    size = FFMIN(size, c->blocksize);
  	//同样是调用的read函数
    ret = read(c->fd, buf, size);
    if (ret == 0 && c->follow)
        return AVERROR(EAGAIN);
    if (ret == 0)
        return AVERROR_EOF;
    return (ret == -1) ? AVERROR(errno) : ret;
}

比较二者处理逻辑,FFmpeg在读取数据时也是使用的read函数,不同的是file_read函数中并没有seek相关的逻辑,这是因为libavformat/file.c中封装的是最基础的IO操作,并不会写有其他无关的逻辑,FFmpeg将所有的IO协议封装为URLProtocl结构体,我们可以简单看下file协议的定义:
 

const URLProtocol ff_file_protocol = {
    .name                = "file",
    .url_open            = file_open,
    .url_read            = file_read,//file_read函数指针
    .url_write           = file_write,
    .url_seek            = file_seek,//seek
    .url_close           = file_close,
		....
    ....
    .default_whitelist   = "file,crypto,data"
};

FFmpeg与MediaCodec在读取AssetFileDescriptor时都是使用的read函数,但是我们暂时无法确定FFmpeg内部seek的逻辑是否存在问题,由此怀疑FFmpeg是不是没有正确的处理AssetFileDescriptor的startOffset。
测试AVFormatContext中的skip_initial_bytes是否存在问题

  • 我们首先测试正常情况下offset为0的fd,我们可以使用Android中的ParcelFileDescriptor来将一个本地路径转换为fd:

Android应用层:

val fd = ParcelFileDescriptor.open(File(videoPath), ParcelFileDescriptor.MODE_READ_ONLY)

Native层:

fmtCtx = avformat_alloc_context();
fmtCtx->skip_initial_bytes = 0;
fmtCtx->iformat = av_find_input_format("mp4");

结果:正常解码

  • 测试offset不为0,我们可以使用十六进制工具打开视频文件手动的在文件头部添加几个字节来模拟offset的情况

Android应用层调用与上述相同

Native层需要设置skip_initial_bytes变量:

fmtCtx = avformat_alloc_context();
fmtCtx->skip_initial_bytes = 3;//在文件头部手动添加的字节数
fmtCtx->iformat = av_find_input_format("mp4");

结果:不能正常解码,可以获取媒体文件基本信息,日志与上述“问题”中的日志相同。
(如果你不具备测试条件,可以使用我的测试分支来检验结果,clone代码后可直接运行)
通过上面一些列的分析,几乎可以确定:FFmpeg在处理mp4(mov格式)的媒体文件时,如果设置了AVFormatContextskip_initial_bytes变量,FFmpeg将不能正确的读取和解码文件。
原因
通过查阅avformat_open_input函数可以看到下述与skip_initial_bytes变量相关的代码:
 

...
if ((ret = init_input(s, filename, &tmp)) < 0)
    goto fail;
...
avio_skip(s->pb, s->skip_initial_bytes);
...    
if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->iformat->read_header)
    if ((ret = s->iformat->read_header(s)) < 0)
        goto fail;
...

在调用了重要函数init_input之后,avformat_open_input调用了avio_skip(s->pb, s->skip_initial_bytes);函数,说明在正确识别IO协议和探测文件格式(如何没有设置iformat)后,avformat_open_input将文件seek到了指定的offset位置,并没有其余的处理逻辑。
随后将会调用AVInputformat中的read_header函数指针,read_header函数指针指向对应文件格式的函数,在mov格式中的read_header函数为mov_read_headermov_read_header函数非常重要,内部将会调用mov_read_default()函数以8字节为单位循环地读取数据并查找是否有与mov_default_parse_table表中相匹配的atom,当查找到trak(可以理解为视频文件的媒体流,比如视频流或音频流)时即会调用mov_build_index()函数来进一步解析stsc(上文提到的sample 与chunk的映射关系)。在后续解码阶段调用av_frame_read()函数时,mov.c将会依赖stsc中的映射关系来查找和读取指定的chunk。
 

//4.3.1版本,mov.c 
static void mov_build_index(MOVContext *mov, AVStream *st) {
  		....
      //3935行
      AVIndexEntry *e;
 			....
      e->pos = current_offset;//直接赋值了解析stsc得到的chunk位置
      e->timestamp = current_dts;
      e->size = sample_size;
      e->min_distance = distance;
      e->flags = keyframe ? AVINDEX_KEYFRAME : 0;
  		//这里打印的是我们上述分析的日志,还记得吗?
      av_log(mov->fc, AV_LOG_TRACE, "AVIndex stream %d, sample %u, offset %"PRIx64", dts %"PRId64", "
              "size %u, distance %u, keyframe %d\n", st->index, current_sample,
              current_offset, current_dts, sample_size, distance, keyframe);
  		....
       
}

//av_frame_read函数最终也会调用到相应媒体格式的read_packet函数,雷神也有分析的文章,这里不再赘述
static int mov_read_packet(AVFormatContext *s, AVPacket *pkt)
{
  ....
    //7887行
    if (st->discard != AVDISCARD_ALL) {
        //这里的sample->pos为上述stsc中解析得到的pos,而且seek模式为SEEK_SET
        int64_t ret64 = avio_seek(sc->pb, sample->pos, SEEK_SET);
      	....
    }
  ....
}

相信熟悉mov格式或文件IO的朋友基本已经看到问题所在了,avformat_open_input虽然在read_header之前seek了指定的skip_initial_bytes,但是在read_header解析chunk的offset时并没有加上我们文件的skip_initial_bytes,而mov.c在后续read_packet操作时又是根据read_header解析到的位置重新seek,最终导致从av_read_frame获得的AVPacket中的数据是错误的数据,所以给到编码器也就无法正常解码。

解决

解决方案比较简单,因为在mov.c解析atom时只传递了AVIOContext,所以我在AVIOContext中添加了同样的

skip_initial_bytes字段,当调用mov_build_index并在AVIndexEntry(采样映射关系的结构体)赋值pos时加上相应的skip_initial_bytes。我已将这种方案提交到了Github上,并详细说明了修改的文件,同时我也基于WhatTheCodec工程编写了新的demo,经过我最近的测试,并没有发现其他的问题。如有其他问题,欢迎大家交流并互相学习。
Github: github.com/YiChaoLove/…
Demo: github.com/YiChaoLove/…

pipe协议

在上述Stackoverflow的提问的回答中有提到使用pipe协议,实际上,这种方式也是可行的,但是我在实现的过程中发现了几个需要注意的问题,由于篇幅有限,我在这里便直接描述问题。

使用pipe协议需要注意缓冲区大小

pipe协议通常用于linux进程间通讯,但是pipe缓冲区是有大小限制的,在《深入理解Linux内核》一书中有提到其大小为16个页,每个页大小为4KB,那么按照LInux系统的规则来计算则为64KB,Android是基于LInux的操作系统,所以在Android中使用pipe协议来传递数据时也需要遵循pipe的调用规范来使用。我们可以在应用层新建单独的线程来向pipe的一端写入数据,而FFmpeg是读的一端。
为了更具有通用性,我在native层手动创建了pipe,把pipe的输出端fd给到FFmpeg,输入端fd由应用层持有并在IO线程中写入数据,这样,我们便可以利用pipe协议非常灵活的写入数据,甚至可以把内存中的视频数据直接传入FFmpeg中。详细代码见MediaFileBuilder.kt中的from(inputStream: InputStream, shortFormatName: String) 方法。

pipe协议对于某些视频(mov格式)文件无效?

当使用pipe时你有可能会遇到有些视频并不能正确的读取和解码,下面是一个类似的报错日志:

....
[FFMPEG]--:Before avformat_find_stream_info() pos: 7613571 bytes read:7613571 seeks:0 nb_streams:2
....
[FFMPEG]--:format: start_time: 0 duration: 7.234 (estimate from stream) bitrate=0 kb/s
[FFMPEG]--:Could not find codec parameters for stream 0 (Video: h264 (avc1 / 0x31637661), none, 720x960, 8285 kb/s): unspecified pixel format
[FFMPEG]--:Could not find codec parameters for stream 1 (Audio: aac (mp4a / 0x6134706D), 44100 Hz, 2 channels, 128 kb/s): unspecified sample format
[FFMPEG]--:After avformat_find_stream_info() pos: 7613571 bytes read:7613571 seeks:0 frames:0
....

日志报错在avformat_find_stream_info函数不能正确的找到编码器参数,经过对这些视频文件的分析和比较,我发现,对于moovmdat之后的视频文件,使用pipe协议将无法正常读取和解码,使用faststart参数将moov调整到mdat之前便可以正常读取和解码。
 

ffmpeg -i video.mp4 -c copy -movflags +faststart output.mp4

faststart参数多用于流媒体优化,mdat为实际存储视频数据的atom,moov用于存储视频信息,如果

moov在其之后,那么播放器在播放视频时就需要一直向后查找搜索moov。将moov放在文件头部(ftyp之后)有利于播放器快速获取视频信息,moov放在尾部那么播放器将不得不先read之前所有的数据,以FFmpeg为例,这一过程将会发生在上文提到的:

mov_read_header函数非常重要,内部将会调用mov_read_default()函数以8字节为单位循环地读取数据并查找是否有与mov_default_parse_table表中相匹配的atom

说到这里,你可能已经明白原因了,使用pipe协议为半双工单向通讯,如果视频文件的moovmdat之后,那么在avforamt_open_input读取完视频信息后,实际上FFmpeg已经read到了数据的结尾,而且pipe协议不支持seek,数据从缓冲区中读取后无法再次读取,所以这种情况下将不能够正确读取和解码视频文件。

总结

本文着重的分析了在使用AssetFileDescriptor向FFmpeg传递数据时遇到的问题,这一问题实际为FFmpeg在解析mov格式文件时不能正确处理skip_initial_bytes所致。最后我分享了在使用pipe协议时遇到的问题与解决方案。上述这两个问题的解决可能会大大方便FFmpeg在Android上的使用,虽然MediaCodec在音视频开发(Android)的部分场景中已经渐渐的取代了FFmpeg,但是FFmpeg的通用性、稳定性和兼容性使之仍然可能在未来的Android音视频开发中长期存在。
上述文章观点或心得仅代表个人,也可能有错误之处,如果你对文章中提及的内容有问题或质疑,欢迎issues。如果你觉得这篇文章对你的开发或学习有帮助,欢迎在FFmpegForAndroidAssetFileDescriptor上star。文中有提到mov格式,可以参考:

  • docs.fileformat.com/video/mov/
  • developer.apple.com/library/arc…



作者:Good_Boy
原文链接 FFmpeg读取Assets资源文件 - 掘金

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1366369.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CTF-PWN-沙箱逃脱-【seccomp和prtcl-1】

文章目录 啥是seccomp#ifndef #define #endif使用使用格式 seccomp无参数条件禁用系统调用有参数条件禁用系统调用 prctl实例 seccomp_export_bpf 啥是seccomp 就是可以禁用掉某些系统调用&#xff0c;然后只能允许某些系统调用 #ifndef #define #endif使用 #ifndef #defin…

Neo4j恢复

主要记录从备份文件中恢复Neo4j 误删数据 为了模拟误删除场景&#xff0c;我们查询Person&#xff0c;并模拟误操作将其进行删除&#xff1b; match(p:Person) return p Step1&#xff1a; 关闭服务 Step2&#xff1a; 恢复数据 找到Neo4j的数据文件夹&#xff0c;我的安…

Linux第18步_安装“Ubuntu系统下的C语言编GCC译器”

Ubuntu系统没有提供C/C的编译环境&#xff0c;因此还需要手动安装build-essential软件包&#xff0c;它包含了 GNU 编辑器&#xff0c;GNU 调试器&#xff0c;和其他编译软件所必需的开发库和工具。本节用于重点介绍安装“Ubuntu系统下的C语言编译器GCC”和使用。 1、在安装前…

矢量,矢量化的梯度下降以及多元线性回归

一、矢量 定义&#xff1a;按照特定顺序排列的元素集合。可以被视为一维数组。 在机器学习中的作用&#xff1a; 特征表示&#xff1a;在机器学习任务中&#xff0c;输入数据通常以矢量的形式表示。例如&#xff0c;图像可以表示为像素值的矢量&#xff0c;文本可以表示为词…

CodeGPT,你的智能编码助手—CSDN出品

CodeGPT是由CSDN打造的一款生成式AI产品&#xff0c;专为开发者量身定制。 无论是在学习新技术还是在实际工作中遇到的各类计算机和开发难题&#xff0c;CodeGPT都能提供强大的支持。其涵盖的功能包括代码优化、续写、解释、提问等&#xff0c;还能生成精准的注释和创作相关内…

springCould中的gateway-从小白开始【9】

目录 1.&#x1f35f;网关是什么 2.&#x1f37f;gateway是什么 3.&#x1f95a;gateway能什么 4.&#x1f32d;核心概念 5.&#x1f9c2;工作流程 6.&#x1f9c8;实例 7.&#x1f953;gateway网关配置的方式 8.&#x1f373;配置动态路由 9.&#x1f9c7;pred…

软件开发和网络安全哪个更好找工作?

为什么今年应届毕业生找工作这么难&#xff1f; 有时间去看看张雪峰今年为什么这么火就明白了。 这么多年人才供给和需求错配的问题&#xff0c;在经济下行的今年&#xff0c;集中爆发。 供给端&#xff0c;大学生越来越多。需求端&#xff0c;低端工作大家不愿去&#xff0c;高…

springboot集成cas客户端

Background 单点登录SSO(Single Sign ON)&#xff0c;指在多个应用系统中&#xff0c;只需登录一次&#xff0c;即可在多个应用系统之间共享登录。统一身份认证CAS&#xff08;Central Authentication Service&#xff09;是SSO的开源实现&#xff0c;利用CAS实现SSO可以很大程…

年销5万的岚图没有爆款

作者 | 辰纹 来源 | 洞见新研社 3款车一年卖了5万台&#xff0c;这个销量不算多&#xff0c;可对于岚图来说&#xff0c;却很不容易&#xff0c;CEO卢放称这是“一场翻身仗”&#xff0c;在写给全体员工的“家信”中表达谢意&#xff0c;称是“大家的团结奋斗&#xff0c;驱动…

代码随想录刷题题Day28

刷题的第二十八天&#xff0c;希望自己能够不断坚持下去&#xff0c;迎来蜕变。&#x1f600;&#x1f600;&#x1f600; 刷题语言&#xff1a;C Day28 任务 ● 343. 整数拆分 ● 96.不同的二叉搜索树 1 整数拆分 343. 整数拆分 思路&#xff1a; 动态规划 &#xff08;1&a…

jenkins通过流水线自动部署项目(k8s部署)

参考&#xff1a;https://www.cnblogs.com/rb2010/p/16195443.html docker 拉取镜像到本地&#xff1a; docker pull docker.io/jenkins/jenkins:2.164配置卷挂载&#xff1a;使用nfs 参考&#xff1a;https://www.kuboard.cn/learning/k8s-intermediate/persistent/nfs.htm…

软考通过率真的低吗?

软考通过lv确实相对较低。软考通过lv低的原因主要有以下几点&#x1f447; ✅软考本身是计算机类别的考试&#xff0c;考试难度本身不小。 ✅报考没有条件限制&#xff0c;报考的人水平参差。弃考和零基础考生较多。 ✅不同的科目通过lv有所差异&#xff0c;初级的大致在30%&am…

如何将 element-ui 中的 el-select 默认展开

<el-form-item label"藕粉桂花糖糕" prop"state" required><el-selectref"mySelect"v-model"form.state"style"width: 280px"placeholder"请选择"><el-option label"藕粉" :value"…

二分查找(二)

点名 点名 某班级 n 位同学的学号为 0 ~ n-1。点名结果记录于升序数组 records。假定仅有一位同学缺席&#xff0c;请返回他的学号。 二分法思路&#xff1a;判断数组的值和对应的下标是否相等&#xff0c;将数组分为两个区间&#xff0c;不相等区间的最左端&#xff0c;就是…

java⽇志体系

⽇志体系 1.体系概述2.日志的使用1.上古时代的sout2.开创先驱的log4j3.搞事情的JUL4.应运⽽⽣的JCL5.再起波澜的logback6.再度⻘春的log4j2 本篇在jdk21下测试通过 1.体系概述 1.日志接口 JCL&#xff1a;Apache基⾦会所属的项⽬&#xff0c;是⼀套Java⽇志接⼝&#xff0c;之…

视频做成二维码查看?多格式视频二维码生成器的使用方法

现在音视频是工作和生活中经常需要使用的一种内容表现形式&#xff0c;很多人都通过这种方式来查看视频内容&#xff0c;比如产品介绍、使用说明、安装教程等。通过一个二维码就可以来承载视频内容&#xff0c;与传统的方式相比拥有更快的内容传播速度&#xff0c;简化用户获取…

[蓝桥杯学习] 线段树

学习blibli 定义 线段树是一种特殊的平衡二叉查找树&#xff0c;使用线段树&#xff0c;可以实现数据的添加、查找和删除。 树的根结点表示了一个完整的单元区间&#xff0c;左右孩子的区间是将父结点的区间进行二分&#xff0c;左右孩子的区间之和&#xff0c;就是他们的根…

三菱plc学习入门(二,三菱plc指令,触点比较,计数器,交替,四则运算,转换数据类型)

今天&#xff0c;进行总结对plc的学习&#xff0c;下面是对plc基础的学习&#xff0c;希望对读者有帮助&#xff0c;欢迎点赞&#xff0c;评论&#xff0c;收藏&#xff01;&#xff01;&#xff01; 目录 触点比较 当数据太大了的时候&#xff08;LDD32位&#xff09; CMP比…

@Transactional注解的一个很容易被忽略的错误用法

Transactional注解的一个很容易被忽略的错误用法 今日审查代码时发现对Transactional注解的一个如下用法&#xff1a; //StockController.javaAutoWired private StockService stockService//商品出库 PostMapping("/product/stock/out-stock") public Boolean pro…

输入输出流、字符字节流、NIO

1、对输入输出流、字符字节流的学习&#xff0c;以之前做的批量下载功能为例 批量下载指的是&#xff0c;将多个文件打包到zip文件中&#xff0c;然后下载该zip文件。 1.1下载网络上的文件 代码参考如下&#xff1a; import java.io.*; import java.net.URL; import java.n…