SkyPlayer:移动端 FFmpeg 播放器深度实践

8 阅读12分钟

项目介绍

什么是 SkyPlayer?

SkyPlayer 是一个 移动端 音视频播放器(暂支持Android),基于 FFmpeg 8.0 官方 ffplay 深度改造。
🔗 项目地址SkyMediaPlayer

项目特点

1. 基于 ffplay 核心改造

  • FFmpeg 8.0:使用 FFmpeg 最新稳定版本,获得最新的格式支持和性能优化
  • 保留经典架构:完整保留 ffplay.c 播放引擎
  • 经过验证的稳定性:ffplay 作为 FFmpeg 官方播放器示例,经过数十年实战验证
  • 完整的播放流程:从解封装、解码、音画同步到渲染的全链路实现

2. 针对 Android 平台深度优化

  • OpenGL ES 2.0 渲染:5 种独立优化的渲染器,支持 YUV420P/422P、NV12/21、RGBA
  • OpenSL ES 音频:超低延迟音频输出(< 20ms),远优于 AudioTrack
  • 安全的 JNI 设计:线程本地存储(TLS)、自动 attach/detach、弱引用防泄漏,避免常见的 JNI 内存泄漏和崩溃问题

3. 工程化设计

  • 线程安全:完整的多线程安全设计和内存管理,避免并发问题
  • 错误处理:完善的异常处理和资源释放机制,防止内存泄漏
  • 消息队列:异步事件处理,解耦播放控制和状态通知
  • RAII 机制:使用现代 C++ 的 RAII 模式管理资源生命周期

4. 清晰的架构设计

  • 分层架构:Java、JNI、Native、FFmpeg 四层清晰分离
  • C/C++ 混合编程:使用现代 C++ 封装 C 语言的 ffplay.c,解决 C→C++ 和 C++→C 的双向调用问题,实现调用隔离和扩展性
  • JNI 安全性:完整的线程安全机制,避免多线程环境下的崩溃和内存泄漏
  • 易于理解:每个模块职责明确,代码注释完整
  • 便于扩展:基于接口设计,支持自定义渲染器和音频输出

5. 优秀的学习价值

  • 完整的技术栈:涵盖 FFmpeg、JNI、OpenGL ES、OpenSL ES
  • 可运行的示例:app 模块提供完整的 Demo
  • 教学级代码:适合学习音视频开发的完整实现

目前已支持本地文件播放,ios端、在线播放、直播等功能持续迭代中。

技术架构

整体架构图

SkyPlayer 采用清晰的分层架构设计,从上到下分为四层,并通过三层调用链路实现跨语言通信:

┌─────────────────────────────────────────────────────────────────────┐
│                        Java/Kotlin Layer                            │
│                     (SkyMediaPlayer.kt)                             │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ • 接口设计:兼容 MediaPlayer API                               │  │
│  │ • 事件处理:MediaEventHandler                                 │  │
│  │ • 生命周期管理:Surface、监听器等                              │  │
│  └───────────────────────────────────────────────────────────────┘  │
└──────────────────────────┬──────────────────────────────────────────┘
                           │ ⬇ Kotlin → Native (JNI 调用)
                           │ • _native_setup() / _setDataSource()
                           │ • _prepareAsync() / _start() / _pause()
                           │ • _seekTo() / _getCurrentPosition()
                           │ ⬆ Native → Kotlin (事件回调)
                           │ • postEventFromNative()
┌──────────────────────────▼──────────────────────────────────────────┐
│                          JNI Layer                                  │
│                   (skymediaplayer_jni.cpp)                          │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ • 方法注册:JNI_OnLoad                                         │  │
│  │ • 线程安全:pthread TLS 管理 JNIEnv                            │  │
│  │ • 对象管理:弱全局引用防泄漏                                    │  │
│  │ • 事件回调:postEventToJava()                                  │  │
│  └───────────────────────────────────────────────────────────────┘  │
└──────────────────────────┬──────────────────────────────────────────┘
                           │ C++ 对象调用
┌──────────────────────────▼──────────────────────────────────────────┐
│                        Native Layer (C++)                           │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │              播放器核心 (skymediaplayer.cpp)                   │  │
│  │  • 封装 ffplay 播放引擎                                        │  │
│  │  • 播放控制:start/pause/seek                                  │  │
│  │  • 状态管理:prepared/playing/paused                           │  │
│  └───────────────────────────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │         视频渲染器 (SkyVideoOutHandler)                        │  │
│  │  ┌─────────────────────────────────────────────────────────┐  │  │
│  │  │ SkyEGL2Renderer (skyrenderer.cpp)                       │  │  │
│  │  │  • EGL 初始化和配置                                      │  │  │
│  │  │  • OpenGL ES 2.0 上下文管理                              │  │  │
│  │  │  • 5 种像素格式渲染器:                                  │  │  │
│  │  │    - YUV420P / YUV422P / NV12 / NV21 / RGBA            │  │  │
│  │  └─────────────────────────────────────────────────────────┘  │  │
│  │  ┌─────────────────────────────────────────────────────────┐  │  │
│  │  │ Android 平台对接                                         │  │  │
│  │  │  • ANativeWindow (从 Surface 获取)                       │  │  │
│  │  │  • EGL Surface 创建和绑定                                │  │  │
│  │  │  • 硬件加速渲染到屏幕                                     │  │  │
│  │  └─────────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │         音频输出 (SkyAudioOutHandler)                          │  │
│  │  ┌─────────────────────────────────────────────────────────┐  │  │
│  │  │ SkySLESAudioOut (skyaudio.cpp)                          │  │  │
│  │  │  • OpenSL ES 引擎创建和初始化                            │  │  │
│  │  │  • 输出混音器配置                                        │  │  │
│  │  │  • 音频播放器创建                                        │  │  │
│  │  │  • 缓冲区队列管理                                        │  │  │
│  │  │  • 低延迟播放 (< 20ms)                                   │  │  │
│  │  └─────────────────────────────────────────────────────────┘  │  │
│  │  ┌─────────────────────────────────────────────────────────┐  │  │
│  │  │ Android 平台对接                                         │  │  │
│  │  │  • OpenSL ES API                                        │  │  │
│  │  │  • Android 音频框架                                      │  │  │
│  │  │  • 硬件音频输出                                          │  │  │
│  │  └─────────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │      消息队列 (sky_msg_queue.cpp)                              │  │
│  │  • 异步事件处理                                                │  │
│  │  • 线程间通信                                                  │  │
│  └───────────────────────────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │      C/C++ 接口桥接 (skymediaplayer_interface.h)               │  │
│  │  • sky_display_image() - 视频帧显示                            │  │
│  │  • sky_open_audio() - 音频设备打开                             │  │
│  │  • sky_pause_audio() - 音频暂停控制                            │  │
│  │  • sky_post_message() - 消息传递                               │  │
│  └───────────────────────────────────────────────────────────────┘  │
└──────────────────────────┬──────────────────────────────────────────┘
                           │ ⬇ C++ → C (直接调用 ffplay.h 接口)
                           │ • stream_open() / stream_close()
                           │ • toggle_pause() / stream_seek()
                           │ ⬆ C → C++ (通过 skymediaplayer_interface.h)
                           │ • sky_display_image() / sky_open_audio()
                           │ • sky_post_message()
┌──────────────────────────▼──────────────────────────────────────────┐
│                       FFmpeg Layer (C)                              │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │              ffplay 核心 (ffplay.c/h - 126KB)                  │  │
│  │  • 完整的播放引擎                                              │  │
│  │  • 解封装、解码                                                │  │
│  │  • 音视频同步                                                  │  │
│  │  • 智能帧丢弃                                                  │  │
│  │  • VideoState 结构体(包含 skyPlayer 指针用于回调)            │  │
│  └───────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘

【三层调用链路说明】
1. Kotlin ↔ Native:通过 JNI 双向调用(方法调用 + 事件回调)
2. C++ ↔ C:通过 ffplay.h 和 skymediaplayer_interface.h 双向调用
3. Native ↔ Android 平台:通过 EGL/OpenGL ES 和 OpenSL ES 对接系统

三层调用链路设计

SkyPlayer 实现了完整的三层双向调用机制:

📱 1. C++ → C 调用链路(skymediaplayer.cpp → ffplay.c)

设计思路:通过 ffplay.h 暴露 C 接口,C++ 通过 extern "C" 调用

// skymediaplayer.cpp 中调用 ffplay.c 的函数
extern "C" {
#include "ffplay.h"
}

void SkyPlayer::prepareAsync() {
    // 调用 ffplay.c 中的函数打开媒体流
    is = stream_open(data_source_, nullptr);
    if (is) {
        is->skyPlayer = this;  // 建立 C → C++ 的回调连接
        setPlayerState(STATE_PREPARED);
    }
}

void SkyPlayer::start() {
    toggle_pause(is);  // 控制播放/暂停
}

void SkyPlayer::seekTo(int64_t msec) {
    stream_seek(is, msec * 1000, 0, 0);  // 定位播放位置
}

ffplay.h 中的关键接口

#ifdef __cplusplus
extern "C" {
#endif

typedef struct VideoState {
    void* skyPlayer;  // C → C++ 回调指针
    // ... 其他字段
} VideoState;

// 供 C++ 调用的 C 接口
VideoState *stream_open(const char *filename, const AVInputFormat *iformat);
void stream_close(VideoState *is);
void toggle_pause(VideoState *is);
void stream_seek(VideoState *is, int64_t pos, int64_t rel, int by_bytes);
double get_current_position(VideoState *is);
int64_t get_media_duration(VideoState *is);

#ifdef __cplusplus
}
#endif

🔄 2. C → C++ 调用链路(ffplay.c → skymediaplayer.cpp)

设计思路:通过 skymediaplayer_interface.h 定义 C 接口,ffplay.c 通过 VideoState.skyPlayer 指针回调

// ffplay.c 中回调 C++ 代码
static void sky_video_image_display(VideoState *is) {
    Frame *vp = frame_queue_peek_last(&is->pictq);
    if (!vp->uploaded) {
        // 调用 C++ 层的视频显示接口
        if (!sky_display_image(is->skyPlayer, vp->frame)) {
            return;
        }
        vp->uploaded = 1;
    }
}

// 音频控制回调
sky_pause_audio(is->skyPlayer, is->paused);
sky_open_audio(is->skyPlayer, &wanted_spec, &spec);

// 消息发送回调
sky_post_simple_message(is->skyPlayer, SKY_MSG_PREPARED);
sky_post_message_ii(is->skyPlayer, SKY_MSG_ERROR, err, 0);

skymediaplayer_interface.h 中的关键接口

#ifdef __cplusplus
extern "C" {
#endif

// 视频显示接口
bool sky_display_image(void *player, AVFrame *frame);

// 音频控制接口
bool sky_open_audio(void *player, SkyAudioSpec *desired, SkyAudioSpec *obtained);
void sky_pause_audio(void *player, bool pause);
void sky_flush_audio(void *player);

// 消息发送接口
bool sky_post_message(void *player, int what, int arg1, int arg2, void *obj);
bool sky_post_simple_message(void *player, int what);
bool sky_post_message_ii(void *player, int what, int arg1, int arg2);

#ifdef __cplusplus
}
#endif

skymediaplayer.cpp 中的接口实现

bool sky_display_image(void *player, AVFrame *frame) {
    auto* skyPlayer = reinterpret_cast<SkyPlayer*>(player);
    return skyPlayer->getSkyVideoOutHandler().displayImage(frame);
}

