android使用FFmpeg解码MP4中的音频并使用AudioTrack播放

虽然网上有很多类似标题的博客,但是他们的代码运行之后一般都会有杂音或者其他的播放问题,原因也很简单,就是他们的代码太老了,网上现在一般流传的都是FFmpeg2点多的相关代码,而官网都是4点多了,所以一些方法的废弃更替导致了别人说能正常播放,而你的代码却运行异常。

废话不多说了,我使用的的ffmpeg-3.3.9编译的.so库,android-ndk-r14b。如果你也跟我的配置一样那就OK了,如果不一样没准也会有启发,但是不保证管用。

编码思路:

  1. 注册所有组件;
  2. 打开文件;
  3. 获取文件信息;
  4. 找到解码器id;
  5. 获取解码器;
  6. 打开解码器;
  7. 创建采样转换对象swrContext;
  8. 配置swrContext参数,注意输出采样率和输入采样率一致,声道布局也一致,选对PCM采样格式;
  9. init swrContext对象,一定要init,否则后续无法转换采样格式;
  10. 获取AudioTrack对象;
  11. 运行audioTrack的play方法;
  12. 循环读取帧内容;
  13. 转换每一帧的采样格式,输出数据保存在buffer中;
  14. 利用audioTrack的write方法播放buffer中的有效内容。

下面是.cpp文件的内容,注释很详细:

#include <jni.h>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <android/log.h>
#include <libswresample/swresample.h>
#include <unistd.h>

