1. FFmpeg视频解码
1.1. 一、视频解码流程
1.1.1. 第一步:注册组件
av_register_all
:例如:编码器、解码器等等。
// 第一步:注册组件
av_register_all();
1.1.2. 第二步:打开封装格式
avformat_open_input
:例如:打开.mp4、.mov、.wmv文件等等。
// 第二步:打开封装格式
// 参数一:封装格式上下文
// 作用:保存整个视频信息(解码器、编码器等等...)
// 信息:码率、帧率等...
AVFormatContext* avformat_context = avformat_alloc_context();
// 参数二:视频路径
// 在我们iOS里面
// NSString* path = @"test.mov";
// const char *url = [path UTF8String]
const char *url = env->GetStringUTFChars(in_file_path, NULL);
// 参数三:指定输入的格式
// 参数四:设置默认参数
int avformat_open_input_result = avformat_open_input(&avformat_context, url, NULL, NULL);
if (avformat_open_input_result != 0){
// 安卓平台下log
__android_log_print(ANDROID_LOG_INFO, "main", "打开文件失败");
// iOS平台下log
// NSLog("打开文件失败");
// 不同的平台替换不同平台log日志
return;
}
1.1.3. 第三步:查找视频基本信息
avformat_find_stream_info
:如果是视频解码,那么查找视频流,如果是音频解码,那么就查找音频流。
// 第三步:查找视频流,拿到视频信息
// 参数一:封装格式上下文
// 参数二:指定默认配置
int avformat_find_stream_info_result = avformat_find_stream_info(avformat_context, NULL);
if (avformat_find_stream_info_result < 0){
__android_log_print(ANDROID_LOG_INFO, "main", "查找失败");
return;
}
1.1.4. 第四步:查找音频解码器
avcodec_find_decoder
:查找解码器。
1. 查找音频流索引位置
// 第四步:查找音频解码器
// 4.1 查找音频流索引位置
int av_stream_index = -1;
for (int i = 0; i < avformat_context->nb_streams; ++i) {
// 判断流类型:视频流、音频流、字母流等等...
if (avformat_context->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO){
av_stream_index = i;
break;
}
}
2. 获取解码器上下文
根据音频流索引,获取解码器上下文。
// 4.2 根据音频流索引,获取解码器上下文
AVCodecContext *avcodec_context = avformat_context->streams[av_stream_index]->codec;
3. 获得解码器ID
根据解码器上下文,获得解码器ID,然后查找解码器。
// 4.3 根据解码器上下文,获得解码器ID,然后查找解码器
AVCodec *avcodec = avcodec_find_decoder(avcodec_context->codec_id);
1.1.5. 第五步:打开解码器
avcodec_open2
:打开解码器。
// 第五步:打开解码器
int avcodec_open2_result = avcodec_open2(avcodec_context, avcodec, NULL);
if (avcodec_open2_result != 0){
__android_log_print(ANDROID_LOG_INFO, "main", "打开解码器失败");
return;
}
// 测试一下
// 打印信息
__android_log_print(ANDROID_LOG_INFO, "main", "解码器名称:%s", avcodec->name);
1.1.6. 第六步:定义类型转换参数
用于swr_convert()
,进行音频采样数据转换操作。
1. 创建音频采样数据上下文
// 第六步:定义类型转换参数
// 6.1 创建音频采样数据上下文
// 参数一:音频采样数据上下文
// 上下文:保存音频信息
SwrContext* swr_context = swr_alloc();
// 参数二:输出声道布局类型(立体声、环绕声、机器人等等...)
// 立体声
int64_t out_ch_layout = AV_CH_LAYOUT_STEREO;
// int out_ch_layout = av_get_default_channel_layout(avcodec_context->channels);
// 参数三:输出采样精度(编码)
// 例如:采样精度8位 = 1字节,采样精度16位 = 2字节
// 直接指定
// int out_sample_fmt = AV_SAMPLE_FMT_S16;
// 动态获取,保持一致
AVSampleFormat out_sample_fmt = avcodec_context->sample_fmt;
// 参数四:输出采样率(44100HZ)
int out_sample_rate = avcodec_context->sample_rate;
// 参数五:输入声道布局类型
int64_t in_ch_layout = av_get_default_channel_layout(avcodec_context->channels);
// 参数六:输入采样精度
AVSampleFormat in_sample_fmt = avcodec_context->sample_fmt;
// 参数七:输入采样率
int in_sample_rate = avcodec_context->sample_rate;
// 参数八:log_offset->log日志,从那里开始统计
int log_offset = 0;
// 参数九:log上下文
swr_alloc_set_opts(swr_context,
out_ch_layout,
out_sample_fmt,
out_sample_rate,
in_ch_layout,
in_sample_fmt,
in_sample_rate,
log_offset, NULL);
// 初始化音频采样数据上下文
swr_init(swr_context);
2. 创建音频压缩数据帧
// 6.2 创建音频压缩数据帧
// 音频压缩数据:acc格式、mp3格式
AVFrame* avframe_in = av_frame_alloc();
// 定义解码结果
int decode_result = 0;
3. 创建音频采样数据帧
// 6.3 创建音频采样数据帧
// 音频采样数据:PCM格式
// 缓冲区大小 = 采样率(44100HZ) * 采样精度(16位 = 2字节)
int MAX_AUDIO_SIZE = 44100 * 2;
uint8_t *out_buffer = (uint8_t *)av_malloc(MAX_AUDIO_SIZE);
1.1.7. 第七步:打开.pcm文件
// 第七步:打开.yuv文件
const char *outfile = env->GetStringUTFChars(out_file_path, NULL);
FILE* file_pcm = fopen(outfile, "wb+");
if (file_pcm == NULL){
__android_log_print(ANDROID_LOG_INFO, "main", "输出文件打开失败");
return;
}
1.1.8. 第八步:读取视频压缩数据帧
av_read_frame
:读取视频压缩数据帧。
// 第八步:读取视频压缩数据帧
int current_index = 0;
// 分析av_read_frame参数。
// 参数一:封装格式上下文
// 参数二:一帧压缩数据
// 如果是解码音频流,是音频压缩帧数据,例如acc、mp3
AVPacket* packet = (AVPacket*)av_malloc(sizeof(AVPacket));
while (av_read_frame(avformat_context, packet) >= 0) {
// >=:读取到了
// <0:读取错误或者读取完毕
// 是否是我们的音频流
if (packet->stream_index == av_stream_index) {
// 第九步:开始音频解码
// ...
current_index++;
__android_log_print(ANDROID_LOG_INFO, "main", "当前解码第%d帧", current_index);
}
}
1.1.9. 第九步:开始视频解码
注意:代码位置在第八步。
avcodec_send_packet
:发送一帧视频压缩数据。
avcodec_receive_frame
:解码一帧视频数据。
// 第九步:开始音频解码
// 发送一帧音频压缩数据
avcodec_send_packet(avcodec_context, packet);
// 解码一帧视频数据
decode_result = avcodec_receive_frame(avcodec_context, avframe_in);
if (decode_result == 0) {
// 音频解码成功
// 第十步:开始类型转换
// ...
// 第十一步:写入.pcm文件
// ...
}
1.1.10. 第十步:开始类型转换
注意:代码位置在第九步。
// 第十步:开始类型转换
// 将解码出来的音频数据格式统一转类型为PCM
// 参数一:音频采样数据上下文
// 参数二:输出音频采样数据
// 参数三:输出音频采样数据->大小
// 参数四:输入音频采样数据
// 参数五:输入音频采样数据->大小
swr_convert(swr_context,
&out_buffer,
MAX_AUDIO_SIZE,
(const uint8_t **)avframe_in->data,
avframe_in->nb_samples);
1.1.11. 第十一步:写入.pcm文件
注意:代码位置在第九步。
// 第十一步:写入.pcm文件
// 获取缓冲区实际存储大小
// 参数一:行大小
// 参数二:输出声道数量
int out_nb_channels = av_get_channel_layout_nb_channels(out_ch_layout);
// 参数三:输入大小
// 参数四:输出音频采样数据格式
// 参数五:字节对齐方式
int out_buffer_size = av_samples_get_buffer_size(NULL,
out_nb_channels,
avframe_in->nb_samples,
out_sample_fmt,
1);
// 写入文件
fwrite(out_buffer, 1, out_buffer_size, file_pcm);
1.1.12. 第十二步:释放内存资源,关闭解码器
// 第十二步:释放内存资源,关闭解码器
fclose(file_pcm);
av_packet_free(&packet);
swr_free(&swr_context);
av_free(out_buffer);
av_frame_free(&avframe_in);
avcodec_close(avcodec_context);
avformat_close_input(&avformat_context);
1.2. 二、新建Android音频解码工程
1.2.1. 1. 新建工程
参考之前FFmpeg集成,新建ndk工程AndroidFFmpegDecodingAudio。
1.2.2. 2. 定义java方法
寻找MainActivity:app->src->main->java->MainActivity,增加代码如下:
public native void ffmepgDecodeAudio(String inFilePath, String outFilePath);
1.2.3. 3. 定义NDK方法
增加android打印。
#include <android/log.h>
在native-lib.cpp中,导入FFmpeg头文件。
extern "C" {
// 引入头文件
// 核心库->音视频编解码库
#include <libavcodec/avcodec.h>
// 封装格式处理库
#include "libavformat/avformat.h"
// 工具库
#include "libavutil/imgutils.h"
// 视频像素数据格式库
#include "libswscale/swscale.h"
// 音频采样数据格式库
#include "libswresample/swresample.h"
}
在native-lib.cpp中新增java方法ffmepgDecodeAudio的C++实现,输入MainActivity.
就会有代码提示,选择正确ffmepgDecodeAudio方法补全代码。
extern "C"
JNIEXPORT void JNICALL
Java_com_ccq_androidffmpegdecodingaudio_MainActivity_ffmepgDecodeAudio(JNIEnv *env, jobject thiz,
jstring in_file_path,
jstring out_file_path) {
// 这里拷贝上面的音频解码流程的代码即可。
}
1.3. 三、测试Android音频解码工程
准备视频文件:test.mov
在AndroidManifest.xml增加SD卡的读写权限。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
MainActivity增加测试代码。
注意:如果打开失败,可能读写存储设备的权限被禁用。
摩托罗拉·刀锋:设置->应用和通知->高级->权限管理器->隐私相关·读写存储设备->找到应用->如果禁用,则修改为允许。
import android.os.Environment;
import java.io.File;
import java.io.IOException;
import android.util.Log;
String rootPath = Environment.getExternalStorageDirectory().getAbsolutePath();
String downloadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
String inFilePath = downloadPath.concat("/test.mov");
String outFilePath = downloadPath.concat("/test.pcm");
// 文件不存在我创建一个文件
File file = new File(outFilePath);
if (file.exists()) {
Log.i("日志:","存在");
} else {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
ffmepgDecodeAudio(inFilePath, outFilePath);
run工程代码,正确打印,同时正确生成pcm文件。
I/main: 解码器名称:acc
I/main: 当前解码第1帧
.
.
.
I/main: 当前解码第502帧
pcm文件音频播放:
ffplay -f s16le -ac 2 -ar 44100 /Users/chenchangqing/Downloads/test.pcm