bool sky_open_audio(void *player, SkyAudioSpec *desired, SkyAudioSpec *obtained) {
    auto* skyPlayer = reinterpret_cast<SkyPlayer*>(player);
    return skyPlayer->getSkyAudioOutHandler().openAudio(desired, obtained);
}

🌉 3. C++ → Kotlin 调用链路(skymediaplayer.cpp → JNI → Kotlin)

完整调用链

skymediaplayer.cpppostMediaEventToJava() 
  → postEventToJava() (JNI层)
  → SkyMediaPlayer.postEventFromNative() (Kotlin静态方法)
  → handleEventFromNative()
  → MediaEventHandler.handleMessage()
  → 监听器回调

Kotlin 层声明 Native 方法

class SkyMediaPlayer : IMediaPlayer {
    private external fun _native_setup()
    private external fun _setDataSource(path: String)
    private external fun _prepareAsync()
    private external fun _start()
    private external fun _pause()
    private external fun _seekTo(msec: Long)
    private external fun _release()
}

JNI 层方法注册

// skymediaplayer_jni.cpp
static JNINativeMethod methods[] = {
    {"_native_setup", "()V", (void*)sky_mediaPlayer_native_setup},
    {"_setDataSource", "(Ljava/lang/String;)V", (void*)sky_mediaPlayer_setDataSource},
    {"_prepareAsync", "()V", (void*)sky_mediaPlayer_prepareAsync},
    {"_start", "()V", (void*)sky_mediaPlayer_start},
    {"_pause", "()V", (void*)sky_mediaPlayer_pause},
    {"_seekTo", "(J)V", (void*)sky_mediaPlayer_seekTo},
    {"_release", "()V", (void*)sky_mediaPlayer_release}
};

extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    // 注册 Native 方法
    env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0]));
    return JNI_VERSION_1_6;
}

Native → Kotlin 事件回调

// skymediaplayer_jni.cpp
bool postEventToJava(SkyPlayer* player, int what, int arg1, int arg2, jobject obj) {
    JNIEnv* env = getJNIEnv();  // 线程本地存储,自动 attach
    
    // 从弱引用获取 Java 对象
    jweak weakRef = player->getWeakJavaPlayerPtr();
    jobject strongRef = env->NewLocalRef(weakRef);
    
    // 调用 Kotlin 静态方法
    env->CallStaticVoidMethod(
        methodManager.getJavaClass(), 
        methodManager.getPostEventFromNative(),
        strongRef, what, arg1, arg2, obj
    );
    
    env->DeleteLocalRef(strongRef);
    return true;
}

Kotlin 层接收事件

class SkyMediaPlayer {
    companion object {
        @Keep
        @JvmStatic
        private fun postEventFromNative(
            player: SkyMediaPlayer?,
            what: Int,
            arg1: Int,
            arg2: Int,
            obj: Any?
        ) {
            player?.handleEventFromNative(what, arg1, arg2, obj)
        }
    }
    
    private fun handleEventFromNative(what: Int, arg1: Int, arg2: Int, obj: Any?) {
        mEventHandler.sendMessage(what, arg1, arg2, obj)
    }
}

🔒 线程安全保障

  1. TLS(线程本地存储):每个线程独立的 JNIEnv 缓存
  2. 自动 attach/detach:线程生命周期自动管理
  3. 弱全局引用:防止 Java 对象内存泄漏
  4. 消息队列:异步事件处理,避免阻塞

FFmpeg 版本升级优势

得益于清晰的调用链路设计,C 和 C++ 代码完全隔离。SkyPlayer 的 FFmpeg 版本升级成本极低,随时跟进 FFmpeg 官方更新:

FFmpeg 6.1 → 8.0 升级实战

  • ffplay.c 替换新版本文件,解决部分编译问题
  • ✅ 仅需调整 ffplay.h 中少量接口声明
  • skymediaplayer_interface.h 保持不变
  • 实际耗时:< 1 小时

技术细节

ffplay 解封装、解码流程

