当前位置: 首页 > news >正文

【小米训练营】C++方向 实践项目 Android Player

note:本人使用的是android studio的虚拟安卓 架构是x86_64 无法直接在真机上运行

day3

演示视频

Screenrecording_20250712_175548.mp4
Screenrecording_20250712_194057.mp4

如果看不到视频,视频文件在Readme.assets/Screenrecording_20250712_170055.mp4Readme.assets/Screenrecording_20250712_175548.mp4Readme.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线程时,引入超时,当超过指定时间之后,直接结束线程。

演示视频

功能说明:

  1. 正常播放视频,无花屏,速度正常,经统计,原视频47s 演示视频中播放时间也是47s
  2. 暂停功能 立即暂停 无延迟
  3. 进度条拖动并跳转 点击跳转

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 使用模运算确保tailhead不会越界。

队列测试

在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);
};
实现细节
  1. 线程主循环(demuxerThreadFunc):
    • 检查暂停状态,使用条件变量等待
    • 调用av_read_frame读取数据包
    • 过滤非视频包,只处理视频流(目前只考虑视频流)
    • 通过回调函数输出视频包(预留队列缓冲器接口,之后可用再别的模块中添加)
    • 处理文件结束和错误情况
  2. 资源管理
    • 构造函数初始化所有成员变量
    • 析构函数自动调用stop()closeFile()
    • cleanup()方法负责释放FFmpeg资源
  3. 扩展性设计
    • 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

如果视频播放不了,视频文件在Readme.assets文件夹中。

http://www.dtcms.com/a/278460.html

相关文章:

  • C++ 左值右值、左值引用右值引用、integral_constant、integral_constant的元模板使用案例
  • 量子计算新突破!阿里“太章3.0”实现512量子比特模拟(2025中国量子算力巅峰)
  • ethers.js-5–和solidity的关系
  • RPC 框架学习笔记
  • Spark 之 like 表达式
  • 软件测试中的BUG等级与生命周期详解
  • 走近科学IT版:EasyTire设置了ip,但是一闪之后就变回到原来的dhcp获得的地址
  • ros2版本自定义插件的实现与热插拔
  • 设计模式(行为型)-迭代器模式
  • java 判断两个集合中没有重复元素
  • iOS高级开发工程师面试——Objective-C 语言特性
  • Linux(Ubuntu)硬盘使用情况解析(已房子举例)
  • rk3588ubuntu 系统移植AIC8800D Wi-Fi6/BT5.0芯片
  • EMQX + Amazon S3 Tables:从实时物联网数据到数据湖仓
  • C++函数指针
  • Redis作缓存时存在的问题及其解决方案
  • 云原生核心技术解析:Docker vs Kubernetes vs Docker Compose
  • Word 与 Excel 下拉菜单对比(附示例下载)
  • 前端将传回的List数据组织成树形数据并展示
  • MEMS IMU如何赋能无人机与机器人精准感知?
  • 跨膜粘蛋白MUC17
  • MAC安装虚拟机
  • UE5多人MOBA+GAS 22、创建技能图标UI,实现显示蓝耗,冷却,以及数字显示的倒数计时还有雷达显示的倒数计时
  • IDEA中使用Servlet,tomcat输出中文乱码
  • ubuntu22.04下配置qt5.15.17开发环境
  • Kotlin委托
  • 【Python】基础语法
  • 亚马逊新规!7月13日起合规性文件须出自符合要求的实验室!
  • 【飞牛云fnOS】告别数据孤岛:飞牛云fnOS私人资料管家
  • 【Hadoop科普篇】大数据怎么处理?Hadoop是什么?跟HDFS, Spark, Flink, Hive, Hbase是什么关系?