在上篇文章中,我们使用的是 Android 自带的 AudioTrack 来音频播放的,现在我们要切换成 OpenSL ES 来播放,那为什么要换成这个呢 ?

一个方面是因为 OpenSL ES 性能会好一些,且都是在 Native 中处理的,而 AudioTrack 是在 Android Java 层处理的,交互涉及到 Jni,相互之间调用也比较麻烦,性能也有额外开销(基本很小)

所以本篇来使用 OpenSL ES 来播放

OpenSL ES 是什么?

OpenSL ES 是一个针对嵌入式系统的开放硬件音频加速库,也可以将其视为一套针对嵌入式平台的音频标准,全称为: Open Sound Library for Embedded Systems ,它提供了一套高性能、 低延迟的音频功能实现方法,并且实现了软硬件音频性能的跨平台部署,大大降低了上层处理音频应用的开发难度 在 Android 开发中,Google 官方从 Android 2.3 (API 9)开始,便支持了 OpenSL ES 标准 ,并且对其进行了扩展。

示例

可以看下 android 中文官网。里面也有一些demo

OpenSL ES 的一些基本概念

OpenSL ES 是基于 c 语言实现的,但其提供的接口是采用面向对象的方式实现,OpenSL ES 的大多数 API 是通过对象来调用的。一般都会通过 object 来调用 Realize 实例化对象,然后通过 GetInterface 获取对应接口的能力,一般都是这样配套使用的

对象和接口概念

Object 和 Interface OpenSL ES 中的两大基本概念,可以类比为 Java 中的对象和接口。

  1. Objects (对象):
    • Objects 是 OpenSL ES 中表示音频对象或组件的实例。每个 Object 都实现了一个或多个接口 (Interfaces), 并且具有自己的生命周期
  2. 常见的 Objects 包括:

    • Player Object: 用于音频播放
    • AudioRecorder Object: 用于音频录制
    • OutputMix Object: 用于混合多个音频流输出
    • MIDI Object: 用于 MIDI 音乐播放
  3. Interfaces (接口):

    • Interfaces 定义了 Objects 所支持的功能集合。每个 Interface 由一组预定义的方法(函数)和数据结构组成,Objects 需要实现这些接口才能提供相应的功能
  4. 常见的 Interfaces 包括:

    • Player Interface: 定义了音频播放的基本功能, 如播放、暂停、停止等
    • AudioRecorder Interface: 定义了音频录制的基本功能
    • EngineInterface: 定义了 OpenSL ES 引擎的配置和控制功能
    • MetadataTraversal Interface: 定义了访问和操作元数据的功能

使用流程

先简单介绍一下整体的流程,本文还是使用 ffmpeg 来进行解码,只是将解码好的数据给 OpenSL ES,有一些流程基本和 ffmpeg + audioTrack 的使用区别不大,不过本文使用了多线程的技术,一边加载压缩数据,一边进行播放, 首先还是在 Java 层级,将 url 传入到 native 层,区别是这里是异步加载了,等待加载好了,然后再去调用 play 方法,非同步行为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Player {
    companion object {
        init { System.loadLibrary("player")  } // 加载 so 库
    }
    private var url: String = ""
		
	// ......

    fun setDataSource(url: String) {   this.url = url  }

    fun play() {  nPlay(url)  }

    private external fun nPlay(url: String)

    fun prepare() {
        if (url.isBlank()) error("url is null")

        nPrepare(url) // 准备
    }

    private external fun nPrepare(url: String)

    fun prepareAsync() {
        if (url.isBlank()) error("url is null")

        nPrepareAsync(url) // 准备
    }

    private external fun nPrepareAsync(url: String)
}

// MainActivity 类
class MainActivity : ComponentActivity() {

    private val play by lazy { Player() }

