1.Android音视频【十】音频mp3剪切
2.FFmpeg读取Assets资源文件
3.音视频探索(6):浅析MediaCodec工作原理
4.WebRTC 源码分析——Android 视频硬件编码
Android音视频【十】音频mp3剪切
在Android中,源码许多短视频制作应用提供了添加背景音乐的源码功能,背景音乐通常是源码从服务器下载并进行本地解码的。用户经常选择音乐的源码一部分进行使用,因此实现方案是源码下载mp3文件,解码其中的源码云豹直播源码2017部分mp3为pcm格式,然后进行进一步操作,源码如文件操作和pcm处理。源码此篇内容主要聚焦于mp3到pcm的源码解码过程,即通常理解的源码mp3剪切。后续文章将涉及如何在视频中添加背景音以及视频剪切等主题。源码上一篇文章中已经介绍了mp3的源码解码方法,本篇主要探讨如何对mp3进行部分解码。源码
Android音视频开发中,源码解码mp3文件或提取视频中的源码音视频流需要用到MediaExtractor类。MediaExtractor类在Android音视频开发中承担了提取音视频相关信息的主要任务,如将音视频文件分离为音频与视频流。要解码mp3文件的一部分音频或从视频中提取特定音视频流,就需要借助MediaExtractor类提供的API。
使用MediaExtractor的步骤主要包括:获取轨道、选择音频流、获取编码信息、指定开始时间以及进行解码。获取轨道时,音频流的格式以audio/开头,如audio/mp4a-latm或audio/mpeg,而视频流的格式以video/开头,如video/avc。获取编码信息后,可以通过MediaFormat类获取音视频相关的详细信息,如采样率、比特率、声道数量等。
选择完轨道后,需要指定开始时间从哪个时间点开始读取对应的音频流数据。时间单位为us(微秒),每毫秒等于微秒。通过MediaExtractor提供的源码博客wseek方法,可以指定从哪个时间点开始读取数据。对于音频解码,还应注意保留时间戳PTS、关键帧等信息,避免解码过程中丢失重要数据。
在解码过程中,需要关注MediaExtractor.sampleTime与时间戳的关系。对于音频,通常情况下PTS与DTS(数据时间戳)一致,直接使用无需调整。而对于视频中可能存在的b帧,需要注意数据的正确顺序。在剪切和解码数据处理时,通过比较sampleTime与预设的结束时间来判断解码是否结束。实现整个过程的代码量较少,通常不到行,主要依赖于Android提供的上层API。
此篇文章总结了音频硬解码的操作,包括如何从mp3等封装格式文件中选择和获取轨道、获取编码参数信息、解码mp3以及截取其中的数据。还涉及如何将pcm文件转换为其他格式,以及在解码过程中应注意的关键点。建议读者结合上一篇关于音视频流媒体技术的文章进行学习,以深入理解这些知识点。全部源码已上传至GitHub,供参考和学习。
FFmpeg读取Assets资源文件
在Android开发中,我们经常将原生资源文件放置于assets目录下,以便在需要时进行读取。通过API提供的resources.assets.open(filename)/openFd(filenam)方法,可以方便地获得InputStream或FileDescriptor。然而,在使用FFmpeg读取assets资源文件时,遇到了难以克服的困难。主要问题是,FFmpeg在读取媒体文件时通过传入的网站源码标签URL来判断IO协议,因此,为了使FFmpeg正确读取assets文件,我们需要选择合适的IO协议构造URL并传入avformat_open_input(...)方法,但实际上操作起来似乎问题重重。
AssetFileDescriptor提供了资源文件的有效文件标识符。是否可以通过此fd来打开媒体文件?答案是肯定的。然而,assets返回的AssetFileDescriptor带有mStartOffset,这意味着实际的有效数据需要从mStartOffset处开始读取。
在Stackoverflow上,有人提出了类似的问题:如何通过AssetFileDescriptor正确地将assets文件传递给FFmpeg使用JNI在Android中。实现方式是在调用avformat_open_input(...)方法之前手动创建AVFormatContext并设置skip_initial_bytes参数。
在使用上述方法时,发现无法正常解码mp4(mov格式)视频文件,Stackoverflow上也有类似的反馈。尽管解码失败,但可以获取视频文件的基本信息。查看FFmpeg的日志后,发现FFmpeg在解析mp4文件信息时并未出错,正确识别了ftyp、mdat、moov等关键atom,但在后续解析中正常解析了sample与chunk的映射关系(stsc),但在解码阶段报出明显的Invalid NAL unit size异常。
分析表明,assets目录下的媒体文件可能被Android进行了特殊处理。然而,从AssetFileDescriptor的官方描述中可以得知,Android并没有对assets下的文件进行特殊处理。在Android音视频开发中,我们经常使用MediaCodec的setDataSource(fd)方法传递媒体数据,MediaCodec仍然可以正常读取assets资源文件,这引发了一个疑问:在使用Android的AssetFileDescriptor时是否需要特殊的处理?MediaCodec是如何处理AssetFileDescriptor的?
搜索源码后发现,MediaExtractor#setDataSource所调用的native方法为NuMediaExtractor::setDataSource,该方法最终将fd封装为FileSource对象。FileSource中读取数据的操作使用了普通的linux内核函数read,这一过程并没有对fd做任何特殊的单机江湖源码处理。
在FFmpeg中,文件协议处理在libavformat/file.c中。比较FFmpeg与MediaCodec处理AssetFileDescriptor的逻辑,两者都使用了read函数,不同的是file_read函数中并没有seek相关的逻辑。这说明libavformat/file.c中封装的是基础的IO操作,并未包含其他无关逻辑。FFmpeg将所有的IO协议封装为URLProtocl结构体。在mov格式中的定义为:
FFmpeg与MediaCodec在读取AssetFileDescriptor时都使用了read函数,但暂时无法确定FFmpeg内部seek的逻辑是否存在问题。怀疑FFmpeg可能没有正确处理AssetFileDescriptor的startOffset。测试AVFormatContext中的skip_initial_bytes是否存在问题。
在Android应用层和Native层进行了相关测试,正常解码。应用层调用与上述相同,Native层需要设置skip_initial_bytes变量。测试结果:不能正常解码,可以获取媒体文件基本信息,日志与上述“问题”中的日志相同。这表明FFmpeg在处理mp4(mov格式)文件时,如果设置了AVFormatContext的skip_initial_bytes变量,FFmpeg将不能正确读取和解码文件。
原因在于,在调用重要函数init_input之后,avformat_open_input将文件seek到了指定的offset位置,但并没有进行其余处理逻辑。随后,avformat_open_input将调用AVInputformat中的read_header函数指针,该指针指向对应文件格式的函数,在mov格式中的read_header函数为mov_read_header。mov_read_header函数中,FFmpeg将根据read_header解析到的位置重新seek,导致从av_read_frame获得的AVPacket中的数据是错误的数据,因此给到编码器也无法正常解码。
解决方案相对简单,因为mov.c解析atom时只传递了AVIOContext,因此在AVIOContext中添加了同样的Cg影视源码skip_initial_bytes字段。在调用mov_build_index并在AVIndexEntry(采样映射关系的结构体)赋值pos时加上相应的skip_initial_bytes。这种方案已被提交到Github,并详细说明了修改的文件。基于WhatTheCodec工程编写的新demo经过测试,并没有发现其他问题。如有其他问题,欢迎交流并互相学习。
在上述Stackoverflow的提问中,有提到使用pipe协议,实际上这种方式也是可行的。但在实现过程中,发现了一些需要注意的问题。为了更具有通用性,在Native层手动创建了pipe,并将pipe的输出端fd给到FFmpeg,输入端fd由应用层持有并在IO线程中写入数据。这样,我们便可以利用pipe协议灵活地写入数据,甚至可以把内存中的视频数据直接传入FFmpeg中。
总结本文,分析了使用AssetFileDescriptor向FFmpeg传递数据时遇到的问题,这一问题实际上是由于FFmpeg在解析mov格式文件时未能正确处理skip_initial_bytes所致。分享了在使用pipe协议时遇到的问题与解决方案。这两个问题的解决可能会大大方便FFmpeg在Android上的使用。虽然MediaCodec在音视频开发的部分场景中已经渐渐取代了FFmpeg,但FFmpeg的通用性、稳定性和兼容性使之仍然可能在未来的Android音视频开发中长期存在。
音视频探索(6):浅析MediaCodec工作原理
MediaCodec类是Android平台用于访问低层多媒体编/解码器的接口,它是Android多媒体架构的一部分,通常与MediaExtractor、MediaMuxer、AudioTrack等工具配合使用,可以处理多种常见的音视频格式,包括H.、H.、AAC、3gp等。MediaCodec的工作原理是通过输入/输出缓存区同步或异步处理数据。客户端首先将要编解码的数据写入编解码器的输入缓存区,并提交给编解码器。编解码器处理后,数据转存到输出缓存区,同时收回客户端对输入缓存区的所有权。然后,客户端从编解码器的输出缓存区读取编码好的数据进行处理,读取完毕后编解码器收回客户端对输出缓存区的所有权。这一过程不断重复,直至编码器停止工作或异常退出。
在整个MediaCodec的使用过程中,会经历配置、启动、数据处理、停止、释放等步骤,对应的状态包括停止(Stopped)、执行(Executing)以及释放(Released),而Stopped状态又细分为未初始化(Uninitialized)、配置(Configured)、异常(Error),Executing状态细分为读写数据(Flushed)、运行(Running)和流结束(End-of-Stream)。当MediaCodec被创建后,它会处于未初始化状态,待设置好配置信息并调用start()方法启动后,它会进入运行状态,并可以进行数据读写操作。若在运行过程中出现错误,MediaCodec会进入Stopped状态。此时,使用reset方法来重置编解码器是必要的,否则MediaCodec所持有的资源最终会被释放。如果MediaCodec正常完成使用,可以向编解码器发送EOS指令,同时调用stop和release方法来终止编解码器的使用。
MediaCodec主要提供了createEncoderByType(String type)、createDecoderByType(String type)两个方法来创建编解码器,这两个方法需要传入一个MIME类型多媒体格式。常见的MIME类型多媒体格式有:image/jpeg、audio/amr、video/3gpp、video/h、video/avc等。此外,MediaCodec还提供了createByCodecName (String name)方法,可以使用组件的具体名称来创建编解码器,但这种方法的使用相对繁琐,且官方建议最好配合MediaCodecList使用,因为MediaCodecList记录了所有可用的编解码器。我们也可以使用MediaCodecList对传入的minmeType参数进行判断,以匹配出MediaCodec对该mineType类型的编解码器是否支持。例如,指定MIME类型为“video/avc”时,可以使用如下代码来创建H.编码器:
java
MediaCodecInfo.CodecCapabilities capabilities = MediaCodecList.getCodecCapabilities("video/avc");
if (capabilities != null) {
MediaCodec codec = MediaCodec.createByCodecName(capabilities.getName());
}
配置和启动编解码器使用MediaCodec的configure方法。这个方法首先提取MediaFormat存储的数据map,然后调用本地方法native_configure实现配置工作。在配置时,需要传入format、surface、crypto、flags参数。format是一个MediaFormat实例,它以“key-value”键值对的形式存储多媒体数据格式信息;surface用于指定解码器的数据源;crypto用于指定一个MediaCrypto对象,以便对媒体数据进行安全解密;flags指明配置的是编码器(CONFIGURE_FLAG_ENCODE)。对于H.编码器的配置,可以使用createVideoFormat("video/avc", , )方法创建“video/avc”类型的编码器的MediaFormat对象,并需要指定视频数据的宽高。如果处理音频数据,则可以调用MediaFormat的createAudioFormat(String mime, int sampleRate,int channelCount)方法。
配置完毕后,通过调用MediaCodec的start()方法启动编码器,并调用本地方法ByteBuffer[] getBuffers(input)开辟一系列输入、输出缓存区。start()方法的源码如下:
java
native_start();
ByteBuffer[] buffers = getBuffers(input);
MediaCodec支持同步(synchronous)和异步(asynchronous)两种编解码模式。同步模式下,编解码器的数据输入和输出是同步的,只有当输出数据处理完毕时,编解码器才会接收下一次输入数据。而异步模式下,输入和输出数据是异步的,编解码器不会等待输出数据处理完毕就接收下一次输入数据。这里主要介绍同步编解码模式,因为它更常用。当编解码器启动后,它会拥有输入和输出缓存区,但是这些缓存区暂时无法使用,需要通过MediaCodec的dequeueInputBuffer/dequeueOutputBuffer方法获取输入输出缓存区的授权,并通过返回的ID来操作这些缓存区。下面是一个官方提供的示例代码:
java
for (;;) {
ByteBuffer[] buffers = codec.dequeueInputBuffer();
if (buffers != null) {
// 处理输入缓存区
}
ByteBuffer[] outputBuffers = codec.dequeueOutputBuffer(new MediaCodec.BufferInfo(), );
if (outputBuffers != null) {
// 处理输出缓存区
}
}
获取编解码器的输入缓存区并写入数据。首先调用MediaCodec的dequeueInputBuffer(long timeoutUs)方法从编码器的输入缓存区集合中获取一个输入缓存区,并返回该缓存区的下标index。接着调用MediaCodec的getInputBuffer(int index),该方法返回缓存区的ByteBuffer,并将获得的ByteBuffer对象及其index存储到BufferMap对象中,以便在输入结束后释放缓存区并交还给编解码器。然后,在获得输入缓冲区后,将数据填入并使用queueInputBuffer将其提交到编解码器中处理,同时释放输入缓存区交还给编解码器。queueInputBuffer的源码如下:
java
native_queueInputBuffer(index, offset, size, presentationTimeUs, flags);
获取编解码器的输出缓存区并读出数据。与获取输入缓存区类似,MediaCodec提供了dequeueOutputBuffer和getOutputBuffer方法来获取输出缓存区。但是,在调用dequeueOutputBuffer时,还需要传入一个MediaCodec.BufferInfo对象,它记录了编解码好的数据在输出缓存区中的偏移量和大小。当调用本地方法native_dequeueOutputBuffer返回INFO_OUTPUT_BUFFERS_CHANGED时,会调用cacheBuffers方法重新获取一组输出缓存区。这意味着在使用getOutputBuffers方法(API 后被弃用,使用getOutputBuffer(index)代替)来获取输出缓存区时,需要在调用dequeueOutputBuffer时判断返回值,如果返回值为MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED,则需要重新获取输出缓存区集合。此外,还需要判断dequeueOutputBuffer的其他两个返回值:MediaCodec.INFO_TRY_AGAIN_LATER、MediaCodec.INFO_OUTPUT_FORMAT_CHANGED,以处理获取缓存区超时或输出数据格式改变的情况。最后,当输出缓存区的数据被处理完毕后,通过调用MediaCodec的releaseOutputBuffer释放输出缓存区,交还给编解码器。releaseOutputBuffer方法接收两个参数:Index、render,其中Index为输出缓存区索引,render表示当配置编码器时指定了surface,那么应该置为true,输出缓存区的数据将被传递到surface中。
WebRTC 源码分析——Android 视频硬件编码
本文深入剖析了 WebRTC 在 Android 平台上的视频硬件编码机制。首先,回顾了 MediaCodec 的概念和基础使用,这是Android中用于处理音频和视频数据的关键组件。MediaCodec 支持编码(将原始数据转换为压缩格式)和解码(将压缩数据转换回原始格式),通常与MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface等组件一起使用。
接下来,文章探讨了WebRTC 如何利用硬件编码器。通过 DefaultVideoEncoderFactory 和 HardwareVideoEncoderFactory 的交互,WebRTC 实现了 h 编码器的初始化和配置。在代码实现中,我们关注了 MediaCodec 的输入和输出缓冲区、编码器工作模式以及 MediaCodec 与 Surface 的关系,这些是理解整个编码流程的关键点。
在编码器初始化的部分,通过 DefaultVideoEncoderFactory 的 createEncoder 函数,实例化了 HardwareVideoEncoder。调用栈显示,这一过程主要在 native 端完成,通过 jni 调用 Java 端代码来获取当前设备支持的编码器信息。
编码数据送入编码器的过程涉及到 VideoEncoder 接口,WebRTC 使用 HardwareVideoEncoder 实现了这一接口,利用 MediaCodec 进行编码。通过 EglBase 和 OpenGL ES 的集成,WebRTC 将 VideoFrame 对象转换为与 MediaCodec 关联的 Surface 的纹理。这一过程确保了编码器接收到了正确的视频数据格式。
获取编码后的数据时,WebRTC 使用 MediaCodec 的同步模式进行获取。当数据可用时,通过 callback.onEncodedFrame(encodedImage, new CodecSpecificInfo()) 方法告知引擎,引擎负责进一步处理编码后的帧,如封装 RTP 包和发送到对端。
码流控制方面,WebRTC 包括拥塞控制和比特率自适应两个主要方面。当比特率发生变化时,WebRTC 会调用 VideoEncoder.setRateAllocation() 方法来更新比特率。在编码过程中,通过特定的代码逻辑来判断并调整当前的码率与所需码率是否匹配,以适应网络条件的变化。
本文以几个疑问的方式从源码角度详细解析了整个编码流程,包括从 MediaCodec 的创建和配置、视频数据的编码到编码后的数据获取和码流控制等关键步骤。通过深入分析,希望读者能够更好地理解 WebRTC 在 Android 平台上的编码技术。
为了进一步加深对 Android 音视频核心知识点的理解,推荐访问以下链接:/Ei3VPD。