【音视频】原生媒体播放器实现

这个系列文章我们来介绍一位海外工程师如何探索安卓音视频基础技术,对于想要开始学习音视频技术的朋友,这些文章是份不错的入门资料,本篇介绍原生媒体播放器。

——来自公众号“关键帧Keyframe”的分享

在本指南中,我们将探讨 AVSample 项目中的原生媒体播放器实现,该实现展示了如何使用 Android 原生 API 构建高性能的音频和视频播放器。该实现利用 OpenSL ES 进行音频播放,使用 OpenGL ES 进行视频渲染,为 Android 的高级媒体框架提供了强大的替代方案。

1、理解架构设计

原生媒体播放器实现围绕 Java/Kotlin UI 代码与原生 C++ 处理之间的清晰分离而构建。这种架构使开发者能够在利用 Android UI 框架便利性的同时,充分发挥原生代码的性能优势。

该实现包含三个主要组件:

  1. Java/Kotlin 接口层 – 为 Android 应用程序提供简单的交互 API
  2. JNI 桥接层 – 连接 Java 代码与原生 C++ 实现
  3. 原生处理层 – 处理实际的音频播放和视频渲染

2、Java/Kotlin 接口

NativeAPI 类作为 Android 应用程序与原生媒体播放器之间的主要接口。它提供了一组简单直接的方法来控制播放:

public externalfun prepare(sampleRate: Int, channel: Int, sampleFormat: Int): Int
publicexternalfun play()
publicexternalfun pause()
publicexternalfun enqueue(data: ByteArray)
publicexternalfun release()
publicexternalfun initNativeWindow(surface: Any)
publicexternalfun enqueue(data: ByteArray, w: Int, h: Int)

这些方法对应基本的媒体播放器操作:

  • 使用音频格式参数准备播放器
  • 开始和暂停播放
  • 排队音频或视频数据
  • 释放资源
  • 初始化视频输出表面

注意:有两个版本的 enqueue 方法 – 一个用于音频 PCM 数据,另一个用于视频 YUV 数据。

3、JNI 桥接层

native_player.cpp 中的 JNI 层充当 Java 和原生代码之间的桥梁。它实现了 NativeAPI 类中声明的原生方法,并将它们映射到相应的 C++ 函数:

JNINativeMethod mNATIVE_METHODS[] = {
    {"prepare",          "(III)I",                (void *) Android_JNI_prepare},
    {"play",             "()V",                   (void *) Android_JNI_play},
    {"pause",            "()V",                   (void *) Android_JNI_pause},
    {"enqueue",          "([B)V",                 (void *) Android_JNI_enqueuePCM},
    {"release",          "()V",                   (void *) Android_JNI_release},
    {"initNativeWindow", "(Ljava/lang/Object;)V", (void *) Android_JNI_initNativeWindow},
    {"enqueue",          "([BII)V",               (void *) Android_JNI_enqueueYUV}
};

这种映射展示了 Java 方法调用如何通过适当的参数转换转换为 C++ 函数调用。

4、使用 OpenSL ES 进行音频播放

原生音频播放器实现使用 OpenSL ES,这是一个免版税、跨平台、硬件加速的音频 API,专为嵌入式系统优化。SLESPlayer 类封装了所有 OpenSL ES 功能。

4.1、初始化流程

音频播放器初始化遵循以下关键步骤:

  1. 创建并初始化 OpenSL ES 引擎
  2. 创建输出混音器(音频接收端)
  3. 配置 PCM 格式参数
  4. 创建具有缓冲队列接口的音频播放器
  5. 为缓冲队列事件注册回调函数
int SLESPlayer::prepare(int sampleRate, int channels, int sampleFormat) {
    // 存储音频参数
    this->sampleFormat = sampleFormat;
    this->mSampleRate = sampleRate;
    this->mChannels = channels;
    this->out_pcm_buffer = static_cast<uint8_t *>(malloc(sampleRate * 2 * 2));
    
    // 创建 SLES 引擎
    if (checkCreate(slCreateEngine(&engineObject, 0, 0, 0, 0, 0)) != SUCCESSED) {
        LOGE("slCreateEngine error!\n");
        return ERROR;
    }
    
    // 初始化引擎并获取接口
    // ...(额外的初始化代码)
    
    // 配置 PCM 格式
    SLDataFormat_PCM pcm = {
        SL_DATAFORMAT_PCM,
        static_cast<SLuint32>(mChannels),
        getSamplesPerSec(this->mSampleRate),
        SL_PCMSAMPLEFORMAT_FIXED_16,
        SL_PCMSAMPLEFORMAT_FIXED_16,
        SL_SPEAKER_FRONT_LEFT,
        SL_BYTEORDER_LITTLEENDIAN
    };
    
    // 创建具有缓冲队列的音频播放器
    // ...(播放器创建代码)
    
    // 注册缓冲回调
    (*pcmBufferQueue)->RegisterCallback(pcmBufferQueue, pcmBufferCallBack, this);
    
    isInit = 1;
    return SUCCESSED;
}

4.2、音频数据流

音频播放使用回调机制,当 OpenSL ES 准备好处理更多采样时,它会请求音频数据:

void pcmBufferCallBack(SLAndroidSimpleBufferQueueItf bf, void *pVoid) {
    SLESPlayer *audioPlayer = static_cast<SLESPlayer *>(pVoid);
    AVData data = audioPlayer->getPCMData();
    if (data.size <= 0) {
        LOGE("GetData() size is 0");
        return;
    }
    memcpy(audioPlayer->out_pcm_buffer, data.data, data.size);
    if ((*audioPlayer->pcmBufferQueue) && audioPlayer->pcmBufferQueue) {
        (*audioPlayer->pcmBufferQueue)->Enqueue(audioPlayer->pcmBufferQueue, audioPlayer->out_pcm_buffer, data.size);
    }
    data.drop();
}

此回调函数从队列中检索音频数据,将其复制到输出缓冲区,并将其排队进行播放。getPCMData() 方法从通过 JNI 接口由 Java 层填充的帧队列中拉取数据。

5、使用 OpenGL ES 进行视频渲染

对于视频播放,该实现使用 OpenGL ES 将 YUV 帧直接渲染到 Android 表面。GLESPlayer 类处理所有视频渲染操作。

5.1、表面初始化

视频播放器使用从 UI 层获取的 Android Surface 进行初始化:

void Android_JNI_initNativeWindow(JNIEnv *jniEnv, jobject jobject1, jobject surface) {
    ANativeWindow *nativeWindow = ANativeWindow_fromSurface(jniEnv, surface);
    if (!mViewPlayer) {
        mViewPlayer = new GLESPlayer();
    }
    mViewPlayer->initView(nativeWindow);
}

此函数将 Java Surface 对象转换为原生 ANativeWindow,并使用它初始化 OpenGL ES 渲染器。

5.2、YUV 帧处理

视频播放器处理 YUV420P 帧,这是视频处理中的常见格式:

void Android_JNI_enqueueYUV(JNIEnv *jniEnv, jobject jobject1, jbyteArray data, int w, int h) {
    AVData avData;
    jbyte *yuv420p = jniEnv->GetByteArrayElements(data, 0);
    if (mViewPlayer) {
        mux.lock();
        avData.width = w;
        avData.height = h;
        avData.isAudio = 0;
        avData.format = AVTEXTURE_YUV420P;
        avData.size = jniEnv->GetArrayLength(data);

        // 分配并复制 Y、U 和 V 平面
        int ysize = w * h;
        int uvsize = ysize / 4;
        avData.datas[0] = static_cast<unsignedchar *>(malloc(ysize));
        avData.datas[1] = static_cast<unsignedchar *>(malloc(uvsize));
        avData.datas[2] = static_cast<unsignedchar *>(malloc(uvsize));
        memcpy(avData.datas[0], yuv420p, ysize);
        memcpy(avData.datas[1], yuv420p + ysize, uvsize);
        memcpy(avData.datas[2], yuv420p + ysize * 5 / 4, uvsize);
        mViewPlayer->enqueue(avData);
        mux.unlock();
    }
    jniEnv->ReleaseByteArrayElements(data, yuv420p, 0);
}