    private val mMusicFile: File = File(Environment.getExternalStorageDirectory(), "input.mp3")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {

            XXPermissions.with(this)
                .permission(Permission.MANAGE_EXTERNAL_STORAGE)
                .request { permissions, allGranted -> }

            PlayerTheme {
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {

                    play.setDataSource(mMusicFile.absolutePath)


                    play.setOnPrepareListener(object : MediaPreparedListener {
                        override fun onPrepared() {
                            play.play()
                        }
                    })

                    play.prepareAsync()
                }
            }
        }
    }
}

然后到了 native 层

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/** 异步去准备 **/
extern "C"
JNIEXPORT void JNICALL
Java_com_fang_player_Player_nPrepareAsync(JNIEnv *env, jobject instance, jstring url_) {
    //获取 java 中的字符串,jstring 转换回 c 中的字符来使用
    const char *url = env->GetStringUTFChars(url_, nullptr);

    if (fFmpegHelper == nullptr) {
        myJniCall = new MyJNICall(pJavaVM, env, instance); 
        fFmpegHelper = new FFmpegHelper(myJniCall, url); // 注意这里的方法执行完成了,栈内存就释放了,url 就会被释放掉,所以内部如果使用到 url 的话,需要重新分配内存
        fFmpegHelper->prepareAsync(); // 准备
    }

    env->ReleaseStringUTFChars(url_, url);
}

这里创建了 myJniCall 和 fFmpegHelper 对象,其两个对象的构造中都只是储存了一些引用,并没有太多逻辑,不再展示,然后调用的 prepareAsync 方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void *threadPrepare(void *context) {
    auto *pFFmpeg = (FFmpegHelper *) context;
    pFFmpeg->prepare(THREAD_CHILD);
    return nullptr;
}

void FFmpegHelper::prepareAsync() {
    // 创建一个线程去播放,多线程编解码边播放
    pthread_t prepareThreadT;
    pthread_create(&prepareThreadT, nullptr, threadPrepare, this);
    pthread_detach(prepareThreadT);
}

然后在 prepareAsync 方法中,创建了线程,在子线程中,创建了 ffmpeg 相关实例

