> 技术文档 > 【小米训练营】C++方向 实践项目 Android Player_小米训练营播放器

【小米训练营】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图片加载

// 支持多种图片格式,自动转换为RGBAunsigned 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_;  // 是否正在精确seekstd::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 esprecision 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 JNICALLJava_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; // 获取帧率

帧率都能理解,时间基是什么?

时间基也是一个分数,即 1 N \\frac {1}{N} N1 在FFmpeg的语境中,表示一个时间单位,比如ffmpeg中pts是 9000 9000 9000,时间基是 1 90000 \\frac {1} {90000} 900001 那么PTS 9000 9000 9000所代表的时间节点是
9000 × 1 90000 = 0.1 s 9000 \\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=3772025-07-11 15:13:00.391 7394-7394 DEBUG  crash_dump64 A Cmdline: com.example.androidplayer2025-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=3642025-07-11 15:13:00.395 7325-7355 AndroidPlayer  com.example.androidplayer D Read video packet: pts=20992, dts=20992, size=242025-07-11 15:13:00.398 7325-7355 AndroidPlayer  com.example.androidplayer D Read audio packet: pts=75776, dts=75776, size=3712025-07-11 15:13:00.399 7325-7355 AndroidPlayer  com.example.androidplayer D Read audio packet: pts=76800, dts=76800, size=3612025-07-11 15:13:00.404 7325-7355 AndroidPlayer  com.example.androidplayer D Read video packet: pts=22016, dts=21504, size=242025-07-11 15:13:00.407 7325-7355 AndroidPlayer  com.example.androidplayer D Read audio packet: pts=77824, dts=77824, size=3852025-07-11 15:13:00.408 7325-7355 AndroidPlayer  com.example.androidplayer D Read audio packet: pts=78848, dts=78848, size=3902025-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会导致正在正常处理的线程触发重置操作,线程来不及释放资源,导致程序内存泄漏,从而崩溃。

原来的实现

@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { player.seek((double) seekBar.getProgress() / 100); }} //当发生进度条移动就seek 导致崩溃

解决方案

修改进度条拖动的逻辑,当用户离手时再触发seek函数,当用户仍处于拖动状态时,不执行seek。代码

mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { // 用户拖动时只记录位置,不执行seek Log.i(\"onProgressChanged\", \"User dragging to: \" + progress + \"%\"); // 这里可以选择显示预览位置,但不实际跳转 } } @Override public void onStartTrackingTouch(SeekBar seekBar) { Log.i(\"SeekBar\", \"User started dragging\"); isUserSeeking = true; } @Override public 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 clicked2025-07-11 15:52:01.541 8251-8251 Player  com.example.androidplayer I Stopping playback2025-07-11 15:52:01.541 8251-8251 NativeLib  com.example.androidplayer I nativeStop called2025-07-11 15:52:01.541 8251-8251 Native Player  com.example.androidplayer I Player::stop() called2025-07-11 15:52:01.541 8251-8251 VideoRender com.example.androidplayer I Stopping VideoRender2025-07-11 15:52:01.565 8251-8280 VideoRender com.example.androidplayer I OpenGL resources cleaned up2025-07-11 15:52:01.568 8251-8280 VideoRender com.example.androidplayer I EGL resources cleaned up2025-07-11 15:52:01.568 8251-8280 VideoRender com.example.androidplayer I Render thread finished, rendered frames: 119, dropped frames: 02025-07-11 15:52:01.576 8251-8251 VideoRender com.example.androidplayer I VideoRender state changed to: 42025-07-11 15:52:01.576 8251-8251 VideoRender com.example.androidplayer I VideoRender stopped2025-07-11 15:52:01.576 8251-8251 Native Player  com.example.androidplayer I VideoRender stopped2025-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 shareableclang: 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.hINSTALL libavutil/threadmessage.hINSTALL libavutil/time.hINSTALL libavutil/timecode.hINSTALL libavutil/timestamp.hINSTALL libavutil/tree.hINSTALL libavutil/twofish.hINSTALL libavutil/version.hINSTALL libavutil/video_enc_params.hINSTALL libavutil/xtea.hINSTALL libavutil/tea.hINSTALL libavutil/tx.hINSTALL libavutil/film_grain_params.hINSTALL libavutil/lzo.hINSTALL libavutil/avconfig.hINSTALL libavutil/ffversion.hINSTALL 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
    • 在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 JNICALLJava_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 → 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文件夹中。