JavaCV FFmpeg AAC编码
上次成功通过FFmpeg采集麦克风的PCM数据,这次针对上一次的程序进行了改造,使用AAC编码采集后的数据。
(传送门) JavaCV FFmpeg采集麦克风PCM音频数据
采集麦克风数据是一个解码过程,而将采集后的数据进行AAC编码则是编码过程,如图:

从上图可以看出,编码过程,数据流是从AVFrame流向AVPacket,而解码过程正好相反,数据流是从AVPacket流向AVFrame。
javacpp-ffmpeg依赖:
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>${ffmpeg.version}</version>
</dependency>
FFmpeg编码的过程是解码的逆过程,不过主线流程是类似的,如下图:

基本上主要的步骤都是:
- 查找编码/解码器
- 打开编码/解码器
- 进行编码/解码
在FFmpeg的demo流程中其实还有创建流avformat_new_stream(),写入头部信息avformat_write_header()和尾部信息av_write_trailer()等操作,这里只是将PCM数据编码成AAC,所以可以暂时不需要考虑这些操作。
将采集音频流数据进行AAC编码的整体流程主要有以下几个步骤:
- 采集音频帧
- 将视音频帧重采样
- 构建AAC编码器
- 对音频帧进行编码
采集音频帧
采集音频流中的音频帧在上一次采集PCM数据的时候已经实现了,主要是从AVFormatContext中用av_read_frame()读取音频数据并进行解码(avcodec_decode_audio4()),实现代码如下:
public AVFrame grab() throws FFmpegException {
if (av_read_frame(pFormatCtx, pkt) >= 0 && pkt.stream_index() == audioIdx) {
ret = avcodec_decode_audio4(pCodecCtx, pFrame, got, pkt);
if (ret < 0) {
throw new FFmpegException(ret, "avcodec_decode_audio4 解码失败");
}
if (got[0] != 0) {
return pFrame;
}
av_packet_unref(pkt);
}
return null;
}
这样通过grab()方法就可以获取到音频流中的音频帧了。
音频帧重采样
在进行AAC编码之前,如果采集的音频帧信息格式跟编码器信息不一致则需要进行重采样,用到的是FFmpeg的SwrContext组件,下面的AudioConverter是对SwrContext封装的组件,内部实现了AVFrame的填充及SwrContext的初始化,使用方式如下:
// 1. 创建AudioConverter,指定转化格式为AV_SAMPLE_FMT_S16
AudioConverter.create(src_channel_layout, src_sample_fmt, src_sample_rate,
dst_channel_layout, AV_SAMPLE_FMT_S16, dst_sample_rate, dst_nb_samples);
// 2. 对音频帧进行转化swr_convert
converter.convert(pFrame);
AudioConverter的convert方式,实际上也是调用了SwrContext的swr_convert方法:
swr_convert(swrCtx, new PointerPointer<>(buffer), bufferLen, pFrame.data(), pFrame.nb_samples());
构建AAC编码器
进行AAC编码之前需要构建AAC编码器,根据上面的流程图利用avcodec_find_encoder()和avcodec_alloc_context3()实现编码器的创建和参数配置,最后用avcodec_open()打开编码器,完整的初始化代码如下:
public static AudioAACEncoder create(int channels, int sample_fmt, int sample_rate, Consumer<byte[]> aacBufConsumer, Map<String, String> opts) throws FFmpegException {
AudioAACEncoder a = new AudioAACEncoder();
// 查找AAC编码器
a.pCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
if (a.pCodec == null) {
throw new FFmpegException("初始化 AV_CODEC_ID_AAC 编码器失败");
}
// 初始化编码器信息
a.pCodecCtx = avcodec_alloc_context3(a.pCodec);
a.pCodecCtx.codec_id(AV_CODEC_ID_AAC);
a.pCodecCtx.codec_type(AVMEDIA_TYPE_AUDIO);
a.pCodecCtx.sample_fmt(sample_fmt);
a.pCodecCtx.sample_rate(sample_rate);
a.pCodecCtx.channel_layout(av_get_default_channel_layout(channels));
// 音频参数设置
a.pCodecCtx.channels(av_get_channel_layout_nb_channels(a.pCodecCtx.channel_layout()));
a.pCodecCtx.bit_rate(64000);
// 其他参数设置
AVDictionary dictionary = new AVDictionary();
opts.forEach((k, v) -> av_dict_set(dictionary, k, v, 0));
a.ret = avcodec_open2(a.pCodecCtx, a.pCodec, dictionary);
if (a.ret < 0) {
throw new FFmpegException(a.ret, "avcodec_open2 编码器打开失败");
}
// 填充音频帧
a.aacFrame = av_frame_alloc();
a.aacFrame.nb_samples(a.pCodecCtx.frame_size());
a.aacFrame.format(a.pCodecCtx.sample_fmt());
a.aacFrameSize = av_samples_get_buffer_size((IntPointer) null, a.pCodecCtx.channels(), //
a.pCodecCtx.frame_size(), a.pCodecCtx.sample_fmt(), 1);
// pCodecCtx.sample_fmt() = S16
// AutoCloseable
a.buffer = new BytePointer(av_malloc(a.aacFrameSize)).capacity(a.aacFrameSize);
avcodec_fill_audio_frame(a.aacFrame, a.pCodecCtx.channels(), a.pCodecCtx.sample_fmt(), a.buffer, a.aacFrameSize, 1);
a.pkt = new AVPacket();
a.pcmBuffer = new byte[DEF_PCM_BUFFER_SIZE];
a.aacBuffConsumer = aacBufConsumer;
return a;
}
这里需要特别注意的是,不是每一帧pcm数据都能编码成为一帧AAC音频帧,所以这里通过Consumer<byte[]> aacBufConsumer指定回调来消费编码完成的AAC音频帧。
对音频帧进行编码
编码器构建完成后就可以对音频帧进行编码了,入参为AVFrame,出参通过Consumer<byte[]> aacBufConsumer指定回调输出byte[],就如上面提到,不是一帧PCM音频数据就能编码成一帧AAC数据,所以这里需要就多帧pcm音频帧进行编码,并缓存未编码的pcm数据留到下一次编码。
public void encode(AVFrame avFrame) throws FFmpegException {
// 计算Pcm容量
int size = AudioUtils.toPcmFrameSize(avFrame, pCodecCtx.channels(), pCodecCtx.sample_fmt());
byte[] buff = new byte[size];
avFrame.data(0).get(buff);
System.arraycopy(buff, 0, pcmBuffer, offset, size);
offset += size;
capacity += size;
while (capacity >= aacFrameSize) {
byte[] aacBuf = new byte[aacFrameSize];
System.arraycopy(pcmBuffer, 0, aacBuf, 0, aacFrameSize);
aacFrame.data(0).put(aacBuf);
// 减去已经用于编码的buff
capacity -= aacFrameSize;
offset = capacity;
if (capacity > 0) { // 如果还有剩余,则放入buffer最前面
byte[] lBuff = new byte[capacity];
System.arraycopy(pcmBuffer, aacFrameSize, lBuff, 0, capacity);
System.arraycopy(lBuff, 0, pcmBuffer, 0, capacity);
}
ret = avcodec_encode_audio2(pCodecCtx, pkt, aacFrame, got);
if (ret < 0) {
throw new FFmpegException(ret, "avcodec_encode_audio2 音频编码失败");
}
if (got[0] != 0) {
byte[] pktBuff = new byte[pkt.size()];
pkt.data().get(pktBuff);
if (aacBuffConsumer != null) {
aacBuffConsumer.accept(pktBuff);
}
av_packet_unref(pkt);
}
}
}
最后只需要调整一下上一次的主程序,将读取pcm数据的部分,调整为将AVFrame丢进编码器,拉取byte数组即可。
public static void main(String[] args) throws FFmpegException, FileNotFoundException {
FFmpegRegister.register();
AudioGrabber a = AudioGrabber.create("External Mic (Realtek(R) Audio)");
FileOutputStream fos = new FileOutputStream(new File("s16.aac"));
AudioAACEncoder encoder = AudioAACEncoder.create(a.channels(), a.sample_fmt(), a.sample_rate(), buff -> {
try {
fos.write(buff);
} catch (IOException e) {
e.printStackTrace();
}
});
for (int i = 0; i < 100; i++) {
encoder.encode(a.grab());
}
encoder.release();
a.release();
}
最终采集编码后的AAC数据可以用VLC播放:

