Android FFmpeg 解码 OpenSL ES 播放音频

 在Android开发中,OpenSLES(Open Sound Library for Embedded Systems)是一个 C/C++ 音频库,提供了底层的音频功能和处理接口。它是 Android 平台上用于实现低延迟和高性能音频功能的一种选择。

本文的主线任务是描述 一个媒体文件通过 FFmpeg 解码后用 OpenSL ES 播放音频的过程

因为代码量很多,所以我直接从 Native 层开始了,看不懂的可以下载源代码配合着看(末尾)


extern "C"
JNIEXPORT void JNICALL
Java_cn_wk_opensl_1demo_MainActivity_audioPlayer(JNIEnv *env, jobject thiz, jstring dataStr) {
    const char *dataSource = env->GetStringUTFChars(dataStr, nullptr);

    pthread_create(&pid_prepare, nullptr, task_prepare, (void *) dataSource);
}

这是 JNI 函数,上层传递媒体文件全路径到 Native 层(因为 FFmpeg 读取文件需要),之后开启准备线程(就是要开始进行异步对文件做处理了)

/**
 * FFmpeg 对媒体文件 做处理
 */
void *task_prepare(void *args) {
    const char *data_source = (const char *) args;
    LOGI("data_source: %s", data_source)

    formatContext = avformat_alloc_context(); // 给 媒体上下文 开辟内存
    av_dict_set(&dictionary, "timeout", "5000000", 0); // 设置字典参数

    // TODO 打开媒体地址(如:文件路径,直播地址rtmp等)
    avformat_open_input(&formatContext, data_source, nullptr, &dictionary);
    // 释放字典(用完就释放)
    av_dict_free(&dictionary);

    // TODO 查找媒体中的音视频流的信息
    avformat_find_stream_info(formatContext, nullptr);

    // TODO 根据流信息,把 音频流、视频流 分开处理
    for (int stream_index = 0; stream_index < formatContext->nb_streams; ++stream_index) {
        AVStream *stream = formatContext->streams[stream_index];      // 获取媒体流(视频,音频)
        AVCodecParameters *parameters = stream->codecpar;             // 从流中获取 编解码 参数
        AVCodec *codec = avcodec_find_decoder(parameters->codec_id);  // 获取编解码器
        AVCodecContext *codecContext = avcodec_alloc_context3(codec); // 给 codecContext 开辟内存
        avcodec_parameters_to_context(codecContext, parameters);      // codecContext 初始化
        avcodec_open2(codecContext, codec, nullptr);          // 打开 编解码器

        // 从编解码参数中,区分流的类型,分别处理 (codec_type == 音频流/视频流/字幕流)
        if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_AUDIO) {
            LOGI("音频流")
            audio_channel = new AudioChannel(codecContext); // codecContext 才是真正干活的
            audio_channel->start(); // 开启 解码线程 和 播放线程

            pthread_create(&pid_start, nullptr, task_start, nullptr); // 数据传输线程
        } else if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_VIDEO) {
            LOGI("视频流")
        } else if (parameters->codec_type == AVMediaType::AVMEDIA_TYPE_SUBTITLE) {
            LOGI("字幕流")
        }
    }
    return nullptr; // 函数的返回值是 void* 必须返回 nullptr
}

总结:把文件路径给到 FFmpeg读取媒体文件,读取后媒体文件的流分开操作(视频搞视频的,音频搞音频的),这里音频处理封装了 AudioChannel 这个类。

需要注意的是,我为了尽可能减少代码省略了很多 FFmpeg 函数的返回值,比如 avcodec_parameters_to_context() 和 avcodec_open2() 都是有返回值的,非0为失败,可以自行对错误做处理


可以看到又开启了一个线程:task_start

/**
 * 将 AVPacket 传给 AudioChannel
 */
void *task_start(void *args) {
    while (1) {
        if (audio_channel && audio_channel->packets.size() > 100) {
            av_usleep(10 * 1000); // FFmpeg 的时间是微秒,所以这个是10毫秒
            continue;
        }
        AVPacket *packet = av_packet_alloc();             // 给 AVPacket 开辟内存
        int ret = av_read_frame(formatContext, packet);   // 从 formatContext 读帧赋值到 AVPacket
        if (!ret) {
            audio_channel->packets.insertToQueue(packet);
        } else {
            break;
        }
    }
    return nullptr;
}

工具线程:将 FFmpeg 读取到的 AVPacket 传给 AudioChannel 而已


重头戏:AudioChannel

void AudioChannel::start() {
    isPlaying = 1;

    // 队列开始工作
    packets.setWork(1);
    frames.setWork(1);

    // 音频解码线程
    pthread_create(&pid_audio_decode, nullptr, task_audio_decode, this);
    // 音频播放线程
    pthread_create(&pid_audio_play, nullptr, task_audio_play, this);
}

