项目介绍
什么是 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.cpp
→ postMediaEventToJava()
→ 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)
}
}
🔒 线程安全保障:
- TLS(线程本地存储):每个线程独立的 JNIEnv 缓存
- 自动 attach/detach:线程生命周期自动管理
- 弱全局引用:防止 Java 对象内存泄漏
- 消息队列:异步事件处理,避免阻塞
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. 音画同步
│ │ │ │ 渲染输出
│ │ │ │────┐
│ │ │ │ │
│ │ │ │<───┘
关键数据结构:
- PacketQueue:存储未解码的 AVPacket
- FrameQueue:存储已解码的 AVFrame
- 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?
| 特性 | AudioTrack | OpenSL 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, ¶m);
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;
}
关键步骤:
- ✅ 执行 Seek
- ✅ 清空缓冲区
- ✅ 刷新解码器
- ✅ 重置时钟
资源管理与释放
🗂️ 资源类型
| 资源类型 | 具体内容 | 风险 |
|---|---|---|
| FFmpeg | AVFormatContext、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)
- 倍速播放
- 截图/录制
- 更多格式支持
- 性能优化
🙏 致谢
感谢以下开源项目: