【小米训练营】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帧数据结构

cpp 复制代码
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. 音频重采样处理

cpp 复制代码
// 使用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. 环形缓冲区写入

cpp 复制代码
// 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回调读取

cpp 复制代码
// 从环形缓冲区读取音频数据用于播放
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. 音频主时钟策略

cpp 复制代码
// 音频播放位置作为基准时钟
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. 视频同步逻辑

cpp 复制代码
// 计算音视频时间差
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. 时间基转换

cpp 复制代码
// 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. 帧率控制

cpp 复制代码
// 视频渲染器中的帧率控制
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水印渲染

cpp 复制代码
// 水印着色器
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. 智能图片缓存系统

cpp 复制代码
// 线程安全的全局缓存
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图片加载

cpp 复制代码
// 支持多种图片格式,自动转换为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. 渲染混合

cpp 复制代码
// 启用透明度混合
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 复制代码
// Java层调用
player.setWatermark("/sdcard/watermark.png", 0.8f, 0.8f, 0.2f, 0.8f);
player.clearWatermark();
cpp 复制代码
// C++层控制
videoRender->setWatermark(watermark_path, x, y, scale, alpha);
遇到的问题

代码中设置的是正方形水印

cpp 复制代码
    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的坐标系会被视频拉伸?

解决方案

缩放宽和高,原视频是根据原视频的比例 相对应的放大或者缩小长宽

cpp 复制代码
// 根据视频宽高比调整水印尺寸,保持正方形 这里要除以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前的帧:

cpp 复制代码
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:

cpp 复制代码
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协调流程

cpp 复制代码
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层播放速度管理

cpp 复制代码
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. 音频倍速播放实现

采用线性插值重采样算法在音频回调中实时处理:

cpp 复制代码
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. 视频倍速播放实现

通过动态调整帧间隔实现视频倍速:

cpp 复制代码
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. 音视频同步保持

倍速播放时的同步策略:

cpp 复制代码
// 音频:通过重采样和时间调整保持倍速
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交互实现

提供丰富的倍速选择界面:

java 复制代码
// 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帧数据结构(与解码器保持一致)

cpp 复制代码
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. 回调函数设计

cpp 复制代码
// YUV帧获取回调(从外部队列获取YUV帧)
using YUVFrameGetCallback = std::function<bool(YUVFrame**)>;

// 渲染完成回调
using RenderCompleteCallback = std::function<void(int64_t pts)>;

3. OpenGL ES渲染管道

cpp 复制代码
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)

glsl 复制代码
// 片段着色器中的转换算法 (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. 线程安全设计

cpp 复制代码
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资源
使用示例
cpp 复制代码
// 创建视频渲染器
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 → 多组件协同跳转
                      (百分比)     (时间转换)     (状态管理+清理+定位)
技术实现要点

百分比到时间戳转换

cpp 复制代码
// 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
核心实现步骤

第一步:缓冲区清理

cpp 复制代码
// 清空数据包队列 - 避免旧数据干扰
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帧内存
        }
    }
}

第二步:跳转

cpp 复制代码
// 时间戳转换:秒 → 毫秒
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;
}

第三步:组件状态重置

cpp 复制代码
// 解码器状态重置 - 清空内部缓冲区
if (videoDecoder) {
    videoDecoder->reset();  // 调用avcodec_flush_buffers()
}

// 渲染器时间重置 - 重新初始化时间控制
if (videoRender) {
    videoRender->resetTiming();  // 重置first_pts和时间状态
}

// 更新当前播放位置
currentPosition = targetTimeSeconds;
Demuxer层的跳转实现
cpp 复制代码
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层处理

cpp 复制代码
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层状态管理

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等待 → 下一帧

具体做法:

先在解复用器中增加时间基和帧率的获取方法

cpp 复制代码
// 新增方法
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中,增加新的时间控制相关成员:

cpp 复制代码
// 新增时间控制成员
AVRational video_time_base_;                    // 视频流时间基
AVRational video_frame_rate_;                   // 视频帧率  
double frame_duration_ms_;                      // 每帧时长(毫秒)
std::chrono::steady_clock::time_point last_frame_time_; // 上帧时间