ffplay 的核心流程可以用以下时序图表示:

┌─────────┐      ┌──────────┐      ┌──────────┐      ┌──────────┐
│ 读取线程 │      │ 视频解码  │      │ 音频解码  │      │ 渲染线程  │
│(read_   │      │ 线程      │      │ 线程      │      │          │
│thread)  │      │(video_   │      │(audio_   │      │          │
└────┬────┘      │thread)   │      │thread)   │      └────┬─────┘
     │           └────┬─────┘      └────┬─────┘           │
     │                │                  │                 │
     │ 1. 打开文件    │                  │                 │
     │ avformat_open_input()             │                 │
     │────────────────────────────────────────────────────>│
     │                │                  │                 │
     │ 2. 查找流信息  │                  │                 │
     │ avformat_find_stream_info()       │                 │
     │────────────────────────────────────────────────────>│
     │                │                  │                 │
     │ 3. 读取 Packet │                  │                 │
     │ av_read_frame()│                  │                 │
     │────┐           │                  │                 │
     │    │           │                  │                 │
     │<───┘           │                  │                 │
     │                │                  │                 │
     │ 4. 分发到队列  │                  │                 │
     │────────────────>│ PacketQueue     │                 │
     │                │                  │                 │
     │────────────────────────────────────>│ PacketQueue   │
     │                │                  │                 │
     │                │ 5. 解码          │                 │
     │                │ avcodec_send_packet()              │
     │                │ avcodec_receive_frame()            │
     │                │────┐             │                 │
     │                │    │             │                 │
     │                │<───┘             │                 │
     │                │                  │                 │
     │                │ 6. 放入帧队列    │                 │
     │                │──────────────────────────────────>│
     │                │                  │                 │
     │                │                  │ 7. 解码         │
     │                │                  │ avcodec_send_packet()
     │                │                  │ avcodec_receive_frame()
     │                │                  │────┐            │
     │                │                  │    │            │
     │                │                  │<───┘            │
     │                │                  │                 │
     │                │                  │ 8. 放入帧队列   │
     │                │                  │─────────────────>│
     │                │                  │                 │
     │                │                  │                 │ 9. 音画同步
     │                │                  │                 │    渲染输出
     │                │                  │                 │────┐
     │                │                  │                 │    │
     │                │                  │                 │<───┘

关键数据结构

  1. PacketQueue:存储未解码的 AVPacket
  2. FrameQueue:存储已解码的 AVFrame
  3. Clock:音频时钟、视频时钟、外部时钟

核心代码片段

// ffplay.c - 解码线程主循环
static int decoder_decode_frame(Decoder *d, AVFrame *frame) {
    int ret = AVERROR(EAGAIN);
    
    for (;;) {
        // 1. 尝试接收解码后的帧
        ret = avcodec_receive_frame(d->avctx, frame);
        if (ret >= 0) {
            // 计算 PTS
            AVRational tb = (AVRational){1, frame->sample_rate};
            if (frame->pts != AV_NOPTS_VALUE)
                frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);
            return 1;
        }
        
        // 2. 从队列获取 Packet
        if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
            return -1;
            
        // 3. 发送 Packet 到解码器
        ret = avcodec_send_packet(d->avctx, d->pkt);
        av_packet_unref(d->pkt);
    }
}

OpenGL ES 2.0 画面渲染

🚀 为什么用 OpenGL ES?

对比维度CPU 渲染(Canvas)OpenGL ES 2.0
性能基准10-100x 更快
功耗高 CPU 占用低功耗
分辨率1080P 吃力4K+ 流畅
扩展性受限滤镜、特效随意加
兼容性系统依赖广泛支持

渲染架构

SkyPlayer 实现了 5 种像素格式的独立渲染器:

