给Android 开发者的 FFmpeg 教程 (四) 播放本地音频

879 阅读6分钟

上一篇:

给Android 开发者的 FFmpeg 教程 (三) 集成FFmpeg命令行

从这一篇开始,我们进入到FFmpeg的api使用阶段,本篇文章我们来探讨如何利用FFmpeg播放本地音频文件。

音频解码流程

1(1).png

下面我将具体来说明一下这些函数的作用:

  1. 获取媒体文件的编码格式
  • avformat_open_input:打开一个输入流并读取文件头信息。
  • avformat_find_stream_info:读取媒体文件的数据包以获取流信息。

这两个方法调用的目的主要是为了嗅探文件的编码格式,方便后续选择相应的解码器做解码工作。


  1. 初始化解码器
  • 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解析的参数值填充解码器上下文。

  1. 初始化重采样器
  • 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类来播放解码后的音频裸流。

  1. 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_16BIT44100AudioFormat.ENCODING_PCM_16BIT正好和C层的重采样参数一一对应。

还需要注意的一点是,AudioTrack的构造方法里我们传入了AudioTrack.MODE_STREAM这个参数,表示音频的数据以流的形式不断写入到AudioTrack中。

  1. 将回调的数据写入AudioTrack中:
FFmpegUtils.onMediaPlayerListener = object : OnMediaPlayerListener{
    override fun onDataReceive(data: ByteArray) {
        audioTrack?.write(data,0,data.size)
    }
}

4、 播放音频:

audioTrack?.play()