视频播放器项目实战-音视频播放
简单易懂版 | 图文结合 | 生动形象
🎬 先打个比方:播放就像餐厅上菜!
想象餐厅的厨房:
🍽️ 餐厅上菜
│
├── 🖼️ 视频菜(主厨负责)
│ ├── 备菜:解码器准备YUV食材
│ ├── 摆盘:Shader调味上色
│ ├── 上菜:GLVideoView端盘上桌
│ └── 摆桌:EGL布置桌面
│
├── 🔊 音频菜(服务员负责)
│ ├── 备菜:重采样调整口味
│ ├── 装盘:PCM装碗
│ ├── 传菜:缓冲队列传递
│ └── 上菜:SLAudioPlay端盘上桌
│
└── 🎯 协调员:确保菜同时上桌
记住这个比喻,你就懂了!
🖼️ 一、视频显示模块
1.1 IVideoView接口设计
就像:端菜的盘子
c
class IVideoView {
public:
virtual void display(AVFrame *frame) = 0;
virtual int init(int width, int height, ANativeWindow *window) = 0;
virtual void close() = 0;
};
// 实现类
class GLVideoView : public IVideoView {
private:
XShader *shader;
XTexture *texture;
IEGL *egl;
int width, height;
public:
virtual void display(AVFrame *frame) override {
// 更新纹理
texture->update(frame);
// 绘制
draw();
// 交换缓冲
egl->swapBuffer();
}
virtual int init(int w, int h, ANativeWindow *window) override {
width = w;
height = h;
// 1️⃣ 初始化EGL
egl = new XEGL();
egl->init(window);
// 2️⃣ 初始化Shader
shader = new XShader();
shader->init();
// 3️⃣ 初始化纹理
texture = new XTexture();
texture->init();
return 0;
}
virtual void close() override {
if (texture) { delete texture; texture = NULL; }
if (shader) { delete shader; shader = NULL; }
if (egl) { delete egl; egl = NULL; }
}
};
1.2 IEGL类封装
就像:餐桌布置
c
class IEGL {
public:
virtual int init(ANativeWindow *window) = 0;
virtual void close() = 0;
virtual void draw() = 0;
virtual void swapBuffer() = 0;
};
class XEGL : public IEGL {
private:
EGLDisplay display;
EGLSurface surface;
EGLContext context;
public:
virtual int init(ANativeWindow *window) override {
// 获取默认显示
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
printf("❌ 获取EGL显示失败!\n");
return -1;
}
// 初始化EGL
if (eglInitialize(display, NULL, NULL) != EGL_TRUE) {
printf("❌ EGL初始化失败!\n");
return -1;
}
// 选择配置
EGLConfig config;
EGLint configSize = 1;
EGLint configAttrs[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_NONE
};
if (eglChooseConfig(display, configAttrs, &config, configSize, &configSize) != EGL_TRUE) {
printf("❌ 选择EGL配置失败!\n");
return -1;
}
// 创建Surface
surface = eglCreateWindowSurface(display, config, window, NULL);
if (surface == EGL_NO_SURFACE) {
printf("❌ 创建EGL Surface失败!\n");
return -1;
}
// 创建Context
EGLint contextAttrs[] = {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_NONE
};
context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttrs);
if (context == EGL_NO_CONTEXT) {
printf("❌ 创建EGL Context失败!\n");
return -1;
}
// 激活Context
if (eglMakeCurrent(display, surface, surface, context) != EGL_TRUE) {
printf("❌ 激活EGL Context失败!\n");
return -1;
}
printf("✅ EGL初始化成功!\n");
return 0;
}
virtual void close() override {
if (display != EGL_NO_DISPLAY) {
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglDestroyContext(display, context);
eglDestroySurface(display, surface);
eglTerminate(display);
display = EGL_NO_DISPLAY;
}
}
virtual void draw() override {
glClear(GL_COLOR_BUFFER_BIT);
}
virtual void swapBuffer() override {
eglSwapBuffers(display, surface);
}
};
1.3 Android 8.0兼容性问题解决
问题:Android 8.0以上OpenGLES不能播放
c
// 解决方案1:设置Surface属性
class XEGL : public IGL {
public:
virtual int init(ANativeWindow *window) override {
// Android 8.0+ 需要设置Surface属性
ANativeWindow_setBuffersGeometry(window, 0, 0, AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM);
// ... 其余初始化代码
}
};
// 解决方案2:使用PixelFormat
virtual int init(ANativeWindow *window) override {
// 设置格式
int format = PIXEL_FORMAT_RGBA;
ANativeWindow_setBuffersGeometry(window, 0, 0, format);
// ... 其余初始化代码
}
🎨 二、Shader模块
2.1 XShader初始化
就像:厨师调味
c
class XShader {
private:
GLuint program;
GLuint vertexShader;
GLuint fragmentShader;
// 顶点着色器
const char *vertexCode =
"attribute vec4 aPosition;\n"
"attribute vec2 aTextCoord;\n"
"varying vec2 vTextCoord;\n"
"void main() {\n"
" gl_Position = aPosition;\n"
" vTextCoord = aTextCoord;\n"
"}\n";
// 片元着色器(YUV420P转RGB)
const char *fragmentCode =
"precision mediump float;\n"
"varying vec2 vTextCoord;\n"
"uniform sampler2D yTexture;\n"
"uniform sampler2D uTexture;\n"
"uniform sampler2D vTexture;\n"
"void main() {\n"
" float y = texture2D(yTexture, vTextCoord).r;\n"
" float u = texture2D(uTexture, vTextCoord).r - 0.5;\n"
" float v = texture2D(vTexture, vTextCoord).r - 0.5;\n"
" \n"
" float r = y + 1.403 * v;\n"
" float g = y - 0.344 * u - 0.714 * v;\n"
" float b = y + 1.770 * u;\n"
" \n"
" gl_FragColor = vec4(r, g, b, 1.0);\n"
"}\n";
public:
int init() {
// 1️⃣ 创建顶点着色器
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexCode, NULL);
glCompileShader(vertexShader);
// 2️⃣ 创建片元着色器
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentCode, NULL);
glCompileShader(fragmentShader);
// 3️⃣ 创建程序
program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
// 4️⃣ 使用程序
glUseProgram(program);
printf("✅ Shader初始化成功!\n");
return 0;
}
};
2.2 NV21和NV12格式Shader
问题:不同手机返回的YUV格式不同
c
// NV21格式Shader
const char *fragmentCodeNV21 =
"precision mediump float;\n"
"varying vec2 vTextCoord;\n"
"uniform sampler2D yTexture;\n"
"uniform sampler2D uvTexture;\n"
"void main() {\n"
" float y = texture2D(yTexture, vTextCoord).r;\n"
" vec2 uv = texture2D(uvTexture, vTextCoord).ar;\n"
" float u = uv.r - 0.5;\n"
" float v = uv.g - 0.5;\n"
" \n"
" float r = y + 1.403 * v;\n"
" float g = y - 0.344 * u - 0.714 * v;\n"
" float b = y + 1.770 * u;\n"
" \n"
" gl_FragColor = vec4(r, g, b, 1.0);\n"
"}\n";
// 根据格式选择Shader
void initShader(int format) {
if (format == X_DATA_TYPE_YUV420P) {
loadShader(vertexCode, fragmentCode420P);
} else if (format == X_DATA_TYPE_NV12) {
loadShader(vertexCode, fragmentCodeNV12);
} else if (format == X_DATA_TYPE_NV21) {
loadShader(vertexCode, fragmentCodeNV21);
}
}
🖼️ 三、纹理模块
3.1 XTexture初始化
就像:准备食材
c
class XTexture {
private:
GLuint textures[3]; // y,u,v纹理
public:
int init() {
// 创建3个纹理(Y、U、V)
glGenTextures(3, textures);
// 设置Y纹理
glBindTexture(GL_TEXTURE_2D, textures[0]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// 设置U纹理
glBindTexture(GL_TEXTURE_2D, textures[1]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// 设置V纹理
glBindTexture(GL_TEXTURE_2D, textures[2]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
printf("✅ 纹理初始化成功!\n");
return 0;
}
void update(AVFrame *frame) {
// 更新Y纹理
glBindTexture(GL_TEXTURE_2D, textures[0]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE,
frame->width, frame->height, 0,
GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->data[0]);
// 更新U纹理
glBindTexture(GL_TEXTURE_2D, textures[1]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE,
frame->width/2, frame->height/2, 0,
GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->data[1]);
// 更新V纹理
glBindTexture(GL_TEXTURE_2D, textures[2]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE,
frame->width/2, frame->height/2, 0,
GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->data[2]);
}
};
3.2 GLVideoView完整实现
c
class GLVideoView : public IVideoView {
private:
XShader *shader = NULL;
XTexture *texture = NULL;
IEGL *egl = NULL;
ANativeWindow *window = NULL;
int width = 0;
int height = 0;
public:
virtual int init(int w, int h, ANativeWindow *win) override {
width = w;
height = h;
window = win;
// 1️⃣ 初始化EGL
egl = new XEGL();
if (egl->init(window) != 0) {
printf("❌ EGL初始化失败!\n");
return -1;
}
// 2️⃣ 初始化Shader
shader = new XShader();
if (shader->init() != 0) {
printf("❌ Shader初始化失败!\n");
return -1;
}
// 3️⃣ 初始化纹理
texture = new XTexture();
if (texture->init() != 0) {
printf("❌ 纹理初始化失败!\n");
return -1;
}
// 4️⃣ 设置视口
glViewport(0, 0, width, height);
printf("✅ GLVideoView初始化成功!\n");
return 0;
}
virtual void display(AVFrame *frame) override {
if (!frame) return;
// 1️⃣ 更新纹理数据
texture->update(frame);
// 2️⃣ 绘制
shader->draw();
// 3️⃣ 交换缓冲
egl->swapBuffer();
}
virtual void close() override {
if (texture) { delete texture; texture = NULL; }
if (shader) { delete shader; shader = NULL; }
if (egl) { delete egl; egl = NULL; }
}
};
🔊 四、音频播放模块
4.1 IResample音频重采样
就像:调整食材口味
c
class IResample {
public:
virtual int init(AVCodecParameters *codec) = 0;
virtual int resample(AVFrame *frame, uint8_t *out) = 0;
};
class FFResample : public IResample {
private:
SwrContext *swrCtx = NULL;
// 输入参数
int inSampleRate = 0;
AVSampleFormat inFormat = AV_SAMPLE_FMT_NONE;
uint64_t inChannelLayout = 0;
// 输出参数
int outSampleRate = 44100;
AVSampleFormat outFormat = AV_SAMPLE_FMT_S16;
uint64_t outChannelLayout = AV_CH_LAYOUT_STEREO;
public:
virtual int init(AVCodecParameters *codec) override {
inSampleRate = codec->sample_rate;
inFormat = (AVSampleFormat)codec->format;
inChannelLayout = codec->channel_layout;
// 创建重采样上下文
swrCtx = swr_alloc();
// 设置输入参数
av_opt_set_int(swrCtx, "in_channel_layout", inChannelLayout, 0);
av_opt_set_int(swrCtx, "in_sample_rate", inSampleRate, 0);
av_opt_set_sample_fmt(swrCtx, "in_sample_fmt", inFormat, 0);
// 设置输出参数
av_opt_set_int(swrCtx, "out_channel_layout", outChannelLayout, 0);
av_opt_set_int(swrCtx, "out_sample_rate", outSampleRate, 0);
av_opt_set_sample_fmt(swrCtx, "out_sample_fmt", outFormat, 0);
// 初始化
if (swr_init(swrCtx) < 0) {
printf("❌ 重采样初始化失败!\n");
return -1;
}
printf("✅ 重采样初始化成功!\n");
return 0;
}
virtual int resample(AVFrame *frame, uint8_t *out) override {
if (!swrCtx) return 0;
// 重采样
int samples = swr_convert(swrCtx, &out, frame->nb_samples,
(const uint8_t**)frame->data, frame->nb_samples);
return samples * 2 * 2; // 返回字节数
}
};
4.2 IAudioPlay音频播放接口
c
class IAudioPlay {
public:
virtual int init(int sampleRate, int channels) = 0;
virtual void update(uint8_t *data, int size) = 0;
virtual void start() = 0;
virtual void stop() = 0;
virtual void close() = 0;
};
4.3 SLAudioPlay音频播放实现
就像:服务员端菜
c
class SLAudioPlay : public IAudioPlay {
private:
SLObjectItf engineObject = NULL;
SLEngineItf engineEngine = NULL;
SLObjectItf outputMixObject = NULL;
SLObjectItf playerObject = NULL;
SLPlayItf playerPlay = NULL;
SLBufferQueueItf bufferQueue = NULL;
uint8_t *buffer = NULL;
int bufferSize = 0;
public:
virtual int init(int sampleRate, int channels) override {
bufferSize = 4096;
buffer = (uint8_t*)malloc(bufferSize);
// 1️⃣ 创建OpenSLES引擎
slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
// 2️⃣ 创建混音器
(*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL);
(*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
// 3️⃣ 配置PCM格式
SLDataFormat_PCM pcmFormat = {
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
};
// 4️⃣ 配置缓冲队列
SLDataLocator_AndroidSimpleBufferQueue bufferQueueLocator = {
SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
10
};
SLDataSource dataSource = {&bufferQueueLocator, &pcmFormat};
SLDataSink dataSink = {&outputMixLocator, outputMixObject};
// 5️⃣ 创建播放器
(*engineEngine)->CreateAudioPlayer(engineEngine, &playerObject,
&dataSource, &dataSink, 1, ids, req);
(*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);
(*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay);
(*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &bufferQueue);
printf("✅ SLAudioPlay初始化成功!\n");
return 0;
}
virtual void update(uint8_t *data, int size) override {
// 将音频数据压入缓冲队列
(*bufferQueue)->Enqueue(bufferQueue, data, size);
}
virtual void start() override {
(*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PLAYING);
}
virtual void stop() override {
(*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_STOPPED);
}
virtual void close() override {
if (playerObject) {
(*playerObject)->Destroy(playerObject);
playerObject = NULL;
}
if (outputMixObject) {
(*outputMixObject)->Destroy(outputMixObject);
outputMixObject = NULL;
}
if (engineObject) {
(*engineObject)->Destroy(engineObject);
engineObject = NULL;
}
if (buffer) {
free(buffer);
buffer = NULL;
}
}
};
📋 五、开发阶段对照表
根据子目录视频标题整理,以下是完整的12个开发阶段对照:
| 阶段 | 视频标题 | 对应章节 | 关键实现 |
|---|---|---|---|
| 1 | IVideoView显示模块架构讲解和代码创建 | 一.1.1 | IVideoView接口设计、GLVideoView实现 |
| 2 | IEGL类初始化封装java部分添加XPlay窗口类 | 一.2 | XEGL初始化、ANativeWindow封装 |
| 3 | XShader基于yuv420p的初始化Init代码完成并测 | 二.1 | XShader初始化、YUV420P转RGB |
| 4 | XTexture初始化并完成GLVideoView的调用 | 三.1 | XTexture纹理创建、update更新 |
| 5 | GLVideoView完成使用shader显示视频 | 三.2 | GLVideoView完整实现、display渲染 |
| 6 | 完成音频重采样IResample模块初始化代码 | 四.1 | FFResample初始化、SwrContext配置 |
| 7 | 完成音频重采样IResample模块的处理功能 | 四.1 | swr_convert重采样、格式转换 |
| 8 | 完成了IAudioPlay和SLAudioPlay的音频播放 | 四.2-4.3 | IAudioPlay接口、SLAudioPlay实现 |
| 9 | IAudioPlay的Update将音频压入缓冲队列并测试 | 四.3 | Enqueue压入队列、缓冲管理 |
| 10 | SLAudioPlay音频播放完成并测试对MP4文件音视频播 | 四.3、五 | 音视频同步播放测试 |
| 11 | 完成硬解码并完成NV21和NV12格式的shader显示编写 | 二.2 | NV21/NV12格式Shader、多格式支持 |
| 12 | 解决android8.0下opengles不能播放的问题 | 一.3 | ANativeWindow_setBuffersGeometry、兼容处理 |
🎯 六、核心要点速记
🖼️ 视频显示流程:
IVideoView → XEGL初始化 → XShader编译 → XTexture创建 → GLVideoView渲染
🔊 音频播放流程:
IResample重采样 → IAudioPlay接口 → SLAudioPlay播放 → 缓冲队列
🎨 Shader格式支持:
YUV420P(标准格式)
NV12(部分手机)
NV21(部分手机)
⚠️ Android兼容:
Android 8.0+ 需要设置Surface属性
ANativeWindow_setBuffersGeometry
播放流程口诀
1️⃣ 视 - 视频解码得YUV
2️⃣ 纹 - 纹理更新数据
3️⃣ 着 - Shader渲染颜色
4️⃣ 显 - EGL显示画面
1️⃣ 音 - 音频解码得PCM
2️⃣ 重 - 重采样调整格式
3️⃣ 压 - 压入缓冲队列
4️⃣ 播 - OpenSLES播放
本文档整理于 2026-06-15# 视频播放器项目实战-音视频播放
简单易懂版 | 图文结合 | 生动形象
🎬 先打个比方:播放就像电影院放映!
想象电影院的放映场景:
markdown
🎥 电影院放映
│
├── 🖼️ 视频播放:放映电影画面
│ ├── 格式转换:YUV转RGB
│ ├── OpenGL渲染:显示到屏幕
│ ├── 双缓冲:平滑切换画面
│ └── 帧率控制:稳定播放速度
│
├── 🔊 音频播放:播放电影声音
│ ├── 重采样:调整音频格式
│ ├── OpenSLES播放:输出到扬声器
│ ├── 缓冲队列:连续播放声音
│ └── 音量控制:调节音量大小
│
└── 🎯 同步控制:画面和声音同步
├── 时间戳对齐:确保同步
├── 丢帧策略:处理延迟
└── 缓冲管理:平衡播放
记住这个比喻,你就懂了!
🖼️ 一、视频播放模块
1.1 视频播放流程
就像:放映电影画面
┌─────────────────────────────────────────────────────┐
│ 视频播放流程 │
├─────────────────────────────────────────────────────┤
│ │
│ 📦 YUV数据(解码输出) │
│ │ │
│ │ 格式转换(sws_scale) │
│ ▼ │
│ 🎨 RGB数据 │
│ │ │
│ │ OpenGL渲染 │
│ ▼ │
│ 🖥️ 屏幕显示 │
│ │ │
│ │ 双缓冲交换 │
│ ▼ │
│ 👁️ 用户看到画面 │
│ │
└─────────────────────────────────────────────────────┘
1.2 视频渲染器实现
c
#include <GLES2/gl2.h>
#include <EGL/egl.h>
class VideoRenderer {
private:
EGLDisplay eglDisplay;
EGLSurface eglSurface;
EGLContext eglContext;
GLuint program;
GLuint yTexture, uTexture, vTexture;
int width, height;
// 顶点着色器
const char *vertexShaderSource =
"attribute vec4 aPosition;\n"
"attribute vec2 aTexCoord;\n"
"varying vec2 vTexCoord;\n"
"void main() {\n"
" gl_Position = aPosition;\n"
" vTexCoord = aTexCoord;\n"
"}\n";
// 片元着色器(YUV转RGB)
const char *fragmentShaderSource =
"precision mediump float;\n"
"varying vec2 vTexCoord;\n"
"uniform sampler2D yTexture;\n"
"uniform sampler2D uTexture;\n"
"uniform sampler2D vTexture;\n"
"void main() {\n"
" float y = texture2D(yTexture, vTexCoord).r;\n"
" float u = texture2D(uTexture, vTexCoord).r - 0.5;\n"
" float v = texture2D(vTexture, vTexCoord).r - 0.5;\n"
" float r = y + 1.403 * v;\n"
" float g = y - 0.344 * u - 0.714 * v;\n"
" float b = y + 1.770 * u;\n"
" gl_FragColor = vec4(r, g, b, 1.0);\n"
"}\n";
public:
// 初始化
int init(int w, int h, ANativeWindow *window) {
width = w;
height = h;
// 1️⃣ 初始化EGL
initEGL(window);
// 2️⃣ 创建Shader程序
program = createProgram(vertexShaderSource, fragmentShaderSource);
// 3️⃣ 创建纹理
createTextures();
// 4️⃣ 设置视口
glViewport(0, 0, width, height);
printf("✅ 视频渲染器初始化成功!\n");
return 0;
}
// 渲染一帧
void render(AVFrame *frame) {
// 1️⃣ 更新纹理数据
updateTexture(yTexture, frame->data[0], frame->width, frame->height);
updateTexture(uTexture, frame->data[1], frame->width/2, frame->height/2);
updateTexture(vTexture, frame->data[2], frame->width/2, frame->height/2);
// 2️⃣ 清除屏幕
glClear(GL_COLOR_BUFFER_BIT);
// 3️⃣ 使用程序
glUseProgram(program);
// 4️⃣ 设置顶点
setVertices();
// 5️⃣ 设置纹理
setTextures();
// 6️⃣ 绘制
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// 7️⃣ 交换缓冲区
eglSwapBuffers(eglDisplay, eglSurface);
}
// 释放
void release() {
// 释放纹理
glDeleteTextures(1, &yTexture);
glDeleteTextures(1, &uTexture);
glDeleteTextures(1, &vTexture);
// 释放程序
glDeleteProgram(program);
// 释放EGL
releaseEGL();
printf("✅ 视频渲染器已释放!\n");
}
private:
// 初始化EGL
void initEGL(ANativeWindow *window) {
// 获取显示
eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(eglDisplay, NULL, NULL);
// 选择配置
EGLConfig config;
EGLint configAttrs[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_NONE
};
eglChooseConfig(eglDisplay, configAttrs, &config, 1, NULL);
// 创建Surface
eglSurface = eglCreateWindowSurface(eglDisplay, config, window, NULL);
// 创建Context
EGLint contextAttrs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
eglContext = eglCreateContext(eglDisplay, config, NULL, contextAttrs);
// 绑定Context
eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
}
// 创建纹理
void createTextures() {
glGenTextures(1, &yTexture);
glGenTextures(1, &uTexture);
glGenTextures(1, &vTexture);
// 设置纹理参数
glBindTexture(GL_TEXTURE_2D, yTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, uTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, vTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}
// 更新纹理
void updateTexture(GLuint texture, uint8_t *data, int w, int h) {
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, w, h, 0,
GL_LUMINANCE, GL_UNSIGNED_BYTE, data);
}
// 设置顶点
void setVertices() {
GLfloat vertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f
};
GLfloat texCoords[] = {
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f
};
GLint aPosition = glGetAttribLocation(program, "aPosition");
GLint aTexCoord = glGetAttribLocation(program, "aTexCoord");
glEnableVertexAttribArray(aPosition);
glVertexAttribPointer(aPosition, 2, GL_FLOAT, GL_FALSE, 0, vertices);
glEnableVertexAttribArray(aTexCoord);
glVertexAttribPointer(aTexCoord, 2, GL_FLOAT, GL_FALSE, 0, texCoords);
}
// 设置纹理
void setTextures() {
GLint yLoc = glGetUniformLocation(program, "yTexture");
GLint uLoc = glGetUniformLocation(program, "uTexture");
GLint vLoc = glGetUniformLocation(program, "vTexture");
glUniform1i(yLoc, 0);
glUniform1i(uLoc, 1);
glUniform1i(vLoc, 2);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, yTexture);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, uTexture);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, vTexture);
}
};
🔊 二、音频播放模块
2.1 音频播放流程
就像:播放电影声音
┌─────────────────────────────────────────────────────┐
│ 音频播放流程 │
├─────────────────────────────────────────────────────┤
│ │
│ 📦 PCM数据(解码输出) │
│ │ │
│ │ 重采样(swr_convert) │
│ ▼ │
│ 🎵 目标格式PCM │
│ │ │
│ │ 放入缓冲队列 │
│ ▼ │
│ 📦 OpenSLES缓冲队列 │
│ │ │
│ │ 播放 │
│ ▼ │
│ 🔊 扬声器输出 │
│ │
└─────────────────────────────────────────────────────┘
2.2 音频播放器实现
c
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
class AudioPlayer {
private:
SLObjectItf engineObject;
SLEngineItf engineEngine;
SLObjectItf outputMixObject;
SLObjectItf playerObject;
SLPlayItf playerPlay;
SLBufferQueueItf bufferQueue;
SwrContext *swrCtx;
uint8_t *buffer;
int bufferSize;
public:
// 初始化
int init(int sampleRate, int channels) {
// 1️⃣ 初始化OpenSLES
initOpenSLES();
// 2️⃣ 初始化重采样
initResampler(sampleRate, channels);
// 3️⃣ 分配缓冲区
bufferSize = 4096;
buffer = (uint8_t*)malloc(bufferSize);
printf("✅ 音频播放器初始化成功!\n");
return 0;
}
// 播放音频帧
void play(AVFrame *frame) {
// 1️⃣ 重采样
int outSamples = swr_convert(swrCtx, &buffer, bufferSize/4,
(const uint8_t**)frame->data, frame->nb_samples);
// 2️⃣ 计算大小
int outSize = outSamples * 2 * 2; // 采样数 × 声道数 × 字节数
// 3️⃣ 放入队列
(*bufferQueue)->Enqueue(bufferQueue, buffer, outSize);
}
// 开始播放
void start() {
(*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PLAYING);
printf("✅ 开始播放音频!\n");
}
// 停止播放
void stop() {
(*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_STOPPED);
printf("✅ 停止播放音频!\n");
}
// 释放
void release() {
// 释放OpenSLES
if (playerObject) (*playerObject)->Destroy(playerObject);
if (outputMixObject) (*outputMixObject)->Destroy(outputMixObject);
if (engineObject) (*engineObject)->Destroy(engineObject);
// 释放重采样
swr_free(&swrCtx);
// 释放缓冲区
free(buffer);
printf("✅ 音频播放器已释放!\n");
}
private:
// 初始化OpenSLES
void initOpenSLES() {
// 创建引擎
slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
// 创建混音器
(*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL);
(*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
// 配置PCM格式
SLDataFormat_PCM pcmFormat = {
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
};
// 配置缓冲队列
SLDataLocator_AndroidSimpleBufferQueue bufferQueueLocator = {
SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
10
};
SLDataSource dataSource = {&bufferQueueLocator, &pcmFormat};
// 配置输出
SLDataLocator_OutputMix outputMixLocator = {
SL_DATALOCATOR_OUTPUTMIX,
outputMixObject
};
SLDataSink dataSink = {&outputMixLocator, NULL};
// 创建播放器
SLInterfaceID ids[] = {SL_IID_BUFFERQUEUE};
SLboolean req[] = {SL_BOOLEAN_TRUE};
(*engineEngine)->CreateAudioPlayer(engineEngine, &playerObject,
&dataSource, &dataSink, 1, ids, req);
(*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);
// 获取接口
(*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay);
(*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &bufferQueue);
}
// 初始化重采样
void initResampler(int sampleRate, int channels) {
swrCtx = swr_alloc();
av_opt_set_int(swrCtx, "in_channel_layout",
channels == 2 ? AV_CH_LAYOUT_STEREO : AV_CH_LAYOUT_MONO, 0);
av_opt_set_int(swrCtx, "in_sample_rate", sampleRate, 0);
av_opt_set_sample_fmt(swrCtx, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0);
av_opt_set_int(swrCtx, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0);
av_opt_set_int(swrCtx, "out_sample_rate", 44100, 0);
av_opt_set_sample_fmt(swrCtx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
swr_init(swrCtx);
}
};
🎯 三、播放控制模块
3.1 播放状态管理
c
enum PlayState {
STATE_IDLE,
STATE_PLAYING,
STATE_PAUSED,
STATE_STOPPED
};
class PlayController {
private:
PlayState state = STATE_IDLE;
public:
void play() {
if (state == STATE_PAUSED || state == STATE_STOPPED) {
state = STATE_PLAYING;
printf("▶️ 开始播放\n");
}
}
void pause() {
if (state == STATE_PLAYING) {
state = STATE_PAUSED;
printf("⏸️ 已暂停\n");
}
}
void stop() {
state = STATE_STOPPED;
printf("⏹️ 已停止\n");
}
PlayState getState() {
return state;
}
};
3.2 音量控制
c
class VolumeController {
private:
int volume = 100; // 0-100
bool muted = false;
public:
void setVolume(int vol) {
volume = vol;
printf("🔊 音量:%d\n", volume);
}
void mute() {
muted = true;
printf("🔇 已静音\n");
}
void unmute() {
muted = false;
printf("🔊 已取消静音\n");
}
int getVolume() {
return muted ? 0 : volume;
}
};
📊 四、播放流程图
4.1 完整播放流程
objectivec
┌─────────────────────────────────────────────────────┐
│ 完整播放流程 │
├─────────────────────────────────────────────────────┤
│ │
│ 📁 视频文件 │
│ │ │
│ │ 解封装 │
│ ▼ │
│ 📦 数据包(AVPacket) │
│ │ │
│ ├─── 视频包 ────┐ │
│ │ │ │
│ │ ▼ │
│ │ 视频解码 │
│ │ │ │
│ │ ▼ │
│ │ YUV帧 │
│ │ │ │
│ │ ▼ │
│ │ OpenGL渲染 │
│ │ │ │
│ │ ▼ │
│ │ 🖥️ 屏幕显示 │
│ │ │
│ ├─── 音频包 ────┐ │
│ │ │ │
│ │ ▼ │
│ │ 音频解码 │
│ │ │ │
│ │ ▼ │
│ │ PCM帧 │
│ │ │ │
│ │ ▼ │
│ │ 重采样 │
│ │ │ │
│ │ ▼ │
│ │ OpenSLES播放 │
│ │ │ │
│ │ ▼ │
│ │ 🔊 扬声器输出 │
│ │
└─────────────────────────────────────────────────────┘
🎯 五、总结
核心要点
| 模块 | 作用 | 生活类比 |
|---|---|---|
| 视频渲染 | 显示画面 | 放映电影 |
| 音频播放 | 播放声音 | 扬声器输出 |
| 播放控制 | 控制播放状态 | 播放按钮 |
| 音量控制 | 调节音量 | 音量旋钮 |
播放流程总结
🎬 音视频播放完整流程:
1️⃣ 解封装读取数据包
2️⃣ 视频包 → 解码 → YUV → OpenGL渲染 → 屏幕
3️⃣ 音频包 → 解码 → PCM → 重采样 → OpenSLES播放 → 扬声器
4️⃣ 同步控制确保画面和声音同步
速记口诀
1️⃣ 解 - 解封装读取
2️⃣ 解 - 解码数据
3️⃣ 渲 - 渲染画面
4️⃣ 播 - 播放声音
5️⃣ 控 - 控制播放
本文档整理于 2026-06-15