#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR, "audio",FORMAT, __VA_ARGS__);
JNIEXPORT void JNICALL
Java_nci_pactera_com_useffmpeg_MainActivity_soundPalyer(JNIEnv *env, jobject instance,
                                                        jstring srcPath_) {
    const char *srcPath = env->GetStringUTFChars(srcPath_, 0);

    //1.注册所有组件
    av_register_all();

    AVFormatContext *avFormatContext = avformat_alloc_context();
    //2.打开文件
    if (avformat_open_input(&avFormatContext, srcPath, NULL, NULL) != 0) {
        LOGE("%s", "打开文件失败");
    }

    //3.获取文件信息
    if (avformat_find_stream_info(avFormatContext, NULL)) {
        LOGE("%s", "获取文件信息失败");
    }

    //4.获取解码器id
    int codecIndex = -1;
    for (int i = 0; i < avFormatContext->nb_streams; ++i) {

        if (avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            codecIndex = i;
            break;
        }
    }
    if (codecIndex < 0) {
        LOGE("%s", "获取解码器id失败");
    }

    //5.获取解码器
    AVCodecParameters *avCodecParameters = avFormatContext->streams[codecIndex]->codecpar;
    AVCodec *avCodec = avcodec_find_decoder(avCodecParameters->codec_id);
    AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
    avcodec_parameters_to_context(avCodecContext, avCodecParameters);

    //6.打开解码器
    if (avcodec_open2(avCodecContext, avCodec, NULL) < 0) {
        LOGE("%s", "打开解码器失败");
    }

    AVFrame *srcFrame = av_frame_alloc();
    AVPacket *avPacket = av_packet_alloc();
    SwrContext *swrContext = swr_alloc();
    //设置swrContext的属性
    swr_alloc_set_opts(swrContext,
                       avCodecContext->channel_layout, //输出音道布局 例如:单声道、双声道、5-1立体声等
            //输出采样格式 这里的值代表PCM格式 网上使用的AV_SAMPLE_FMT_S16这种是格式他们的区别在于是否是平面的,但是我现在还理解不了
                       //只能说结果就是用AV_SAMPLE_FMT_S16P播放出来的声音正常,AV_SAMPLE_FMT_S16播放出来的声音会拉长声调也不对
                       AV_SAMPLE_FMT_S16P,
                       avCodecContext->sample_rate,//输出采样率 和输入一致就行,不要自己随便写个44100或者16000
                       avCodecContext->channel_layout,//输入音道布局
                       avCodecContext->sample_fmt, //输入采样格式
                       avCodecContext->sample_rate, //输入采样率
                       0, NULL);
    //一定要记得配置好属性后init,否则转换方法不好使
    swr_init(swrContext);

    //JNI调用java方法--------start
    jclass audio_main_class = (*env).GetObjectClass(instance);//获取java类对象
    //方法签名的获取的方法 cmd到D:\Useffmpeg\app\build\intermediates\classes\debug下
    //执行javap -s 包名.类名然后找到对应方法名descriptor:后面的就是方法签名
    //获取创建audioTrack的方法id
    jmethodID get_audio_mid = (*env).GetMethodID(audio_main_class, //类对象
                                                 "getAudioTrack",//方法名
                                                 "(II)Landroid/media/AudioTrack;");//方法签名
    //根据声道布局获取声道个数
    int channels = av_get_channel_layout_nb_channels(avCodecContext->channel_layout);
    //根据方法id调用创建方法获取audioTrack对象
    jobject audio_track = (*env).CallObjectMethod(instance, get_audio_mid,
                                                  avCodecContext->sample_rate, channels);
    if (NULL == audio_track) {
        LOGE("%s", "获取audioTrack对象失败");
    }
    //获取AudioTrack类对象
    jclass audio_track_class = (*env).GetObjectClass(audio_track);
    //获取AudioTrack的play方法id
    jmethodID audio_track_play_mid = (*env).GetMethodID(audio_track_class, "play", "()V");
    //调用play方法
    (*env).CallVoidMethod(audio_track, audio_track_play_mid);
    //获取write方法id
    jmethodID audio_track_write_mid = (*env).GetMethodID(audio_track_class, "write", "([BII)I");
    //JNI调用java方法--------end

    //开辟输出缓存空间
    int out_size = channels * avCodecContext->sample_rate;
    uint8_t *out = static_cast<uint8_t *>(av_malloc(out_size));

    //7.一帧一帧循环解码
    int frameCount = 0;
    while (av_read_frame(avFormatContext, avPacket) >= 0) {

        //这里注意,只获取文件中的音频类型数据,如果不判断会导致数据类型错误而出现异常
        if (avPacket->stream_index == codecIndex)
            //avcodec_send_packet和avcodec_receive_frame配合使用替代avcodec_decode_audio4读取一帧内容
            if (avcodec_send_packet(avCodecContext, avPacket) >= 0) {

                if (avcodec_receive_frame(avCodecContext, srcFrame) >= 0) {

                    //采样格式转换,返回值是每个输出声道的采样数
                    int len = swr_convert(swrContext, &out, out_size,
                                          (const uint8_t **) (srcFrame->data),
                                          srcFrame->nb_samples);
                    if (len >= 0) {
                        //格式转换后所有声道的有效采样数,
                        // 注意不要用srcFrame->linesize[0]因为这个值可能会超出有效采样范围,
                        // 从而多复制很多的错误数据,导致播放噪音
                        int bufferSize = len * channels;
                        //write方法的参数为byteArray所以创建一个
                        jbyteArray audio_data = (*env).NewByteArray(bufferSize);
                        //将PCM数据复制到byteArray中
                        (*env).SetByteArrayRegion(audio_data, 0, bufferSize,
                                                  (const jbyte *) (out));
                        //调用write方法
                        (*env).CallIntMethod(audio_track, audio_track_write_mid, audio_data, 0,
                                             bufferSize);
                        //这里注意是否局部变量,否则会溢出
                        (*env).DeleteLocalRef(audio_data);
                        LOGE("解码第%d帧", ++frameCount);
                        usleep(16 * 1000);
                    }
                }
            }

        av_packet_unref(avPacket);
    }
    av_frame_free(&srcFrame);
    av_free(out);
    swr_free(&swrContext);
    avcodec_close(avCodecContext);
    avformat_close_input(&avFormatContext);

    env->ReleaseStringUTFChars(srcPath_, srcPath);
}
}

源代码也上传了CSDN下面有链接可以自己去下载运行试试下载链接,里面还包括了从MP4解码YUV然后播放的代码。有问题欢迎留言一起学习。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值