上一篇:
给Android 开发者的 FFmpeg 教程 (三) 集成FFmpeg命令行
从这一篇开始,我们进入到FFmpeg的api使用阶段,本篇文章我们来探讨如何利用FFmpeg播放本地音频文件。
音频解码流程
下面我将具体来说明一下这些函数的作用:
- 获取媒体文件的编码格式
- avformat_open_input:打开一个输入流并读取文件头信息。
- avformat_find_stream_info:读取媒体文件的数据包以获取流信息。
这两个方法调用的目的主要是为了嗅探文件的编码格式,方便后续选择相应的解码器做解码工作。
- 初始化解码器
- avcodec_find_decoder:根据codec id 寻找对应的解码器(
AVCodec
)。 - av_parser_init:根据codec id初始化一个
AVCodecParserContext
实例。 - avcodec_alloc_context3 : 根据
AVCodec
初始化对应的上下文实例(AVCodecContext
)。 - avcodec_parameters_to_context:根据
avformat_find_stream_info
解析的参数值填充解码器上下文。
- 初始化重采样器
- swr_alloc:分配一个
SwrContext
实例。 - swr_alloc_set_opts: 设置重采样的参数,包括输入/输出音频通道格式类型,输入/输出采样率,输入/输出格式。
- swr_init:设置重采样参数后初始化上下文。
重采样器的作用是将解码出来的裸流按指定的格式重采样,使AudioTrack
能够正常的播放音频。
4.数据解码
- av_parser_parse2: 解析一个数据包。
- avcodec_send_packet:提供原始分组数据作为解码器的输入数据。
- avcodec_receive_frame:从解码器返回解码后的输出数据。
- swr_convert:对音频重采样。
解码
解码及重采样工作的jni代码如下:
#define AUDIO_INBUF_SIZE 20480
#define AUDIO_REFILL_THRESH 4096
JNIEXPORT jint JNICALL
Java_com_roj_formatfactory_FFmpegUtils_startParseNative(JNIEnv *env, jobject thiz, jstring src_path) {
const char* src = env->GetStringUTFChars(src_path,0);
AVFormatContext *formatCxt = nullptr;
AVCodecContext *codecCxt = nullptr;
/*用于存放待解码的数据,AV_INPUT_BUFFER_PADDING_SIZE的值为64,指的是在输入比特流末尾额外分配的用于解码的所需字节数。这主要是因为一些比特流读取器做了优化处理,一次性读取32位或64位数据,所以内存空间额外加上这一部分的大小。*/
uint8_t inbuf[AUDIO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
FILE* f = nullptr;
AVPacket* avPacket = nullptr;
avPacket = av_packet_alloc();
AVFrame* avFrame = nullptr;
int ret;
uint8_t *data;
size_t data_size,len;
jclass ffmpeg_utils = env->FindClass("com/roj/formatfactory/FFmpegUtils");
jmethodID onDataReceive = env->GetMethodID(ffmpeg_utils,"onDataReceive", "([B)V");
/*打开媒体文件,并查找文件的音频流,获取音频流的编码格式*/
avformat_open_input(&formatCxt,src, nullptr, nullptr);
avformat_find_stream_info(formatCxt, nullptr);
AVCodecID codec_id;
int audioIndex;
for(int i =0;i < formatCxt->nb_streams;i++) {
AVStream *avStream = formatCxt->streams[i];
if (avStream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
//在此获取音频流的编码id
codec_id = avStream->codecpar->codec_id;
audioIndex = i;
}
}
if(!codec_id){
return -1;
}
//根据音频的编码id查找解码器
const AVCodec* avCodec = avcodec_find_decoder(codec_id);
AVCodecParserContext *parse = nullptr;
SwrContext *swrContext = nullptr;
parse = av_parser_init(avCodec->id);
codecCxt = avcodec_alloc_context3(avCodec);
avcodec_parameters_to_context(codecCxt, formatCxt->streams[audioIndex]->codecpar);
int rst = 0;
rst = avcodec_open2(codecCxt,avCodec, nullptr);
if(rst != 0){
goto fail;
}
//打开媒体文件,准备读入文件数据流,这里增加了Android 10使用fd打开文件的处理逻辑
if(av_strstart(src,"file_fd:",&src)){
int fd = atoi(src);
f = fdopen(fd,"rb");
}else{
f = fopen(src,"rb");
}
//从文件中读入数据流到inbuf中
data_size = fread(inbuf,1,AUDIO_INBUF_SIZE,f);
data = inbuf;
/*初始化重采样器,这里swr_alloc_set_opt方法设置重采样后的音频格式为S16,输出声道为立体声,采样频率为44100*/
swrContext = swr_alloc();
swr_alloc_set_opts(swrContext,AV_CH_LAYOUT_STEREO,AV_SAMPLE_FMT_S16,
44100,codecCxt->channel_layout,codecCxt->sample_fmt,codecCxt->sample_rate,
0, nullptr);
swr_init(swrContext);
while(data_size > 0){
//进入解码的逻辑
if(avFrame == nullptr){
avFrame = av_frame_alloc();
}
/*将输入的数据封装到AVPacket中,其中av_parser_parse2的返回值ret表示数据流中已被封装的数据长度*/
ret = av_parser_parse2(parse,codecCxt,&avPacket->data,&avPacket->size,data,data_size,
AV_NOPTS_VALUE,AV_NOPTS_VALUE,0);
//数据指针向后移动到未使用的位置
data += ret;
//数据长度减去ret
data_size -= ret;
if(avPacket->size){
//用于存放解码后的数据
auto *out_buffer = (uint8_t *) av_malloc(2 * 44100);
//获取立体声声道数
int outChannelCount = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);
int rst = avcodec_send_packet(codecCxt,avPacket);
while(rst >= 0){
//接收解码数据
rst = avcodec_receive_frame(codecCxt,avFrame);
if(rst == AVERROR(EAGAIN) || rst < 0 || rst == AVERROR(EOF)){
break;
}
//对解码后的数据重采样,并将重采样的数据存放到out_buffer中
swr_convert(swrContext,&out_buffer,2 * 44100,(const uint8_t **)avFrame->data,avFrame->nb_samples);
//获取输出数据的buffer size
int buffer_size = av_samples_get_buffer_size(nullptr,outChannelCount,avFrame->nb_samples,
AV_SAMPLE_FMT_S16,1);
jbyteArray arr = env->NewByteArray(buffer_size);
env->SetByteArrayRegion(arr, 0, buffer_size, reinterpret_cast<const jbyte *>(out_buffer));
//将解码后的裸流输出到Java层
env->CallVoidMethod(thiz,onDataReceive,arr);
env->DeleteLocalRef(arr);
}
}
if (data_size < AUDIO_REFILL_THRESH) {
/*如果inbuf的数据长度小于AUDIO_REFILL_THRESH,继续从文件中读取数据,将数据长度补充到AUDIO_INBUF_SIZE*/
memmove(inbuf, data, data_size);
data = inbuf;
len = fread(data + data_size, 1,
AUDIO_INBUF_SIZE - data_size, f);
if (len > 0)
data_size += len;
}
}
fclose(f);
av_parser_close(parse);
fail:
avformat_close_input(&formatCxt);
avcodec_free_context(&codecCxt);
av_packet_free(&avPacket);
env->ReleaseStringUTFChars(src_path,src);
if(!avFrame){
av_frame_free(&avFrame);
}
return rst;
}
上述关键性代码的说明已通过注释给出,给出的代码虽然能够满足音频播放的需求,但仍存在优化空间。代码设计的方案是每解码一个包的数据便回调一次给Java层,这种频繁的Jni调用开销非常大,解码完整个音频会消耗比较长的时间。各位读者可自行尝试一下,对上述代码进行优化,减少jni回调的开销。
播放音频
将数据回调到java层后,我们便可以通过AudioTrack类来播放解码后的音频裸流。
- 在
FFmpegUtils.kt
中定义一个回调方法,用于给C层回调解码后的数据,具体如下所示:
object FFmpegUtils {
var onMediaPlayerListener : OnMediaPlayerListener? = null
external fun startParseNative(srcPath: String) : Int
fun onDataReceive(data: ByteArray){
Log.i(AppConstant.LOG_TAG,"收到了数据回调,size is ${data.size}")
onMediaPlayerListener?.onDataReceive(data)
}
init {
System.loadLibrary("native_lib")
}
}
interface OnMediaPlayerListener{
fun onDataReceive(data : ByteArray)
}
2. 对AudioTrack类初始化:
val attr = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setLegacyStreamType( AudioManager.STREAM_MUSIC)
.build()
val audioFormat = AudioFormat.Builder().setEncoding(
AudioFormat.ENCODING_PCM_16BIT).setSampleRate(44100)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO).build()
val audioTrack = AudioTrack(attr,audioFormat,AudioTrack.getMinBufferSize(
44100,
AudioFormat.CHANNEL_OUT_STEREO,AudioFormat.ENCODING_PCM_16BIT
),AudioTrack.MODE_STREAM,AudioManager.AUDIO_SESSION_ID_GENERATE)
注意到上面AudioFormat
里设置的参数了么, AudioFormat.ENCODING_PCM_16BIT
、44100
、AudioFormat.ENCODING_PCM_16BIT
正好和C层的重采样参数一一对应。
还需要注意的一点是,AudioTrack
的构造方法里我们传入了AudioTrack.MODE_STREAM
这个参数,表示音频的数据以流的形式不断写入到AudioTrack
中。
- 将回调的数据写入
AudioTrack
中:
FFmpegUtils.onMediaPlayerListener = object : OnMediaPlayerListener{
override fun onDataReceive(data: ByteArray) {
audioTrack?.write(data,0,data.size)
}
}
4、 播放音频:
audioTrack?.play()