player中,核心的控制逻辑如下:

cpp 复制代码
// 计算帧时长: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中累加

拖动进度条程序崩溃

当快速拖动进度条时,程序发生崩溃,问题日志

sh 复制代码
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会导致正在正常处理的线程触发重置操作,线程来不及释放资源,导致程序内存泄漏,从而崩溃。

原来的实现

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

解决方案

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

java 复制代码
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 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

编译报错
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中添加配置

sh 复制代码
--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
编译结果
sh 复制代码
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

cpp 复制代码
#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模板的定义中增加显示的模板实例化

cpp 复制代码
// 显式模板实例化 - 这一行非常重要!
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)
核心接口
cpp 复制代码
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. 创建和初始化

cpp 复制代码
// 创建解复用器对象
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. 设置数据包回调

cpp 复制代码
// 设置视频包处理回调(连接到队列缓冲器)
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. 启动解复用线程

cpp 复制代码
// 启动解复用器
if (!demuxer->start()) {
    LOGE("Failed to start demuxer");
    return false;
}

LOGI("Demuxer started, state: %d", static_cast<int>(demuxer->getState()));
// 此时解复用器开始在后台线程中持续读取视频包

4. 运行时控制

cpp 复制代码
// 暂停解复用(线程仍运行,但停止读取数据包)
demuxer->pause();

// 恢复解复用
demuxer->resume();

// 检查运行状态
if (demuxer->isRunning()) {
    LOGI("Demuxer is running");
}

// 获取当前状态
Demuxer::State state = demuxer->getState();

5. 停止和清理

cpp 复制代码
// 停止解复用器(停止线程并等待完成)
demuxer->stop();

// 关闭文件(自动清理FFmpeg资源)
demuxer->closeFile();

// 解复用器对象析构时会自动调用stop()和closeFile()
player的设计

Player类采用状态机+组件化设计,作为播放器的核心控制器:

核心组件
cpp 复制代码
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 复制代码
// Java层
public class Player {
    private long nativeContext;  // 存储C++对象指针
    
    private native int nativePlay(String file, Surface surface);
    private native void nativePause(boolean p);
}
cpp 复制代码
// 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 → 渲染器
              (消费者)                 (生产者)
头文件核心设计
cpp 复制代码
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)

cpp 复制代码
// 创建线程安全数据包队列
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)

cpp 复制代码
// 启动解复用线程
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文件写入

cpp 复制代码
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文件上传到虚拟机内,使用工具播放验证,这里需要注意的一点是分辨率要和原视频分辨率一致 否则播放会花屏:

bash 复制代码
# 使用ffplay播放YUV文件
ffplay -f rawvideo -pixel_format yuv420p -video_size 1024x436 test-tlp.yuv

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

相关推荐
Reggie_L3 分钟前
网络编程-java
java·开发语言·网络
HHRL-yx21 分钟前
C++网络编程 2.TCP套接字(socket)编程详解
网络·c++·tcp/ip
小灰灰搞电子42 分钟前
Qt Quick 粒子系统详解
开发语言·qt
wadesir1 小时前
Python获取网页乱码问题终极解决方案 | Python爬虫编码处理指南
开发语言·爬虫·python
As_wind_1 小时前
Go 语言学习之测试
开发语言·学习·golang
望获linux1 小时前
【Linux基础知识系列】第五十四篇 - 网络协议基础:TCP/IP
java·linux·服务器·开发语言·架构·操作系统·嵌入式软件
刚入坑的新人编程1 小时前
暑期算法训练.3
c++·算法
liupenglove1 小时前
云端【多维度限流】技术方案设计,为服务稳定保驾护航
java·开发语言·网络
平哥努力学习ing2 小时前
C语言内存函数
c语言·开发语言·算法
科大饭桶2 小时前
数据结构自学Day8: 堆的排序以及TopK问题
数据结构·c++·算法·leetcode·二叉树·c