初始化 FFmpeg

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void FFmpegHelper::prepare(ThreadMode threadMode) {
    av_register_all(); // 初始化所有的辩解码器
    avformat_network_init();

    int formatOpenInputRes = 0;
    int formatFindStreamInfoRes = 0;

    formatOpenInputRes = avformat_open_input(&pFormatContext, url, nullptr, nullptr);

    // 第一步
    if (formatOpenInputRes != 0) { // 打开读取文件失败
        // 失败,需要告知 java 层,需要释放流资源
        LOGE("format open input error: %s", av_err2str(formatOpenInputRes));
        callPlayerJniError(threadMode, formatOpenInputRes, av_err2str(formatOpenInputRes));
        return; // 因为 return 了,之前打开的相关操作,需要释放
    }

    // 第二部,读取流信息,并将读取到的信息,封装在 pFormatContext 中
    formatFindStreamInfoRes = avformat_find_stream_info(pFormatContext, NULL);
    if (formatFindStreamInfoRes < 0) {
        LOGE("format find stream info error: %s", av_err2str(formatFindStreamInfoRes));
        callPlayerJniError(threadMode, formatFindStreamInfoRes, av_err2str(formatFindStreamInfoRes));
        return;
    }

    // 第三部:查找音频流的 index, 为什么要找 ?因为一个文件中,含有多一个流
    int audioStreamIndex = av_find_best_stream(pFormatContext, AVMediaType::AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
    if (audioStreamIndex < 0) {
        LOGE("format audio stream error: %d, %s", audioStreamIndex, av_err2str(audioStreamIndex));
        callPlayerJniError(threadMode, audioStreamIndex, av_err2str(audioStreamIndex));
        return;
    }

    pAudio = new AudioManager(audioStreamIndex, pJniCall, pFormatContext);
    pAudio->analysisStream(threadMode, pFormatContext->streams);

    // ---------- 重采样 end ----------
    // 回调到 Java 告诉他准备好了
    pJniCall->callPlayerPrepared(threadMode);
}

然后最后一步,通过 Jni 的调用,告知 Java 层准备好了,在上面的代码中,我们可以知道,会调用 play 方法,可以开始播放了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void AudioManager::play() {
    // 多线程,边解码边播放
    // 一个线程不断的加载数据,一个线程不断的读取解码播放
    // 加载压缩数据
    pthread_t readPacketThreadT;
    pthread_create(&readPacketThreadT, nullptr, threadReadPacket, this);
    pthread_detach(readPacketThreadT);


    // 解码数据
    pthread_t playThreadT;
    pthread_create(&playThreadT, nullptr, threadPlay, this);
    pthread_detach(playThreadT);
}

这里从代码就可以看的出来,创建了两个线程,一个线程不断的加载数据,一个线程不断的读取解码播放,为什么这么做呢?假设说我们只有一个线程去处理的话,要做的事情是压缩数据,然后解码数据去播放,逻辑是加载一帧的数据,然后就去解码播放,同步行为,压力较大,可能会造成,播放端的视频已经播放完成了,而可能会遇到加载数据可能并没有准备好数据的情况(网络拥堵)。声音卡顿,所以我们需要使用两个线程,加载数据和解码,各自的线程去处理,这样压力就会小很多,甚至是加载的数据可以提前缓存一些,需要快进,播放流畅性都会更好。所以我们会使用到队列去缓存数据

缓存队列

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
PacketQueue::PacketQueue() {
    // 创建对象
    pPacketQueue = new std::queue<AVPacket *>;
    pthread_mutex_init(&packetMutex, nullptr);
    pthread_cond_init(&packetCond, nullptr);
}

PacketQueue::~PacketQueue() {
    if (pPacketQueue != nullptr) {
        clear();
        delete pPacketQueue;
        pPacketQueue = nullptr;
    }
    pthread_mutex_destroy(&packetMutex);
    pthread_cond_destroy(&packetCond);
}

void PacketQueue::push(AVPacket *packet) {
    pthread_mutex_lock(&packetMutex);
    pPacketQueue->push(packet);

    pthread_cond_signal(&packetCond); // 有数据了,通知另外一个线程可以读取, 因为 pop 是阻塞的,可能获取为空的时候,阻塞住了

    pthread_mutex_unlock(&packetMutex);
}

AVPacket *PacketQueue::pop() {
    AVPacket *pPacket;

    pthread_mutex_lock(&packetMutex);
    while (pPacketQueue->empty()) { // 如果为空,阻塞在这里
        pthread_cond_wait(&packetCond, &packetMutex);
    }

    pPacket = pPacketQueue->front(); // 取队列最前面的那个流数据
    // 出队
    pPacketQueue->pop();

    pthread_mutex_unlock(&packetMutex);

    return pPacket;
}

void PacketQueue::clear() {
    pthread_mutex_lock(&packetMutex); // 操作队列数据,防止其他线程在别的地方操作

    if (pPacketQueue) {
        while (!pPacketQueue->empty()) {
            AVPacket* packet = pPacketQueue->front();
            av_packet_free(&packet); // 释放
            pPacketQueue->pop(); // 出队
        }
    }

    pthread_mutex_unlock(&packetMutex);
}

int PacketQueue::getSize() const {
    return pPacketQueue->size();
}

注意这里的缓存队列是利用 queue 来缓存 AVPacket 数据的,使用到了 packetMutex 和 packetCond,其实就像 Java 中的 Lock 锁,还有唤醒锁的行为,在对数据进行增删改查的时候,防止多线程去同时操作数据而发生数据错乱了!

子线程加载压缩数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void *threadReadPacket(void *context) { // 加载压缩数据
    auto *audio = (AudioManager *) context;
    const int MAX_QUEUE_SIZE = 600; // 设置最大队列大小

    while (audio->pPlayerStatus != nullptr && !audio->pPlayerStatus->isExit) {
        // 检查队列大小
        if (audio->pPacketQueue->getSize() >= MAX_QUEUE_SIZE) { // 固定大小不合适,这里只是一种思路,生产环境不一定是这样
            LOGE("队列已满,暂停加载");
            usleep(100000); // 暂停100毫秒
            continue;
        }

        // 开始解码
        AVPacket *pPacket = av_packet_alloc();
        if (av_read_frame(audio->pFormatContext, pPacket) >= 0) {
            if (pPacket->stream_index == audio->audioStreamIndex) { // 判断是否是音频流,因为一个文件中,可能会有多个流
                audio->pPacketQueue->push(pPacket);
            } else {
                // 1. 解引用数据 data , 2. 销毁 pPacket 结构体内存  3. pPacket = NULL
                av_packet_free(&pPacket);
            }
        } else {
            // 1. 解引用数据 data , 2. 销毁 pPacket 结构体内存  3. pPacket = NULL
            av_packet_free(&pPacket);
        }
    }
    return nullptr;
}

这里的代码就是使用 ffmpeg 通过 av_read_frame 去加载 AVPacket 数据了,然后将加载好的数据放在 pPacketQueue 队列当中,后续如果要播放的话,就去这个队列里面取,这里做了一个逻辑是假定了一个队列的阈值,当缓存了一定的数量后,就睡眠一会不再去加载压缩数据了,一个原因是我们播放是比较慢的,而且我们不一定能够播放完整个音频,如果不设置的话,这个线程一直不断的在加载,可能整个音频都已经被你加载完了,如果是使用流量的话,那耗费的流量比较多了,再一个是耗费 CPU 资源,所以我们只提前缓存一点点,能够保证流畅播放就行

这里只是在测试环境中,一种很粗陋的优化,实际在生产环境中,问了一下 OpenAi 可能会有更好的方案,比如

  1. 自适应缓冲区大小
    • 许多现代流媒体系统使用自适应缓冲区大小。这意味着缓冲区大小会根据网络条件、设备性能和用户行为动态调整
  2. 时间基础的缓冲
    • 与其使用固定数量的数据包,更常见的做法是基于时间来设置缓冲区。例如,缓冲30秒或60秒的内容。这样可以更好地适应不同比特率的流
  3. 分段加载
    • 大多数流媒体协议(如HLS、DASH)使用分段加载。内容被分割成小段(通常是几秒到几分钟长),客户端一次只加载几个段
  4. 预测性缓冲
    • 一些高级系统会预测用户行为。例如,如果用户经常跳过片头,系统可能会减少对片头的缓冲
  5. 多级缓冲
    • 实现一个小的前端缓冲区用于即时播放,和一个较大的后端缓冲区用于平滑播放
  6. 网络条件适应
    • 根据网络带宽和延迟调整缓冲策略。在网络条件好的时候加载更多,条件差时减少加载

更加具体的优化方案就不展开了,到了用到的时候再说吧,然后是另外一个线程去解码了

子线程解码数据播放

以上的逻辑我们使用了 ffmpeg 解封装了数据,到了这一步我们只需要将数据解码,然后丢给 OpenSL ES 就可以了,所以我们还有 2 个流程,初始化 OpenSL ES 和使用 ffmpeg 解码数据,首先看下 OpenSL ES 的初始化

OpenSL ES 的创建初始化流程,在网上找了一张图,基本上是根据如下图流程进行创建,涉及到代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 初始化 OpenSLES
void AudioManager::initCrateOpenSLES() {
    // 3.1 创建引擎接口对象
    SLObjectItf engineObject = nullptr;
    SLEngineItf engineEngine;
    // 创建引擎对象
    slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr);
    //  实例化
    (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    // // 获取引擎对象接口
    (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
    // 3.2 设置混音器
    static SLObjectItf outputMixObject = nullptr;
    const SLInterfaceID ids[1] = {SL_IID_ENVIRONMENTALREVERB};
    const SLboolean req[1] = {SL_BOOLEAN_FALSE};
    (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, ids, req);
    // 初始化混音器
    (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
    SLEnvironmentalReverbItf outputMixEnvironmentalReverb = nullptr;
    (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,
                                     &outputMixEnvironmentalReverb);
    SLEnvironmentalReverbSettings reverbSettings = SL_I3DL2_ENVIRONMENT_PRESET_STONECORRIDOR;
    (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(outputMixEnvironmentalReverb,
                                                                      &reverbSettings);
    // 3.3 创建播放器
    SLObjectItf pPlayer = nullptr;
    SLPlayItf pPlayItf = nullptr;
    // 数据源简单缓冲队列定位器
    SLDataLocator_AndroidSimpleBufferQueue simpleBufferQueue = {
            SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
    SLDataFormat_PCM formatPcm = {
            SL_DATAFORMAT_PCM, // 格式类型
            2,  // 通道数
            SL_SAMPLINGRATE_44_1,  //采样率
            SL_PCMSAMPLEFORMAT_FIXED_16, // 位宽
            SL_PCMSAMPLEFORMAT_FIXED_16,
            SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, // 通道屏蔽
            SL_BYTEORDER_LITTLEENDIAN}; // 字节顺序
    // 数据源
    SLDataSource audioSrc = {&simpleBufferQueue, &formatPcm};
    // 针对数据接收器的输出混合定位器(混音器)
    SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX /*定位器类型*/, outputMixObject /*输出混合*/};
    // 输出
    SLDataSink audioSnk = {&outputMix, nullptr};
    //需要的接口 操作队列的接口
//    SL_IID_BUFFERQUEUE表示缓冲区队列接口,用于传入待播放的音频数据
//    SL_IID_VOLUME表示音量控制接口,用于设置播放音量
//    SL_IID_PLAYBACKRATE表示播放速率接口,用于控制播放速度
    SLInterfaceID interfaceIds[3] = {SL_IID_BUFFERQUEUE, SL_IID_VOLUME, SL_IID_PLAYBACKRATE}; // 如果想要环绕立体声音,可以增加 SL_IID_SURROUND
    // 定义了一个包含3个布尔值的数组,表示上面三个接口是否为必需接口。这里全部设置为SL_BOOLEAN_TRUE,表示这三个接口都是必需的
    SLboolean interfaceRequired[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; // 也可以设置 false,设置 false 的话,表示的意思就是,设置false即使创建失败了,也可以继续创建播放器对象
    // 创建播放器
    (*engineEngine)->CreateAudioPlayer(engineEngine, &pPlayer, &audioSrc, &audioSnk, 3,
                                       interfaceIds, interfaceRequired);
    // 实现(初始化)刚创建的播放器对象。
    (*pPlayer)->Realize(pPlayer, SL_BOOLEAN_FALSE);
    // 获取播放器对象的Play接口,用于控制播放器的播放、暂停等操作
    (*pPlayer)->GetInterface(pPlayer, SL_IID_PLAY, &pPlayItf);
    // 3.4 设置缓存队列和回调函数
    SLAndroidSimpleBufferQueueItf playerBufferQueue;
    (*pPlayer)->GetInterface(pPlayer, SL_IID_BUFFERQUEUE, &playerBufferQueue);
    (*playerBufferQueue)->RegisterCallback(playerBufferQueue, playerCallback, this); 
    // 3.5 设置播放状态
    (*pPlayItf)->SetPlayState(pPlayItf, SL_PLAYSTATE_PLAYING);
    // 3.6 调用回调函数 - 手动激活回调函数
    playerCallback(playerBufferQueue, this); 
}

这里初始化的就是最先开始提到的,每个 object 需要使用 Realize 实例化,然后通过 GetInterface 获取接口的能力,最后需要注意的是需要手动调用 playerCallback 激活回调函数,这里好像是比较奇怪了,一般 Java 只需要设置回调函数,好像并没有手动激活这么一说

回调函数

1
2
3
4
5
6
7
8
void playerCallback(SLAndroidSimpleBufferQueueItf caller, void *pContext) { // void *pContext 是一个指针,传入的就是  AudioManager
    if (pContext != nullptr) {
        auto *pFFmepg = (AudioManager *) pContext;
        int dataSize = pFFmepg->resampleAudio(); // 解码,去播放,dataSize 是一帧的大小

        (*caller)->Enqueue(caller, pFFmepg->resampleOutBuffer, dataSize); // 去播放
    }
}

手动激活回调函数后,就会被调用到这个 playerCallback 方法中,这个方法中就是剩下的解码了,通过 resampleAudio 方法去解码,然后得到的数据通过 caller 调用 Enqueue() 方法,这个就是丢给了 OpenSL ES 了,注意这里当播放完成后,会再次回调这个方法解码播放,一直这样重复,所以它能够播放,然后看看解码的方法做了什么

解码音频数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int AudioManager::resampleAudio() {
    int dataSize = 0;
    AVPacket *pPacket = nullptr;// 压缩数据
    AVFrame *pFrame = av_frame_alloc(); // 解码出来的数据

    while (pPlayerStatus != nullptr && !pPlayerStatus->isExit) {
        pPacket = pPacketQueue->pop(); // 取压缩的数据,去解码
        int codecSendPacketRes = avcodec_send_packet(pCodecContext, pPacket); // 将压缩数据发送给解码器
        if (codecSendPacketRes == 0) {
            int codecReceiveFrameRes = avcodec_receive_frame(pCodecContext, pFrame);
            if (codecReceiveFrameRes == 0) {

                // 第一个参数: 分配好的 SwrContext 上下文,它包含了转换操作所需的所有参数(如输入输出的声道布局、采样格式和采样率
                // 第二个参数: 输出缓冲区,用来存储转换后的音频数据。如果是打包音频数据,则只需要设置第一个缓冲区
                // 第三个参数:每个声道中可用的输出空间,单位为样本数。例如,如果你希望输出 1024 个样本,你将设置 out_count 为 1024。
                // 第四个参数:输入缓冲区,包含待转换的音频数据。如果打包音频数据,则只需要设置第一个缓冲区。
                // 第五个参数:每个声道中可用的输入样本数。这代表待转换音频数据的数量。
                // 返回的是实际转换的输出样本数
                // 计算一帧的大小: 总大小=样本数×每个样本的字节数×通道数
                dataSize = swr_convert(swrContext, &resampleOutBuffer, pFrame->nb_samples,
                                       (const uint8_t **) pFrame->data, pFrame->nb_samples);

                dataSize = dataSize * 2 * 2; // 一帧的大小, 解释:2 * 2 表示的是 每个样本的字节数×通道数(或者叫声道数,比如说立体声就是2)

                break;
            }
        }
        // 解引用
        av_packet_unref(pPacket);
        av_frame_unref(pFrame);
    }

    // 1. 解引用数据 data , 2. 销毁 pPacket 结构体内存  3. pPacket = NULL
    av_packet_free(&pPacket);
    av_frame_free(&pFrame);
    return dataSize;
}

这里的步骤就是使用 ffmpeg 去解码数据,然后重采样,至于重采样,可以看下上篇文章的解释,然后计算好的 dataSize 返回给 Enqueue,这里的 dataSize 是一帧的大小,如果少了或者多了,可能会导致播放不完整,最后丢给 OpenSL ES 就能够正常播放了。好了,整体的核心的代码就是这些,其他的一些相关代码如果有需要多可以看下 github 项目。

参考: https://cloud.tencent.com/developer/article/1832836 https://blog.csdn.net/ta893115871/article/details/117650780