这里对比一下,同样的100帧pcm数据和aac数据的大小,相差还是很大的。

=========================================================
AAC编码源码可关注公众号 “HiIT青年” 发送 “ffmpeg-aac” 获取。

关注公众号,阅读更多文章。
JavaCV FFmpeg AAC编码的更多相关文章
- Ffmpeg AAC 编码错误 Input contains (near) NaN/+-Inf
Ffmpeg AAC编码 如果传入参Frame的Sample Format 为 AV_SAMPLE_FMT_S16,会出现 错误提示 Input contains (near) NaN/+-Inf,需 ...
- JavaCV FFmpeg H264编码
上次成功通过FFmpeg采集摄像头的YUV数据,这次针对上一次的程序进行了改造,使用H264编码采集后的数据. (传送门) JavaCV FFmpeg采集摄像头YUV数据 采集摄像头数据是一个解码过程 ...
- 开发RTSP 直播软件 H264 AAC 编码
上一篇对摄像头预览,拍照做了大概的介绍,现在已经可以拿到视频帧了,在加上 RTSP 实现,就是直播的雏形,当然还要加上一些 WEB 管理和手机平台的支援,就是一整套直播软件. 介绍一些基础概念:RTP ...
- ffmpeg音频编码
在弄音频采集时,需要设置缓存的大小,如果只是简单的采集和直接播放PCM数据,缓存的大小一般不影响播放和保存. 但是,如果需要使用FFMpeg音频编码,这时,音频缓存的大小必须设置av_samples_ ...
- C++实现RTMP协议发送H.264编码及AAC编码的音视频
http://www.cnblogs.com/haibindev/archive/2011/12/29/2305712.html C++实现RTMP协议发送H.264编码及AAC编码的音视频 RTMP ...
- C++实现RTMP协议发送H.264编码及AAC编码的音视频(转)
C++实现RTMP协议发送H.264编码及AAC编码的音视频(转) RTMP(Real Time Messaging Protocol)是专门用来传输音视频数据的流媒体协议,最初由Macromedia ...
- 【转】C++实现RTMP协议发送H.264编码及AAC编码的音视频
RTMP(Real Time Messaging Protocol)是专门用来传输音视频数据的流媒体协议,最初由Macromedia 公司创建,后来归Adobe公司所有,是一种私有协议,主要用来联系F ...
- JavaCV 学习(二):使用 JavaCV + FFmpeg 制作拉流播放器
一.前言 在 Android 音视频开发学习思路 中,我们不断的学习和了解音视频相关的知识,随着知识点不断的学习,我们现在应该做的事情,就是将知识点不断的串联起来.这样才能得到更深层次的领悟.通过整理 ...
- RTMP协议发送H.264编码及AAC编码的音视频,实现摄像头直播
RTMP(Real Time Messaging Protocol)是专门用来传输音视频数据的流媒体协议,最初由Macromedia 公司创建,后来归Adobe公司所有,是一种私有协议,主要用来联系F ...
随机推荐
- Python学习随笔:获取当前主机名和用户名的方法
在Python中,要获取当前主机的主机名和登录用户名很简单: 使用os.getlogin():返回当前登录用户名 使用socket.gethostname():返回当前机器主机名 代码如下: > ...
- PyQt(Python+Qt)学习随笔:Qt Designer中主窗口对象的tabShape属性
tabShape属性用于控制主窗口标签部件(Tab Widget)中的标签的形状,对应类型为QTabWidget.TabShape,有两种取值: 1.QTabWidget.Rounded:对应值为0, ...
- PHP代码审计分段讲解(1)
PHP源码来自:https://github.com/bowu678/php_bugs 快乐的暑期学习生活+1 01 extract变量覆盖 <?php $flag='xxx'; extract ...
- python 保存list,map方法
1. 保存list import numpy as np a = [1,2,3,4,5] np.save("number.npy", a) k = np.load("nu ...
- DFS,BFS 练习(深搜,广搜,图,leetcode)
https://leetcode-cn.com/problems/route-between-nodes-lcci/ 节点间通路.给定有向图,设计一个算法,找出两个节点之间是否存在一条路径. 示例1: ...
- Day1-7【Scrum 冲刺博客集合】
Day1-Day7博客链接 Day1[Scrum 冲刺博客] Day2[Scrum 冲刺博客] Day3[Scrum 冲刺博客] Day4[Scrum 冲刺博客] Day5[Scrum 冲刺博客] D ...
- vue 中 this.$options.data() 重置
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- CSP-S 2020 游记
2020.10.11 初赛了,没怎么做题,之前在网上两次初赛模拟赛 95pts / 94pts,还白嫖了一本书,感觉挺好. 去考场,中途不舒服去了厕所,回来发现有点来不及,阅读程序最后两题不会瞎蒙. ...
- AcWing 398. 交通实时查询系统
大型补档计划 题目链接 只有割点是必行点. 在任意一个点双中,都有分叉没有点交集的两条路径. 所以 v-DCC 缩点. 但是他问的是路径走到另一条路径的必行点.我蒙蔽了,发现自己对无向图双联通分量理解 ...
- tomcat-1-介绍篇
java语言分为三个体系: javase javaee,是javase的基础 一般就是指jdk javaee java的企业版本 其实是一套规范,就是用java语言做企业开发(目前看来就是开发一些动态 ...