此函数从 YUV420P 帧数据中提取 Y、U 和 V 平面,并将它们传递给 OpenGL ES 渲染器进行显示。

6、集成示例

6.1、音频播放器集成

音频播放器示例展示了如何捕获音频、准备原生播放器并向其提供 PCM 数据:

private lateinitvar mNativeAPI: NativeAPI
privatevar isInitSuccess = false

overridefun init() {
    AudioCapture.init()
    mNativeAPI = NativeAPI()
    mFileInputStream = FileInputStream(AUDIO_PATH)
}

overridefun initListener() {
    AudioCapture.addRecordListener(object : AudioCapture.OnRecordListener {
        overridefun onStart(sampleRate: Int, channels: Int, sampleFormat: Int) {
            Thread {
                if (mNativeAPI.prepare(sampleRate, 1, sampleFormat) == 1) {
                    isInitSuccess = true
                    mNativeAPI.play()
                    runOnUiThread { startTime(timer) }
                }
            }.start()
        }

        overridefun onData(byteArray: ByteArray) {
            if (isInitSuccess) {
                val read = mFileInputStream.read(byteArray)
                mNativeAPI.enqueue(byteArray)
            }
        }
    })
}

此示例初始化音频捕获,使用适当的音频参数准备原生播放器,并在 PCM 数据可用时将其排队。

6.2、视频播放器集成

视频播放器示例演示了如何将 YUV 帧渲染到 Surface:

private lateinitvar mNativeAPI: NativeAPI
privatevar mWidth = 352
privatevar mHeight = 288
privatevar isPlayer = false

overridefun init() {
    mNativeAPI = NativeAPI()
    
    surface.holder.addCallback(object : SurfaceHolder.Callback {
        overridefun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
            mNativeAPI.initNativeWindow(holder!!.surface)
        }
        // ... 其他回调方法
    })
}

fun btn_play(view: View) {
    isPlayer = true
    startTime(timer)
    mFileInputStream = FileInputStream(mYUV420SPPATH)
    Thread {
        do {
            var byte = ByteArray(mWidth * mHeight * 3 / 2)
            val len = mFileInputStream!!.read(byte)
            if (len > 0) {
                mNativeAPI.enqueue(byte, mWidth, mHeight)
            }
        } while (len > 0 && isPlayer)
        mNativeAPI.release()
        mFileInputStream!!.close()
        runOnUiThread {
            cleanTime(timer)
        }
    }.start()
}

此示例使用 Surface 初始化视频播放器,从文件读取 YUV 帧,并将它们排队进行渲染。

7、性能考虑

原生媒体播放器实现提供了几个性能优势:

7.1、降低延迟

通过在原生代码中处理音频和视频,该实现最小化了 Java-Kotlin 桥接操作的开销。

7.2、硬件加速

OpenSL ES 和 OpenGL ES 分别利用硬件加速进行音频播放和视频渲染。

7.3、高效内存管理

该实现使用直接缓冲区访问和谨慎的内存管理来最小化垃圾回收的影响。

7.4、线程优化

音频和视频处理在单独的线程中进行,防止 UI 卡顿并确保流畅播放。

重要提示:在实现自己的原生媒体播放器时,访问帧队列等共享资源时,始终确保线程之间的正确同步。所提供的实现使用互斥锁来保护关键部分,这对于防止竞争条件至关重要。

8、结论

AVSample 项目中的原生媒体播放器实现展示了在 Android 上构建高性能媒体应用程序的强大方法。通过利用 OpenSL ES 处理音频和 OpenGL ES 处理视频,开发者可以创建比使用高级 API 构建的播放器延迟更低、性能更好的媒体播放器。

该实现提供了一个坚实的基础,可以通过添加音频效果、视频滤镜或支持更多媒体格式等额外功能来扩展。Java UI 代码与原生处理之间的清晰分离使其易于集成到现有的 Android 应用程序中,同时保持原生代码执行的性能优势。

学习和提升音视频开发技术,推荐你加入我们的知识星球:【关键帧的音视频开发圈】

【音视频】原生媒体播放器实现

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论