开启两个线程:一个解码线程,一个播放线程(FFmpeg 解码,OpenSL ES 播放)

void *task_audio_decode(void *args) {  // 很头痛,C的子线程函数必须是这个格式,所以要包装一层....
    auto *audio_channel = static_cast<AudioChannel *>(args);
    audio_channel->audio_decode();
    return nullptr;
}

/**
 * 音频解码:codecContext 把 AVPacket 解码为 AVFrame
 */
void AudioChannel::audio_decode() {
    AVPacket *pkt = nullptr;
    while (isPlaying) {
        if (isPlaying && frames.size() > 100) {
            av_usleep(10 * 1000);
            continue;
        }
        int ret = packets.getQueueAndDel(pkt);
        if (!ret) {
            continue; // 生产-消费模型,所以可能会失败,重来就行
        }
        // TODO 把 AVPacket 给 codecContext 解码
        ret = avcodec_send_packet(codecContext, pkt);

        AVFrame *frame = av_frame_alloc(); // 给 AVFrame 开辟内存

        // TODO 从 codecContext 中拿解码后的产物 AVFrame
        ret = avcodec_receive_frame(codecContext, frame);

        if (ret == AVERROR(EAGAIN))
            continue; // 音频帧可能获取失败,重新拿一次

        // 原始包 AVFrame 加入播放队列
        frames.insertToQueue(frame);
    }
}

总结:FFmpeg 将 AVPacket 解码为 AVFrame 并塞进播放队列

接下来是播放线程:

void *task_audio_play(void *args) {  // 头痛头痛
    auto *audio_channel = static_cast<AudioChannel *>(args);
    audio_channel->audio_play();
    return nullptr;
}

/**
 * 音频播放
 */
void AudioChannel::audio_play() {
    SLresult result; // 用于接收 执行成功或者失败的返回值

    // TODO 创建引擎对象并获取【引擎接口】
    slCreateEngine(&engineObject, 0, 0,
                   0, 0, 0);
    (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineInterface);

    // TODO 创建、初始化混音器
    (*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject, 0, 0, 0);
    (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);

    // TODO 创建并初始化播放器
    SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 10};
    SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, // PCM数据格式
                                   2, // 声道数
                                   SL_SAMPLINGRATE_44_1, // 采样率(每秒44100个点)
                                   SL_PCMSAMPLEFORMAT_FIXED_16, // 每秒采样样本 存放大小 16bit
                                   SL_PCMSAMPLEFORMAT_FIXED_16, // 每个样本位数 16bit
                                   SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, // 前左声道  前右声道
                                   SL_BYTEORDER_LITTLEENDIAN};
    SLDataSource audioSrc = {&loc_bufq, &format_pcm};
    SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
    SLDataSink audioSnk = {&loc_outmix, NULL};
    const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
    const SLboolean req[1] = {SL_BOOLEAN_TRUE};
    (*engineInterface)->CreateAudioPlayer(engineInterface, &bqPlayerObject, &audioSrc, &audioSnk, 1, ids, req);
    (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
    (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);

    // TODO 设置回调函数
    (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE, &bqPlayerBufferQueue);
    (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, this);

    // TODO 设置播放状态
    (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);

    // 6.手动激活回调函数
    bqPlayerCallback(bqPlayerBufferQueue, this);
}

怎么样?头痛吗,我也很头痛,代码量真的挺多的(我还把返回值去掉了的),这个 OpenSL ES 的使用真的没十年脑血栓设计不出来,其实基本上都是样板代码,大概知道什么意思就行了,关键是回调函数:

/**
 * 真正播放的函数,这个函数会一直调用
 * 关键是 SLAndroidSimpleBufferQueueItf 这个结构体,往这个结构体的队列加 PCM 数据就能播放了
 */
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *args) {
    auto *audio_channel = static_cast<AudioChannel *>(args);
    int pcm_size = audio_channel->getPCM();

    (*bq)->Enqueue(bq, audio_channel->out_buffers, pcm_size);
}

 播放音频的关键就是这个,往 Enqueue 上加 PCM 数据就能播放了


本篇文章仅实现了 FFmpeg 和 OpenGL ES 配和播放媒体文件音频的功能,其中有非常多的细节没有去完善(比如函数错误返回值的处理、内存泄漏等等),因为我为了更好的阅读和理解 FFmpeg 和 OpenSL ES,对非主线代码做了删减,所以读者可以自行添加

源代码链接:https://github.com/yinwokang/Android-OpenSLES/

猜你喜欢

转载自blog.csdn.net/weixin_47592544/article/details/131070189