【小米训练营】C++方向 实践项目 Android Player
note:本人使用的是android studio的虚拟安卓 架构是x86_64 无法直接在真机上运行
day3
演示视频
Screenrecording_20250712_175548.mp4
Screenrecording_20250712_194057.mp4
如果看不到视频,视频文件在Readme.assets/Screenrecording_20250712_170055.mp4
、Readme.assets/Screenrecording_20250712_175548.mp4
、Readme.assets/Screenrecording_20250712_194057.mp4
中。
其中:
Readme.assets/Screenrecording_20250712_170055.mp4
演示了单个视频的效果。Readme.assets/Screenrecording_20250712_175548.mp4
演示了多个视频的效果。Readme.assets/Screenrecording_20250712_194057.mp4
演示了视频倍速播放的效果。
整体架构
环形缓冲区
为什么需要环形缓冲区
因为音频解码出来的PCM帧并不是音频渲染的最小单位 最小单位是采样点 解码出来的PCM帧可能包含900个点 也可能包含1000个点 但是渲染线程可能每次只需要850个点 环形缓冲区需要支持这种任意读取任意写入的操作
设计思路
以字节作为最小单位 使用两个指针(读指针 read_pos_
和写指针 write_pos_
)来维护整个数据结构。
读指针表示下一个可读的数据的位置
写指针表示下一个可写的数据的位置
当写到最后的时候,使用模运算来形成一个循环。
对于读操作:
- 如果读指针和写指针相等 则表示缓冲区为空,否则读取
read_pos_ + size
范围内的数据,并更新read_pos_ = (read_pos_ + size) % capacity_
使用模运算是为了形成一个循环 同时防止读指针越界
对于写操作:
- 如果写指针和读指针相差1 则表示缓冲区已满,否则写入
write_pos_ + size
范围内的数据,并更新write_pos_ = (write_pos_ + size) % capacity_
使用模运算是为了形成一个循环 同时防止写指针越界
线程安全
考虑到有两个线程会同时访问这个给环形缓冲区,因此对于相关修改操作使用互斥锁保证线程安全
音频解码器的设计和实现
AudioDecoder采用独立线程+消费者模式+环形缓冲队列的设计,负责将音频数据包解码为标准PCM格式:
核心架构
外部AudioPacketQueue → AudioDecoder Thread → PCM帧 → 环形缓冲队列 → AAudio回调播放(消费者) (生产者) (实时播放)
关键设计特性
1. PCM帧数据结构
struct PCMFrame {uint8_t* data; // PCM数据缓冲区int data_size; // 数据大小(字节)int sample_rate; // 采样率int channels; // 声道数int samples_per_channel; // 每声道采样数AVSampleFormat sample_format; // 采样格式(S16)int64_t pts; // 时间戳
};
2. 环形缓冲队列策略
与传统队列不同,音频解码器使用环形缓冲区来处理PCM数据,原因是:
- 采样粒度差异:解码出来的PCM帧包含任意数量的采样点(如900或1000个点)
- 播放需求不同:音频渲染线程每次可能只需要特定数量的采样点(如850个点)
- 灵活读写:环形缓冲区支持任意大小的读取和写入操作
3. 音频重采样处理
// 使用libswresample进行格式转换
SwrContext* swr_context_;// 转换为目标PCM格式
int converted_samples = swr_convert(swr_context_, &pcm_buffer_, out_samples,(const uint8_t**)src_frame->data, src_frame->nb_samples);// 输出到环形缓冲区
PCMFrame pcm_frame;
pcm_frame.data = pcm_buffer_;
pcm_frame.data_size = data_size;
pcm_frame.sample_rate = target_sample_rate_;
pcm_frame.channels = target_channels_;
pcm_frame.samples_per_channel = converted_samples;
4. 环形缓冲区写入
// PCM帧回调 - 写入环形缓冲区
void Player::onPCMFrame(const AudioDecoder::PCMFrame& frame) {if (audioCircularBuffer && frame.data && frame.data_size > 0) {size_t written = audioCircularBuffer->write(frame.data, frame.data_size);if (written < frame.data_size) {// 缓冲区满,丢弃部分数据LOGW(TAG, "Audio buffer overflow, discarded %zu bytes", frame.data_size - written);}}
}
5. AAudio回调读取
// 从环形缓冲区读取音频数据用于播放
size_t bytes_read = player->readPCMDataFromBuffer(audioData, adjusted_bytes);// 精确更新音频播放位置
double frames_played = static_cast<double>(bytes_read) / (channels * 2);
double time_increment = frames_played / static_cast<double>(sample_rate);
player->audio_playback_position_ = player->audio_playback_position_.load() + time_increment;
音视频同步
核心设计思路
本项目采用音频为主时钟的同步策略,通过精确的时间控制和缓冲区管理实现稳定的音视频同步播放。
实现架构
音频时钟(主时钟) ← 音频播放位置 ← AAudio回调精确更新↓
视频同步判断 ← 视频帧PTS ← 时间基转换↓
帧丢弃/延迟策略 → 视频渲染
关键技术实现
1. 音频主时钟策略
// 音频播放位置作为基准时钟
std::atomic<double> audio_playback_position_;// 音频回调中精确更新时间
double frames_played = static_cast<double>(bytes_read) / (channels * 2);
double time_increment = frames_played / static_cast<double>(sample_rate);
audio_playback_position_ = audio_playback_position_.load() + time_increment;
2. 视频同步逻辑
// 计算音视频时间差
double audio_time = audio_playback_position_.load();
double video_frame_time = ptsToSeconds(decoderFrame->pts, video_time_base_);
double sync_diff = video_frame_time - audio_time;// 同步策略
if (sync_diff < -0.1) {// 视频严重滞后,丢弃帧freeYUVFrame(decoderFrame);continue;
} else if (sync_diff > 0.02) {// 视频超前,适当延迟std::this_thread::sleep_for(std::chrono::milliseconds(60));
}
3. 时间基转换
// PTS转换为秒的高精度转换
double ptsToSeconds(int64_t pts, AVRational time_base) {if (pts == AV_NOPTS_VALUE || time_base.den == 0) {return 0.0;}return static_cast<double>(pts) * time_base.num / time_base.den;
}
4. 帧率控制
// 视频渲染器中的帧率控制
if (frame_rate.num > 0 && frame_rate.den > 0) {frame_duration_ms_ = (1000.0 * frame_rate.den) / frame_rate.num;
}// 渲染时间控制
if (target_seconds > elapsed_seconds) {double wait_time = target_seconds - elapsed_seconds;if (wait_time > 0 && wait_time < 0.1) {std::this_thread::sleep_for(std::chrono::milliseconds(wait_ms));}
}
环形缓冲区的作用
音频解码出来的PCM帧并不是音频渲染的最小单位,环形缓冲区支持任意读取任意写入的操作,确保音频播放的连续性和时间精度。
给视频添加水印
核心设计思路
基于OpenGL ES渲染管道实现高性能水印叠加,支持多种图片格式,具备智能缓存和透明度控制。
实现架构
图片文件 → stb_image加载 → 智能缓存 → OpenGL纹理 → 水印着色器 → 混合渲染↓位置/缩放/透明度参数控制
关键技术实现
1. OpenGL ES水印渲染
// 水印着色器
const char* watermark_vertex_shader_source_ = R"(
attribute vec4 a_position;
attribute vec2 a_texcoord;
varying vec2 v_texcoord;
void main() {gl_Position = a_position;v_texcoord = a_texcoord;
}
)";const char* watermark_fragment_shader_source_ = R"(
precision mediump float;
varying vec2 v_texcoord;
uniform sampler2D watermark_texture;
uniform float alpha;
void main() {vec4 texColor = texture2D(watermark_texture, v_texcoord);gl_FragColor = vec4(texColor.rgb, texColor.a * alpha);
}
)";
2. 智能图片缓存系统
// 线程安全的全局缓存
static std::unordered_map<std::string, std::shared_ptr<ImageCache>> image_cache_map_;
static std::mutex image_cache_mutex_;// 缓存管理
std::shared_ptr<ImageCache> getImageFromCache(const std::string& path) {std::lock_guard<std::mutex> lock(image_cache_mutex_);auto it = image_cache_map_.find(path);if (it != image_cache_map_.end()) {return it->second;}return nullptr;
}
3. stb_image图片加载
// 支持多种图片格式,自动转换为RGBA
unsigned char* image_data = stbi_load(path.c_str(), &width, &height, &channels, 4);
if (!image_data) {LOGE(TAG, "Failed to load watermark image: %s", stbi_failure_reason());return false;
}
4. 渲染混合
// 启用透明度混合
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);// 设置水印透明度
glUniform1f(watermark_alpha_location_, watermark_alpha_);// 渲染水印到视频上
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
使用方式
// Java层调用
player.setWatermark("/sdcard/watermark.png", 0.8f, 0.8f, 0.2f, 0.8f);
player.clearWatermark();
// C++层控制
videoRender->setWatermark(watermark_path, x, y, scale, alpha);
遇到的问题
代码中设置的是正方形水印
const float watermark_vertices[] = {// 位置 纹理坐标watermark_x - watermark_width, watermark_y - watermark_height, 0.0f, 1.0f, // 左下watermark_x + watermark_width, watermark_y - watermark_height, 1.0f, 1.0f, // 右下watermark_x - watermark_width, watermark_y + watermark_height, 0.0f, 0.0f, // 左上watermark_x + watermark_width, watermark_y + watermark_height, 1.0f, 0.0f // 右上};
但是绘制出来变成了16:9
的水印
问题原因
似乎和openGL有关,如果是正方形的视频,水印就会变成正方形的,应该是openGL的坐标系会被视频拉伸?
解决方案
缩放宽和高,原视频是根据原视频的比例 相对应的放大或者缩小长宽
// 根据视频宽高比调整水印尺寸,保持正方形 这里要除以2 因为是上下或者 左右两倍
float video_aspect = (float)video_width_ / (float)video_height_ / 2.0f;
float watermark_width = watermark_size;
float watermark_height = watermark_size;if (video_aspect > 1.0f) {// 视频较宽,需要增大水印的高度来补偿拉伸watermark_height = watermark_size * video_aspect;
} else {// 视频较高,需要增大水印的宽度来补偿压缩watermark_width = watermark_size / video_aspect;
}const float watermark_vertices[] = {// 位置 纹理坐标watermark_x - watermark_width, watermark_y - watermark_height, 0.0f, 1.0f, // 左下watermark_x + watermark_width, watermark_y - watermark_height, 1.0f, 1.0f, // 右下watermark_x - watermark_width, watermark_y + watermark_height, 0.0f, 0.0f, // 左上watermark_x + watermark_width, watermark_y + watermark_height, 1.0f, 0.0f // 右上
};
解决后的效果
可以看到目前水印渲染出来变成了正方形了
精确seek的实现
核心思路
传统seek只能定位到关键帧(I帧),精确seek通过先定位关键帧,再逐帧解码并丢弃不需要的帧来实现精确定位。
目标时间点 ←──── 精确seek目标 ↑
丢弃帧 ← P帧 ← P帧 ← I帧(关键帧) ← 解复用器定位点(舍弃) (舍弃) (输出)
实现方案
1. VideoDecoder扩展
添加精确seek状态变量和方法,当seek时 解码器会丢弃所有target_pts_ms前的帧:
std::atomic<bool> is_seeking_; // 是否正在精确seek
std::atomic<int64_t> seek_target_pts_ms_; // 目标时间戳(毫秒)
AVRational time_base_; // 视频流时间基void setSeekTargetPts(int64_t target_pts_ms); // 设置目标时间戳
void setVideoTimeBase(AVRational time_base); // 设置时间基
2. 智能丢帧逻辑
解码器在receiveFrames()
中检查每帧PTS:
if (is_seeking_.load() && seek_target_pts_ms_.load() >= 0) {int64_t frame_pts_ms = av_rescale_q(frame_->pts, time_base_, {1, 1000});if (frame_pts_ms >= seek_target_pts_ms_.load()) {is_seeking_ = false; // 到达目标,结束seek模式// 输出这一帧} else {// 丢弃这一帧dropped_frame_count_++;continue;}
}
3. Player协调流程
int Player::seek(double position) {// 暂停组件,清空队列pauseAllComponents();clearQueues();// 解复用器seek到关键帧int64_t timestamp_ms = position * duration * 1000;demuxer->seek(timestamp_ms);// 设置解码器精确seek参数videoDecoder->reset();videoDecoder->setVideoTimeBase(demuxer->getVideoTimeBase());videoDecoder->setSeekTargetPts(timestamp_ms);// 恢复播放resumeAllComponents();
}
技术要点
- 时间基转换:使用
av_rescale_q()
进行高精度时间基转换 - 状态管理:seek开始时设置标志,到达目标时自动清除
- 性能优化:最小化丢帧数量,快速退出seek模式
倍速播放的实现
核心设计思路
实现真正的音视频倍速播放,支持0.25x到4.0x的速度范围,通过音频重采样+视频帧间隔调整+音视频同步确保播放质量。
实现架构
播放速度控制 → 音频重采样 + 视频帧间隔调整 → 音视频同步保持↓ ↓ ↓ ↓
Java UI选择 → Native Layer → 线性插值重采样 + 帧率调整 → 统一时间基准
关键技术实现
1. Native层播放速度管理
class Player {
private:std::atomic<float> playback_speed_; // 播放速度 (0.25-4.0)public:// 设置播放速度int setSpeed(float speed) {// 限制播放速度范围if (speed < 0.25f || speed > 4.0f) {LOGE(TAG, "Invalid playback speed: %.2f (must be between 0.25 and 4.0)", speed);return -1;}playback_speed_ = speed;// 同步更新视频渲染器速度if (videoRender) {videoRender->setPlaybackSpeed(speed);}return 0;}float getSpeed() const {return playback_speed_.load();}
};
2. 音频倍速播放实现
采用线性插值重采样算法在音频回调中实时处理:
int Player::audioCallback(AAudioStream* stream, void* userData, void* audioData, int32_t numFrames) {Player* player = static_cast<Player*>(userData);float playback_speed = player->playback_speed_.load();if (playback_speed == 1.0f) {// 正常速度,直接读取bytes_read = player->readPCMDataFromBuffer(audioData, bytes_needed);} else {// 倍速播放,进行音频重采样size_t source_bytes_needed = static_cast<size_t>(bytes_needed * playback_speed);uint8_t* temp_buffer = new uint8_t[source_bytes_needed];size_t temp_bytes_read = player->readPCMDataFromBuffer(temp_buffer, source_bytes_needed);if (temp_bytes_read > 0) {// 线性插值重采样(支持多声道)int16_t* source_samples = reinterpret_cast<int16_t*>(temp_buffer);int16_t* output_samples = reinterpret_cast<int16_t*>(audioData);int source_frame_count = temp_bytes_read / (channels * 2);int output_frame_count = bytes_needed / (channels * 2);// 为每个音频帧进行插值处理for (int i = 0; i < output_frame_count; i++) {float source_index = i * playback_speed;int index1 = static_cast<int>(source_index);int index2 = index1 + 1;// 为每个声道进行线性插值for (int ch = 0; ch < channels; ch++) {if (index2 < source_frame_count) {float fraction = source_index - index1;int sample1_idx = index1 * channels + ch;int sample2_idx = index2 * channels + ch;int output_idx = i * channels + ch;output_samples[output_idx] = static_cast<int16_t>(source_samples[sample1_idx] * (1.0f - fraction) + source_samples[sample2_idx] * fraction);} else if (index1 < source_frame_count) {output_samples[i * channels + ch] = source_samples[index1 * channels + ch];} else {output_samples[i * channels + ch] = 0;}}}bytes_read = bytes_needed;}delete[] temp_buffer;}// 根据播放速度调整时间推进if (bytes_read > 0) {double frames_played = static_cast<double>(bytes_read) / (channels * 2);double time_increment = frames_played / static_cast<double>(sample_rate);double adjusted_time_increment = time_increment * playback_speed; // 倍速调整player->audio_playback_position_ = player->audio_playback_position_.load() + adjusted_time_increment;}
}
3. 视频倍速播放实现
通过动态调整帧间隔实现视频倍速:
class VideoRender {
private:double base_frame_duration_ms_; // 原始帧间隔(毫秒)double frame_duration_ms_; // 当前帧间隔(已考虑速度)std::atomic<float> playback_speed_; // 播放速度public:void setPlaybackSpeed(float speed) {playback_speed_ = speed;// 基于原始帧间隔重新计算当前帧间隔frame_duration_ms_ = base_frame_duration_ms_ / speed;LOGI(TAG, "Video speed adjusted: base=%.2fms, current=%.2fms (%.2fx)", base_frame_duration_ms_, frame_duration_ms_, speed);}void setVideoTiming(AVRational time_base, AVRational frame_rate) {// 保存原始帧间隔if (frame_rate.num > 0 && frame_rate.den > 0) {base_frame_duration_ms_ = (1000.0 * frame_rate.den) / frame_rate.num;} else {base_frame_duration_ms_ = 40.0; // 默认25fps}// 根据当前播放速度计算实际帧间隔float current_speed = playback_speed_.load();frame_duration_ms_ = base_frame_duration_ms_ / current_speed;}
};
4. 音视频同步保持
倍速播放时的同步策略:
// 音频:通过重采样和时间调整保持倍速
double adjusted_time_increment = time_increment * playback_speed;
audio_playback_position_ += adjusted_time_increment;// 视频:通过帧间隔调整保持倍速
frame_duration_ms_ = base_frame_duration_ms_ / playback_speed;// 同步检查:两者都基于相同的播放速度参数
double audio_time = audio_playback_position_.load();
double video_time = ptsToSeconds(frame_pts, video_time_base_);
double sync_diff = video_time - audio_time; // 同步差异检测
5. UI交互实现
提供丰富的倍速选择界面:
// MainActivity.java - 倍速选择器
private void showSpeedSelector() {final float[] speedValues = {0.25f, 0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 2.0f, 3.0f, 4.0f};final String[] speedTexts = {"0.25x", "0.5x", "0.75x", "1x", "1.25x", "1.5x", "2x", "3x", "4x"};AlertDialog.Builder builder = new AlertDialog.Builder(this);builder.setTitle("选择播放速度").setSingleChoiceItems(speedTexts, currentSelection, (dialog, which) -> {float selectedSpeed = speedValues[which];// 调用native方法设置播放速度int result = player.setSpeed(selectedSpeed);if (result == 0) {speedButton.setText(speedTexts[which]);currentPlaybackSpeed = selectedSpeed; // 保存状态Toast.makeText(this, "播放速度已设置为 " + speedTexts[which], Toast.LENGTH_SHORT).show();}dialog.dismiss();}).show();
}// 播放状态恢复
private void restorePlaybackSpeed() {if (currentPlaybackSpeed != 1.0f) {player.setSpeed(currentPlaybackSpeed);Button speedButton = findViewById(R.id.button3);speedButton.setText(formatSpeedText(currentPlaybackSpeed));}
}
技术要点
- 音频质量保证:使用线性插值算法避免音频失真
- 多声道支持:确保立体声等多声道音频的正确处理
- 内存管理:动态分配临时缓冲区,避免内存泄漏
- 状态持久化:保存用户选择的播放速度,支持状态恢复
- 性能优化:正常播放时跳过重采样,减少CPU开销
day2
整体流程
视频渲染器(VideoRender)的设计
设计概述
基于VideoDecoder,设计并实现了对称的视频渲染器(VideoRender
)。视频渲染器采用独立线程+消费者模式+OpenGL ES硬件加速的设计,负责将YUV帧通过GPU硬件加速渲染到Android Surface。
核心架构
外部YUVFrameQueue → VideoRender Thread → OpenGL ES → Android Surface(Consumer) (GPU Render)
关键设计特性
1. YUV帧数据结构(与解码器保持一致)
struct YUVFrame {uint8_t* y_data, *u_data, *v_data; // YUV分量数据指针int y_linesize, u_linesize, v_linesize; // 各分量行大小int width, height; // 视频尺寸int64_t pts; // 时间戳
};
2. 回调函数设计
// YUV帧获取回调(从外部队列获取YUV帧)
using YUVFrameGetCallback = std::function<bool(YUVFrame**)>;// 渲染完成回调
using RenderCompleteCallback = std::function<void(int64_t pts)>;
3. OpenGL ES渲染管道
bool init(JNIEnv* env, jobject surface, int width, int height);
void setYUVFrameGetCallback(YUVFrameGetCallback callback); // 从外部队列获取帧
void setRenderCompleteCallback(RenderCompleteCallback callback); // 渲染完成通知
核心实现要点
1. OpenGL ES 3.0着色器渲染
- 顶点着色器:使用GLSL,处理四边形顶点变换和纹理坐标映射
- 片段着色器:实现YUV420P到RGB的颜色空间转换(ITU-R BT.601标准)
- 三纹理绑定:Y、U、V分量分别绑定到不同的纹理单元
2. YUV到RGB转换矩阵 (OpenGL ES 3.0)
// 片段着色器中的转换算法 (ES 3.0语法)
#version 300 es
precision mediump float;
in vec2 v_texcoord;
out vec4 fragColor;float y = texture(y_texture, v_texcoord).r;
float u = texture(u_texture, v_texcoord).r - 0.5;
float v = texture(v_texture, v_texcoord).r - 0.5;// ITU-R BT.601转换矩阵
float r = y + 1.402 * v;
float g = y - 0.344 * u - 0.714 * v;
float b = y + 1.772 * u;
fragColor = vec4(r, g, b, 1.0);
3. 线程安全设计
void renderThreadFunc() {while (!should_stop_) {// 处理暂停状态if (should_pause_) {std::unique_lock<std::mutex> lock(state_mutex_);pause_cv_.wait(lock, [this] { return !should_pause_ || should_stop_; });}// 从外部队列获取YUV帧YUVFrame* frame = nullptr;if (getYUVFrameFromQueue(&frame)) {// 更新纹理并渲染updateYUVTextures(*frame);performRender();}}
}
4. EGL环境管理
- EGL显示初始化:获取并配置EGL显示环境
- Surface绑定:将EGL上下文绑定到Android Surface
- 资源自动清理:析构时自动释放EGL和OpenGL资源
使用示例
// 创建视频渲染器
auto videoRender = std::make_unique<VideoRender>();// 初始化渲染器(绑定到Android Surface)
if (!videoRender->init(env, surface, video_width, video_height)) {LOGE("Failed to initialize video renderer");return;
}// 设置YUV帧获取回调
videoRender->setYUVFrameGetCallback([&](YUVFrame** frame) -> bool {return yuvFrameQueue->tryDequeue(*frame); // 从队列获取帧
});// 设置渲染完成回调
videoRender->setRenderCompleteCallback([&](int64_t pts) {LOGD("Frame rendered, pts: %lld", pts);// 更新播放位置,同步控制等
});// 启动渲染线程
videoRender->start();
视频跳转的实现
设计概述
视频跳转功能实现了基于百分比进度的精确跳转,支持在播放时间轴上任意位置的快速定位。采用多层协同处理的架构,确保跳转过程的稳定性和准确性。
核心架构
Java Layer (0.0-1.0) → JNI Layer → Player Layer → 多组件协同跳转(百分比) (时间转换) (状态管理+清理+定位)
技术实现要点
百分比到时间戳转换
// Player::seek(double position) - position为百分比(0.0-1.0)
int Player::seek(double position) {// 参数验证:确保百分比在有效范围内if (position < 0.0 || position > 1.0) {LOGE(TAG, "Invalid seek position: %.2f (must be between 0.0 and 1.0)", position);return -1;}// 百分比转换为实际时间点double targetTimeSeconds = 0.0;if (duration > 0.0) {targetTimeSeconds = position * duration; // 关键转换逻辑} else {LOGE(TAG, "Cannot seek: duration is unknown");return -1;}LOGI(TAG, "Converting position %.2f%% to time %.2f seconds (duration: %.2f)", position * 100, targetTimeSeconds, duration);
多层组件协同工作流程
播放状态检查 → 组件暂停 → 缓冲区清理 → 文件跳转 → 状态重置 → 播放恢复↓ ↓ ↓ ↓ ↓ ↓
State Validate → Pause → Clear Queues → Demuxer Seek → Reset → Resume
核心实现步骤
第一步:缓冲区清理
// 清空数据包队列 - 避免旧数据干扰
if (packetQueue) {AVPacket* packet = nullptr;while (packetQueue->tryDequeue(packet)) {if (packet) {av_packet_free(&packet); // 释放内存}}
}// 清空YUV帧队列 - 避免旧帧显示
if (yuvFrameQueue) {VideoDecoder::YUVFrame* frame = nullptr;while (yuvFrameQueue->tryDequeue(frame)) {if (frame) {freeYUVFrame(frame); // 释放YUV帧内存}}
}
第二步:跳转
// 时间戳转换:秒 → 毫秒
int64_t timestamp_ms = static_cast<int64_t>(targetTimeSeconds * 1000);// FFmpeg文件跳转:使用BACKWARD标志跳转到关键帧
if (!demuxer || !demuxer->seek(timestamp_ms)) {LOGE(TAG, "Failed to seek to position %.2f seconds", targetTimeSeconds);return -1;
}
第三步:组件状态重置
// 解码器状态重置 - 清空内部缓冲区
if (videoDecoder) {videoDecoder->reset(); // 调用avcodec_flush_buffers()
}// 渲染器时间重置 - 重新初始化时间控制
if (videoRender) {videoRender->resetTiming(); // 重置first_pts和时间状态
}// 更新当前播放位置
currentPosition = targetTimeSeconds;
Demuxer层的跳转实现
bool Demuxer::seek(int64_t timestamp_ms) {if (!format_context_ || video_stream_index_ == -1) {DEFAULT_LOGE("Invalid state for seeking");return false;}// 转换为FFmpeg时间基int64_t seek_target = av_rescale_q(timestamp_ms, AV_TIME_BASE_Q, format_context_->streams[video_stream_index_]->time_base);// 跳转到关键帧(向后查找最近的I帧)int result = av_seek_frame(format_context_, video_stream_index_, seek_target, AVSEEK_FLAG_BACKWARD);if (result < 0) {DEFAULT_LOGE("av_seek_frame failed: %s", av_err2str(result));return false;}DEFAULT_LOGI("Successfully seeked to timestamp: %lld ms", timestamp_ms);return true;
}
JNI和Java层集成
JNI层处理:
JNIEXPORT jint JNICALL
Java_com_example_androidplayer_Player_nativeSeek(JNIEnv *env, jobject thiz, jdouble position) {LOGI(TAG, "nativeSeek called - position: %.2f%% (%.2f/1.0)", position * 100, position);Player* player = getPlayerFromJava(env, thiz);if (player == nullptr) {LOGE(TAG, "Player instance is null");return -1;}return player->seek(static_cast<double>(position));
}
Java层状态管理:
public void seek(double position) {// 设置Seeking状态PlayerState originalState = mState;mState = PlayerState.Seeking;int result = nativeSeek(position);if (result == 0) {// Seek成功,恢复原状态(除非原来是End状态)if (originalState != PlayerState.End) {mState = originalState;} else {mState = PlayerState.Paused; // Seek后暂停}} else {// Seek失败,恢复原状态mState = originalState;throw new RuntimeException("Seek failed with error code: " + result);}
}
遇到的问题和解决方案
编译着色器报错
解决方案
修改着色器关于版本的定义 把版本定义放在第一行
播放速度不对
原视频长度为47s 但是在播放器上播放的速度只有10几秒 显然时间对不上,因该是时间同步没做好
解决方案
增加帧率控制逻辑
Demuxer → 读取视频流信息 → 提取帧率(如25fps = 40ms/帧)↓
Player → 初始化VideoRender → 设置时间参数↓
VideoRender → 渲染帧 → 检查时间间隔 → 必要时Sleep等待 → 下一帧
具体做法:
先在解复用器中增加时间基和帧率的获取方法
// 新增方法
AVRational getVideoTimeBase() const; // 获取时间基
AVRational getVideoFrameRate() const; // 获取帧率
帧率都能理解,时间基是什么?
时间基也是一个分数,即1N\frac {1}{N}N1 在FFmpeg的语境中,表示一个时间单位,比如ffmpeg中pts是900090009000,时间基是190000\frac {1} {90000}900001 那么PTS 900090009000所代表的时间节点是
9000×190000=0.1s9000 \times \frac {1} {90000} = 0.1s 9000×900001=0.1s
这些信息可通过Avstream
获得,此外,帧率也可用通过计算得到具体的计算方式是:
FPS=1后一帧的时间戳−前一帧的时间戳\text{FPS}=\frac {1} {后一帧的时间戳-前一帧的时间戳} FPS=后一帧的时间戳−前一帧的时间戳1
在VideoRender
中,增加新的时间控制相关成员:
// 新增时间控制成员
AVRational video_time_base_; // 视频流时间基
AVRational video_frame_rate_; // 视频帧率
double frame_duration_ms_; // 每帧时长(毫秒)
std::chrono::steady_clock::time_point last_frame_time_; // 上帧时间
在player
中,核心的控制逻辑如下:
// 计算帧时长:1000ms * 分母 / 分子
frame_duration_ms_ = (1000.0 * frame_rate.den) / frame_rate.num;// 渲染节奏控制
auto elapsed_ms = current_time - last_frame_time_;
if (elapsed_ms < frame_duration_ms_) {int sleep_time = frame_duration_ms_ - elapsed_ms;std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
}
暂停时视频不是立即暂停
当使用暂停按钮时,视频仍会前进几帧,而不是立即暂停
解决方案
这个问题应该是在于控制逻辑,在暂停时,控制逻辑是
暂停解复用 -> 暂停解码器 -> 暂停渲染器
问题在于先暂停解复用 但是解码和渲染器仍在工作,因此仍会额外显示几帧的画面
因此需要修改控制逻辑先暂停渲染器 再暂停解复用器和解码器
暂停渲染器 -> 暂停解复用器 -> 暂停解码器
为什么要最后暂停解码器?
因为需要掐断数据的来源,否则会导致数据不断在buffer中累加
拖动进度条程序崩溃
当快速拖动进度条时,程序发生崩溃,问题日志
2025-07-11 15:13:00.391 7325-7355 AndroidPlayer com.example.androidplayer D Read audio packet: pts=73728, dts=73728, size=377
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A Cmdline: com.example.androidplayer
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A pid: 7325, tid: 7356, name: e.androidplayer >>> com.example.androidplayer <<<
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A #00 pc 0000000000450e49 /data/app/~~J8saOt6Ion-f0HPMtgkN2g==/com.example.androidplayer-4QPLC5VpmZsuER4HP2BCHg==/base.apk!libffmpeg-tlp.so (offset 0x58c000)
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A #01 pc 000000000044fff2 /data/app/~~J8saOt6Ion-f0HPMtgkN2g==/com.example.androidplayer-4QPLC5VpmZsuER4HP2BCHg==/base.apk!libffmpeg-tlp.so (offset 0x58c000)
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A #02 pc 000000000044f76f /data/app/~~J8saOt6Ion-f0HPMtgkN2g==/com.example.androidplayer-4QPLC5VpmZsuER4HP2BCHg==/base.apk!libffmpeg-tlp.so (offset 0x58c000) (ff_h264_execute_decode_slices+143)
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A #03 pc 00000000004587a4 /data/app/~~J8saOt6Ion-f0HPMtgkN2g==/com.example.androidplayer-4QPLC5VpmZsuER4HP2BCHg==/base.apk!libffmpeg-tlp.so (offset 0x58c000)
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A #04 pc 0000000000313ac8 /data/app/~~J8saOt6Ion-f0HPMtgkN2g==/com.example.androidplayer-4QPLC5VpmZsuER4HP2BCHg==/base.apk!libffmpeg-tlp.so (offset 0x58c000)
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A #05 pc 000000000031393c /data/app/~~J8saOt6Ion-f0HPMtgkN2g==/com.example.androidplayer-4QPLC5VpmZsuER4HP2BCHg==/base.apk!libffmpeg-tlp.so (offset 0x58c000) (avcodec_send_packet+188)
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A #06 pc 000000000008c6bb /data/app/~~J8saOt6Ion-f0HPMtgkN2g==/com.example.androidplayer-4QPLC5VpmZsuER4HP2BCHg==/base.apk!libandroidplayer.so (offset 0x1a8c000) (VideoDecoder::decodePacket(AVPacket*)+91) (BuildId: 7b04e0929eeb93706ef3d814e5b2f64706206645)
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A #07 pc 000000000008bf12 /data/app/~~J8saOt6Ion-f0HPMtgkN2g==/com.example.androidplayer-4QPLC5VpmZsuER4HP2BCHg==/base.apk!libandroidplayer.so (offset 0x1a8c000) (VideoDecoder::decoderThreadFunc()+402) (BuildId: 7b04e0929eeb93706ef3d814e5b2f64706206645)
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A #08 pc 000000000008d73d /data/app/~~J8saOt6Ion-f0HPMtgkN2g==/com.example.androidplayer-4QPLC5VpmZsuER4HP2BCHg==/base.apk!libandroidplayer.so (offset 0x1a8c000) (BuildId: 7b04e0929eeb93706ef3d814e5b2f64706206645)
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A #09 pc 000000000008d68d /data/app/~~J8saOt6Ion-f0HPMtgkN2g==/com.example.androidplayer-4QPLC5VpmZsuER4HP2BCHg==/base.apk!libandroidplayer.so (offset 0x1a8c000) (BuildId: 7b04e0929eeb93706ef3d814e5b2f64706206645)
2025-07-11 15:13:00.391 7394-7394 DEBUG crash_dump64 A #10 pc 000000000008d362 /data/app/~~J8saOt6Ion-f0HPMtgkN2g==/com.example.androidplayer-4QPLC5VpmZsuER4HP2BCHg==/base.apk!libandroidplayer.so (offset 0x1a8c000) (BuildId: 7b04e0929eeb93706ef3d814e5b2f64706206645)
2025-07-11 15:13:00.394 7325-7355 AndroidPlayer com.example.androidplayer D Read audio packet: pts=74752, dts=74752, size=364
2025-07-11 15:13:00.395 7325-7355 AndroidPlayer com.example.androidplayer D Read video packet: pts=20992, dts=20992, size=24
2025-07-11 15:13:00.398 7325-7355 AndroidPlayer com.example.androidplayer D Read audio packet: pts=75776, dts=75776, size=371
2025-07-11 15:13:00.399 7325-7355 AndroidPlayer com.example.androidplayer D Read audio packet: pts=76800, dts=76800, size=361
2025-07-11 15:13:00.404 7325-7355 AndroidPlayer com.example.androidplayer D Read video packet: pts=22016, dts=21504, size=24
2025-07-11 15:13:00.407 7325-7355 AndroidPlayer com.example.androidplayer D Read audio packet: pts=77824, dts=77824, size=385
2025-07-11 15:13:00.408 7325-7355 AndroidPlayer com.example.androidplayer D Read audio packet: pts=78848, dts=78848, size=390
2025-07-11 15:13:00.440 756-876 InputDispatcher system_server E channel 'ff66efe com.example.androidplayer/com.example.androidplayer.MainActivity' ~ Channel is unrecoverably broken and will be disposed!
---------------------------- PROCESS ENDED (7325) for package com.example.androidplayer ----------------------------
问题分析
出现这个问题的原因是当进度条变动时,就会调用seek
函数,而seek
会导致正在正常处理的线程触发重置操作,线程来不及释放资源,导致程序内存泄漏,从而崩溃。
原来的实现
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {if (fromUser) {player.seek((double) seekBar.getProgress() / 100);}
} //当发生进度条移动就seek 导致崩溃
解决方案
修改进度条拖动的逻辑,当用户离手时再触发seek
函数,当用户仍处于拖动状态时,不执行seek
。代码
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {if (fromUser) {// 用户拖动时只记录位置,不执行seekLog.i("onProgressChanged", "User dragging to: " + progress + "%");// 这里可以选择显示预览位置,但不实际跳转}}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {Log.i("SeekBar", "User started dragging");isUserSeeking = true;}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {Log.i("SeekBar", "User stopped dragging, seeking to: " + seekBar.getProgress() + "%");isUserSeeking = false;// 只在用户放手时执行seek操作player.seek((double) seekBar.getProgress() / 100);}
});
}
调用stop函数 发生了死循环
日志信息
2025-07-11 15:52:01.541 8251-8251 AndroidPlayer com.example.androidplayer I Stop button clicked
2025-07-11 15:52:01.541 8251-8251 Player com.example.androidplayer I Stopping playback
2025-07-11 15:52:01.541 8251-8251 NativeLib com.example.androidplayer I nativeStop called
2025-07-11 15:52:01.541 8251-8251 Native Player com.example.androidplayer I Player::stop() called
2025-07-11 15:52:01.541 8251-8251 VideoRender com.example.androidplayer I Stopping VideoRender
2025-07-11 15:52:01.565 8251-8280 VideoRender com.example.androidplayer I OpenGL resources cleaned up
2025-07-11 15:52:01.568 8251-8280 VideoRender com.example.androidplayer I EGL resources cleaned up
2025-07-11 15:52:01.568 8251-8280 VideoRender com.example.androidplayer I Render thread finished, rendered frames: 119, dropped frames: 0
2025-07-11 15:52:01.576 8251-8251 VideoRender com.example.androidplayer I VideoRender state changed to: 4
2025-07-11 15:52:01.576 8251-8251 VideoRender com.example.androidplayer I VideoRender stopped
2025-07-11 15:52:01.576 8251-8251 Native Player com.example.androidplayer I VideoRender stopped
2025-07-11 15:52:01.576 8251-8251 VideoDecoder com.example.androidplayer I Stopping VideoDecoder
从日志中发现,没有打印Player Stoped
,说明在调用stop函数时,发生了阻塞。经过分析,发现问题在于线程的stop方式,当player调用某个组件的stop方法时,该线程会使用join等待数据全部消耗完,但是由于player的stop逻辑是先停渲染器,再停别的,这会导致帧缓冲不断有新帧进入,但是又没有消耗手段,从而引发阻塞。
解决方案
player stop时,先暂停,清空所有缓冲队列,然后在stop线程时,引入超时,当超过指定时间之后,直接结束线程。
演示视频
功能说明:
- 正常播放视频,无花屏,速度正常,经统计,原视频47s 演示视频中播放时间也是47s
- 暂停功能 立即暂停 无延迟
- 进度条拖动并跳转 点击跳转
day1
整体流程图
交叉编译libffmpeg-tlp.so文件
思路是先将所有的库编译成.a静态库 然后链接成so动态库 编译脚本在根目录下的script
中,名字为buildx86_64.sh
。
编译报错
with -fPIC
/home/ubuntu2204/Android/Sdk/ndk/21.3.6528147/toolchains/llvm/prebuilt/linux-x86_64/bin/../lib/gcc/x86_64-linux-android/4.9.x/../../../../x86_64-linux-android/bin/ld: error: ../ffmpeg-android/x86_64/lib/libavcodec.a(vp9lpf_16bpp.o): requires dynamic R_X86_64_PC32 reloc against 'ff_pw_m1' which may overflow at runtime; recompile with -fPIC
/home/ubuntu2204/Android/Sdk/ndk/21.3.6528147/toolchains/llvm/prebuilt/linux-x86_64/bin/../lib/gcc/x86_64-linux-android/4.9.x/../../../../x86_64-linux-android/bin/ld: error: ../ffmpeg-android/x86_64/lib/libavcodec.a(vp9mc.o): requires dynamic R_X86_64_PC32 reloc against 'ff_pw_64' which may overflow at runtime; recompile with -fPIC
/home/ubuntu2204/Android/Sdk/ndk/21.3.6528147/toolchains/llvm/prebuilt/linux-x86_64/bin/../lib/gcc/x86_64-linux-android/4.9.x/../../../../x86_64-linux-android/bin/ld: error: ../ffmpeg-android/x86_64/lib/libavcodec.a(vp9mc_16bpp.o): requires dynamic R_X86_64_PC32 reloc against 'ff_pw_1023' which may overflow at runtime; recompile with -fPIC
/home/ubuntu2204/Android/Sdk/ndk/21.3.6528147/toolchains/llvm/prebuilt/linux-x86_64/bin/../lib/gcc/x86_64-linux-android/4.9.x/../../../../x86_64-linux-android/bin/ld: error: ../ffmpeg-android/x86_64/lib/libswscale.a(rgb2rgb.o): requires dynamic R_X86_64_PC32 reloc against 'ff_w1111' which may overflow at runtime; recompile with -fPIC
/home/ubuntu2204/Android/Sdk/ndk/21.3.6528147/toolchains/llvm/prebuilt/linux-x86_64/bin/../lib/gcc/x86_64-linux-android/4.9.x/../../../../x86_64-linux-android/bin/ld: error: ../ffmpeg-android/x86_64/lib/libswscale.a(swscale.o): requires dynamic R_X86_64_PC32 reloc against 'ff_M24A' which may overflow at runtime; recompile with -fPIC
/home/ubuntu2204/Android/Sdk/ndk/21.3.6528147/toolchains/llvm/prebuilt/linux-x86_64/bin/../lib/gcc/x86_64-linux-android/4.9.x/../../../../x86_64-linux-android/bin/ld: warning: shared library text segment is not shareable
clang: error: linker command failed with exit code 1 (use -v to see invocation)
查找到的原因:不明,猜测和虚拟机的CPU模拟的指令集有关系
**解决方案:**禁用相关的指令集优化 在config中添加配置
--disable-asm \
--disable-mmx \
--disable-mmxext \
--disable-sse \
--disable-sse2 \
--disable-sse3 \
--disable-ssse3 \
--disable-sse4 \
--disable-sse42 \
--disable-avx \
--disable-avx2 \
--disable-inline-asm
编译结果
INSTALL libavutil/stereo3d.h
INSTALL libavutil/threadmessage.h
INSTALL libavutil/time.h
INSTALL libavutil/timecode.h
INSTALL libavutil/timestamp.h
INSTALL libavutil/tree.h
INSTALL libavutil/twofish.h
INSTALL libavutil/version.h
INSTALL libavutil/video_enc_params.h
INSTALL libavutil/xtea.h
INSTALL libavutil/tea.h
INSTALL libavutil/tx.h
INSTALL libavutil/film_grain_params.h
INSTALL libavutil/lzo.h
INSTALL libavutil/avconfig.h
INSTALL libavutil/ffversion.h
INSTALL libavutil/libavutil.pc
直接Android 项目运行发现报错,没有log.h文件
代码中关于log的部分都标红了,因此log.h中应该只是一个关于安卓日志方法的宏定义,增加log.h
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
再次编译执行,已经正常,无报错
设计项目结构
本项目是一个基于FFmpeg的Android视频播放器,采用分层架构设计,主要包含以下几个核心部分:
项目总体架构
AndroidPlayer (Android App)├── Java层 (应用逻辑层)│ ├── MainActivity.java # 主界面活动,负责UI交互│ └── Player.java # 播放器封装类,提供播放控制接口│├── JNI层 (桥接层)│ ├── native_lib.cpp # JNI方法实现入口│ ├── AAudioRender.cpp/h # 音频渲染模块│ └── ANWRender.cpp/h # 视频渲染模块 (Android Native Window)│├── Native层 (核心处理层)│ ├── audio/ # 音频处理模块│ ├── video/ # 视频处理模块 │ ├── demuxer/ # 解封装模块│ └── util/ # 工具类模块│└── FFmpeg库└── libffmpeg-tlp.so # 编译后的FFmpeg动态库
JNI层详细目录结构
app/src/main/cpp/
├── CMakeLists.txt # CMake构建配置文件
├── native_lib.cpp # JNI方法入口文件
├── AAudioRender.cpp # 音频渲染实现
├── ANWRender.cpp # 视频渲染实现
├── include/ # 头文件目录
│ ├── AAudioRender.h # 音频渲染类头文件
│ ├── ANWRender.h # 视频渲染类头文件
│ ├── log.h # Android日志宏定义
│ └── libavcodec/ # FFmpeg libavcodec 头文件
│ └── libavdevice/ # FFmpeg libavdevice 头文件
│ └── libavfilter/ # FFmpeg libavfilter 头文件
│ └── libavformat/ # FFmpeg libavformat 头文件
│ └── libavutil/ # FFmpeg libavutil 头文件
│ └── libswresample/ # FFmpeg libswresample 头文件
│ └── libswscale/ # FFmpeg libswscale 头文件
└── src/ # 预留的功能模块目录(当前为空)├── audio/ # 音频处理模块(预留)├── video/ # 视频处理模块(预留)├── demuxer/ # 解封装模块(预留)└── util/ # 工具模块(预留)
上传视频文件到虚拟安卓机 并检查文件是否可用
运行结果显示 文件可用 因此在文件读取时 不会有问题
线程安全队列的实现
实现思路:使用一个固定大小的数组作为缓冲区,定义head
永远指向队列的头部 和 tail
指向尾部 每次增加一个元素 tail+1
每次出队 head+1
使用模运算确保tail
和head
不会越界。
队列测试
在play中增加了test的native方法,然后在native中调用刚刚实现的队列 发现无法编译,具体的报错是
解决方案1:
在queue模板的定义中增加显示的模板实例化
// 显式模板实例化 - 这一行非常重要!
template class ThreadSafeQueue<int>;
为什么需要显示的模板实例化?
因为i,编译器只能在同一个编译单元中同时看到模板声明和实现时,才能生成具体类型的代码。在我的实现中,我在include中定义了模板头文件,但是实现却放在了另外一个文件中,对于queue的模板定义和实现是分离的,因此需要在queue.cpp
中显示指定模板实例化。
更好的解决方案
把queue.cpp
实现放在queue.h
中这样就不用再queue.cpp
中显示指定模板实例化了。 这和STL的设计方法是一致的,STL也是把模板和实现放在了一起。
测试结果
logcat中正确打印了测试值。
解复用器demuxer类的的设计和实现
设计思路
解复用器(Demuxer
)是视频播放器的核心组件之一,负责从媒体文件中分离出视频流数据包。本项目中解复用器采用独立线程+生产者模式的设计:
- 独立线程运行:解复用器在专门的线程中持续工作,不阻塞主线程和UI
- 生产者角色:专门负责读取和生产视频数据包,为后续的解码器提供数据源
- 状态机管理:使用完整的状态机控制解复用器的生命周期
- 队列缓冲:预留队列缓冲器接口,实现与解码器的异步数据传输
核心架构
MediaFile → Demuxer Thread → Video Packets → Queue Buffer → Decoder(Producer) (Consumer)
核心接口
class Demuxer {
public:// 文件操作bool openFile(const std::string& file_path);void closeFile();// 线程控制bool start(); // 启动解复用线程void stop(); // 停止线程void pause(); // 暂停读取void resume(); // 恢复读取// 状态查询State getState() const;bool isRunning() const;// 信息获取int getVideoStreamIndex() const;AVCodecParameters* getVideoCodecParameters() const;int64_t getDurationMs() const;// 数据输出(预留接口)void setVideoPacketCallback(VideoPacketCallback callback);
};
实现细节
- 线程主循环(
demuxerThreadFunc
):- 检查暂停状态,使用条件变量等待
- 调用
av_read_frame
读取数据包 - 过滤非视频包,只处理视频流(目前只考虑视频流)
- 通过回调函数输出视频包(预留队列缓冲器接口,之后可用再别的模块中添加)
- 处理文件结束和错误情况
- 资源管理:
- 构造函数初始化所有成员变量
- 析构函数自动调用
stop()
和closeFile()
cleanup()
方法负责释放FFmpeg资源
- 扩展性设计:
VideoPacketCallback
回调类型为std::function<void(AVPacket*)>
- 在TODO注释处预留队列缓冲器连接点
- 支持后续添加音频流处理
解复用器的使用方法
解复用器采用独立线程+回调的设计模式,使用流程包含初始化、启动、控制和清理四个阶段:
基本使用流程
1. 创建和初始化
// 创建解复用器对象
std::unique_ptr<Demuxer> demuxer = std::make_unique<Demuxer>();// 打开媒体文件
std::string filePath = "/sdcard/test_video.mp4";
if (!demuxer->openFile(filePath)) {LOGE("Failed to open file: %s", filePath.c_str());return false;
}// 获取视频流信息
int videoStreamIndex = demuxer->getVideoStreamIndex();
AVCodecParameters* codecpar = demuxer->getVideoCodecParameters();
int64_t duration = demuxer->getDurationMs();
LOGI("Video stream: %d, Duration: %lld ms", videoStreamIndex, duration);
2. 设置数据包回调
// 设置视频包处理回调(连接到队列缓冲器)
demuxer->setVideoPacketCallback([this](AVPacket* packet) {// 复制数据包并放入线程安全队列AVPacket* packet_copy = av_packet_alloc();if (av_packet_ref(packet_copy, packet) == 0) {if (!packetQueue->enqueue(packet_copy)) {// 队列满了,丢弃数据包av_packet_free(&packet_copy);LOGW("Packet queue full, dropping packet");}}
});
3. 启动解复用线程
// 启动解复用器
if (!demuxer->start()) {LOGE("Failed to start demuxer");return false;
}LOGI("Demuxer started, state: %d", static_cast<int>(demuxer->getState()));
// 此时解复用器开始在后台线程中持续读取视频包
4. 运行时控制
// 暂停解复用(线程仍运行,但停止读取数据包)
demuxer->pause();// 恢复解复用
demuxer->resume();// 检查运行状态
if (demuxer->isRunning()) {LOGI("Demuxer is running");
}// 获取当前状态
Demuxer::State state = demuxer->getState();
5. 停止和清理
// 停止解复用器(停止线程并等待完成)
demuxer->stop();// 关闭文件(自动清理FFmpeg资源)
demuxer->closeFile();// 解复用器对象析构时会自动调用stop()和closeFile()
player的设计
Player类采用状态机+组件化设计,作为播放器的核心控制器:
核心组件
class Player {enum PlayerState { IDLE, PLAYING, PAUSED, STOPPED, ERROR };// 核心方法int setDataSource(const std::string& filePath); // 设置数据源int setSurface(JNIEnv* env, jobject surface); // 设置视频渲染表面int play(); // 开始播放int pause(); // 暂停播放 int stop(); // 停止播放// 状态和信息PlayerState getState() const;double getDuration() const;double getPosition() const;
};
java Player对象和c++ player对象绑定
采用JNI对象绑定模式,实现Java层和C++层的一对一映射:
绑定机制
// Java层
public class Player {private long nativeContext; // 存储C++对象指针private native int nativePlay(String file, Surface surface);private native void nativePause(boolean p);
}
// C++层 - JNI实现
static Player* getPlayerFromJava(JNIEnv* env, jobject thiz) {jlong ptr = env->GetLongField(thiz, nativeContextField);return reinterpret_cast<Player*>(ptr); // 指针转换
}JNIEXPORT jint JNICALL
Java_com_example_androidplayer_Player_nativePlay(JNIEnv *env, jobject thiz, jstring file, jobject surface) {Player* player = getPlayerFromJava(env, thiz); // 获取绑定的C++对象// 设置数据源、Surface,调用play()
}
为什么这样设计
- 一对一绑定:每个Java Player对应唯一的C++ Player实例
- 状态一致性:操作的始终是同一个C++对象,避免状态混乱
- 生命周期管理:通过nativeContext管理C++对象生命周期
解码器模块的设计
VideoDecoder采用独立线程+消费者模式+YUV输出的设计,负责将视频数据包解码为标准YUV格式:
核心架构
外部PacketQueue → VideoDecoder Thread → YUV420P Frames → 渲染器(消费者) (生产者)
头文件核心设计
class VideoDecoder {enum class State { IDLE, PREPARING, RUNNING, PAUSED, STOPPED, FLUSHING, ERROR };// YUV帧输出结构struct YUVFrame {uint8_t* y_data, *u_data, *v_data; // YUV分量数据int y_linesize, u_linesize, v_linesize; // 行大小int width, height; // 尺寸int64_t pts; // 时间戳};// 核心方法bool init(AVCodecParameters* codecpar, int target_width=0, int target_height=0);void setPacketGetCallback(PacketGetCallback callback); // 从外部队列获取数据包void setYUVFrameCallback(YUVFrameCallback callback); // 输出YUV帧
};
设计思路
解码器从外部队列中获取packet 并解码为YUV格式的视频帧 然后将视频帧放入一个新的缓冲区,以供渲染器使用
Player解码流程实现
Player作为核心控制器,整合了Demuxer、VideoDecoder和ThreadSafeQueue,实现了完整的双线程解码流程并自动保存YUV数据到文件。
完整架构流程
MediaFile → Demuxer Thread → ThreadSafeQueue<AVPacket*> → VideoDecoder Thread → YUV File(生产者) (消费者) (/sdcard/test-tlp.yuv)
核心实现流程
1. 初始化阶段 (initializePlayer
)
// 创建线程安全数据包队列
packetQueue = std::make_unique<ThreadSafeQueue<AVPacket*>>(100);// 初始化解复用器
demuxer->openFile(dataSource);
demuxer->setVideoPacketCallback([this](AVPacket* packet) {this->onVideoPacket(packet); // 数据包放入队列
});// 初始化解码器
videoDecoder->init(codecpar);
videoDecoder->setPacketGetCallback([this](AVPacket** packet) -> bool {return this->getPacketFromQueue(packet); // 从队列获取数据包
});
videoDecoder->setYUVFrameCallback([this](const YUVFrame& frame) {this->onYUVFrame(frame); // 保存YUV帧到文件
});
2. 播放启动 (play
)
// 启动解复用线程
demuxer->start(); // 开始读取视频文件// 启动解码线程
videoDecoder->start(); // 开始解码处理
3. 数据流处理
-
生产者流程(解复用线程):
av_read_frame() → AVPacket → onVideoPacket() → av_packet_ref() → packetQueue->enqueue()
-
消费者流程(解码线程):
packetQueue->tryDequeue() → avcodec_send_packet() → avcodec_receive_frame() → sws_scale() → YUVFrame → onYUVFrame() → writeYUVFrame()
4. YUV文件写入
void writeYUVFrame(const YUVFrame& frame) {// 按YUV420P格式写入:Y分量 + U分量 + V分量// Y: width × height// U: (width/2) × (height/2) // V: (width/2) × (height/2)for (int i = 0; i < frame.height; i++) {yuvFile.write(frame.y_data + i * frame.y_linesize, frame.width);}// ... U和V分量写入
}
关键特性
- 双线程并行:解复用和解码完全异步,提高处理效率
- 线程安全队列:使用
tryDequeue()
非阻塞方式避免死锁 - 内存管理:
av_packet_ref()
复制数据包,av_packet_free()
自动释放 - 格式转换:任意输入格式 → YUV420P标准输出
使用效果
运行后会在/sdcard/test-tlp.yuv
生成标准YUV420P文件,将生成的YUV文件上传到虚拟机内,使用工具播放验证,这里需要注意的一点是分辨率要和原视频分辨率一致 否则播放会花屏:
# 使用ffplay播放YUV文件
ffplay -f rawvideo -pixel_format yuv420p -video_size 1024x436 test-tlp.yuv