// skyrenderer.cpp - 渲染器工厂
std::unique_ptr<SkyEGL2RendererImp> createRenderImpFactory(AVPixelFormat format) {
    switch (format) {
        case AV_PIX_FMT_YUV420P:
            return std::make_unique<SkyEGL2RendererYUV420pImp>(format);
        case AV_PIX_FMT_NV12:
            return std::make_unique<SkyEGL2RendererNV12Imp>(format);
        case AV_PIX_FMT_NV21:
            return std::make_unique<SkyEGL2RendererNV21Imp>(format);
        case AV_PIX_FMT_RGBA:
            return std::make_unique<SkyEGL2RendererRGBAImp>(format);
        case AV_PIX_FMT_YUV422P:
            return std::make_unique<SkyEGL2RendererYUV422pImp>(format);
        default:
            return nullptr;
    }
}

🎨 YUV420P 渲染实现

渲染管线

AVFrame (YUV) → 上传纹理 → Shader 转换 → RGB 输出

核心代码

// Fragment Shader - YUV → RGB 转换
void main() {
    vec3 yuv;
    yuv.x = texture2D(samplerY, texCoord).r;        // Y 平面
    yuv.y = texture2D(samplerU, texCoord).r - 0.5;  // U 平面
    yuv.z = texture2D(samplerV, texCoord).r - 0.5;  // V 平面
    
    vec3 rgb = colorMatrix * yuv;  // 色彩空间转换
    gl_FragColor = vec4(rgb, 1.0);
}
// 纹理上传(3 个平面)
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, 
             width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, 
             avFrame->data[0]);  // Y
glTexImage2D(..., width/2, height/2, ..., avFrame->data[1]);  // U
glTexImage2D(..., width/2, height/2, ..., avFrame->data[2]);  // V

OpenSL ES 音频渲染

⚡ 为什么用 OpenSL ES?

特性AudioTrackOpenSL ES
延迟> 100ms< 20ms
性能中间层开销直接硬件访问
控制受限精确缓冲区管理
适用场景普通播放实时音频、游戏

🔧 初始化流程

// 1. 创建引擎和混音器
slCreateEngine(&slObject_, ...);
(*slEngine_)->CreateOutputMix(...);

// 2. 配置音频格式
format_pcm_.numChannels = channels;
format_pcm_.samplesPerSec = sampleRate * 1000;
format_pcm_.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;

// 3. 创建播放器并注册回调
(*slEngine_)->CreateAudioPlayer(...);
(*slBufferQueueItf_)->RegisterCallback(slBufferQueueItf_, callback, this);

// 4. 启动音频线程
audio_thread_ = std::thread([this]() { audioOutputThread(); });

🔄 缓冲区管理

void audioOutputThread() {
    // 设置实时优先级
    pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);
    
    while (running) {
        if (hasEmptyBuffer()) {
            // 从 FFmpeg 获取音频数据
            spec_.callback(userdata, buffer, size);
            
            // 入队播放
            (*slBufferQueueItf_)->Enqueue(slBufferQueueItf_, buffer, size);
        } else {
            // 等待缓冲区空闲
            wait(10ms);
        }
    }
}

关键优化

  • 🎯 实时线程优先级:保证音频不卡顿
  • 🔄 多缓冲区设计:平滑播放
  • ⏱️ 精确时间控制:< 20ms 延迟

音画同步与 Seek 实现

🎬 音画同步策略

核心思想:以音频为主时钟,视频跟随

音频时钟(Master)
    ↓
计算视频与音频的时间差
    ↓
视频太快 → 延迟显示
视频太慢 → 丢帧

核心算法

double compute_target_delay(double delay, VideoState *is) {
    double master_clock = get_master_clock(is);  // 音频时钟
    double diff = get_clock(&is->vidclk) - master_clock;  // 时间差
    
    if (diff <= -sync_threshold)
        delay = FFMAX(0, delay + diff);  // 视频慢,减少延迟
    else if (diff >= sync_threshold)
        delay = delay + diff;  // 视频快,增加延迟
    
    return delay;
}

⏩ Seek 实现

挑战:多线程协调 + 状态重置

解决方案

// 1. 发起 Seek 请求
stream_seek(is, target_pos, 0, 0);

// 2. 读取线程处理
if (is->seek_req) {
    avformat_seek_file(is->ic, -1, INT64_MIN, is->seek_pos, INT64_MAX, 0);
    
    // 清空所有队列
    packet_queue_flush(&is->videoq);
    packet_queue_flush(&is->audioq);
    
    // 通知解码器刷新
    packet_queue_put(&is->videoq, &flush_pkt);
    packet_queue_put(&is->audioq, &flush_pkt);
    
    is->seek_req = 0;
}

关键步骤

  1. ✅ 执行 Seek
  2. ✅ 清空缓冲区
  3. ✅ 刷新解码器
  4. ✅ 重置时钟

资源管理与释放

🗂️ 资源类型

资源类型具体内容风险
FFmpegAVFormatContext、AVCodecContext、AVFrame内存泄漏
OpenGL纹理、着色器、EGL 上下文GPU 资源泄漏
OpenSL ES引擎、播放器、缓冲区音频设备占用
JNI全局引用、局部引用Java 对象泄漏
线程读取、解码、音频线程线程泄漏

🔄 释放顺序

关键原则:先停止使用者,再释放资源

SkyPlayer::~SkyPlayer() {
    stop();                          // 1. 停止所有线程
    sky_ffplay_destroy(ffplayCtx_);  // 2. 释放 FFmpeg
    videoOutHandler_.terminate();    // 3. 释放渲染器
    audioOutHandler_.closeAudio();   // 4. 释放音频
}

OpenGL 释放

eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglDestroyContext(display_, context_);
eglDestroySurface(display_, surface_);
eglTerminate(display_);

OpenSL ES 释放

stop_thread_flag_.store(true);  // 停止线程
audio_thread_.join();           // 等待线程退出
(*slPlayItf_)->SetPlayState(slPlayItf_, SL_PLAYSTATE_STOPPED);
(*slPlayerObject_)->Destroy(slPlayerObject_);  // 销毁播放器
(*slObject_)->Destroy(slObject_);              // 销毁引擎

JNI 释放

env->DeleteWeakGlobalRef(weakRef);  // 释放弱引用
env->SetLongField(thiz, ptrField, 0);  // 清除指针
delete player;  // 删除对象

🛡️ RAII 保障

class SkyPlayer {
    std::unique_ptr<SkyEGL2Renderer> renderer_;  // 自动释放
    std::unique_ptr<SkyAudioOut> audioOut_;      // 自动释放
    
    ~SkyPlayer() {
        // 智能指针自动调用析构函数
    }
};

总结与展望

项目总结

SkyPlayer 基于 FFmpeg 8.0 官方 ffplay 实现了完整的 Android 音视频播放器,主要完成:

  • 播放引擎:保留 ffplay 核心逻辑,实现解封装、解码、音视频同步
  • 硬件加速:OpenGL ES 2.0 渲染(5 种像素格式)+ OpenSL ES 音频(< 20ms 延迟)
  • 跨语言调用:Kotlin ↔ JNI ↔ C++ ↔ C 完整链路
  • 工程化:线程安全、资源管理、异步事件处理

解决了 FFmpeg 在 Android 平台的集成、性能优化和稳定性问题。

开发路线

✅ 已完成

  • 核心播放引擎(基于 ffplay)
  • 5 种像素格式硬件渲染
  • OpenSL ES 低延迟音频
  • JNI 线程安全框架
  • 本地文件播放
  • 播放控制(播放/暂停/Seek)

🚧 进行中

  • 在线视频播放(HTTP/HTTPS)
  • 直播流支持(RTMP、HLS)
  • 字幕渲染
  • 播放列表管理

📋 计划中

  • 硬件解码(MediaCodec)
  • 倍速播放
  • 截图/录制
  • 更多格式支持
  • 性能优化

🙏 致谢

感谢以下开源项目: