一、为什么需要上传进度回调

在嵌入式设备开发中,文件上传是个再常见不过的需求了。想象一下,你正在开发一个智能摄像头,需要把录制的视频上传到云端。如果用户盯着屏幕看了半天,连上传了多少都不知道,那体验得多糟糕啊!这就好比你去餐厅吃饭,服务员端了盘菜上来,却死活不告诉你还要等多久才能上齐。

OBS(对象存储服务)作为常用的云存储方案,提供了完善的上传接口。但默认情况下,它并不会主动告诉你上传进度。这就需要我们开发回调函数,让设备能够实时汇报"我现在传了多少"。

二、C++实现OBS上传进度回调

2.1 基本回调函数结构

我们先来看一个最简单的回调函数实现(技术栈:C++17 + OBS SDK):

/**
 * 上传进度回调函数
 * @param uploaded 已上传字节数
 * @param total 文件总字节数
 * @param userData 用户自定义数据指针
 */
void UploadProgressCallback(int64_t uploaded, int64_t total, void* userData) {
    // 计算上传百分比
    double percentage = (total > 0) ? (uploaded * 100.0 / total) : 0;
    
    // 这里假设userData是设备显示屏对象指针
    auto display = static_cast<DeviceDisplay*>(userData);
    if (display) {
        display->UpdateProgress(percentage); // 更新设备显示
    }
    
    // 同时输出到日志
    std::cout << "上传进度: " << std::fixed << std::setprecision(2) 
              << percentage << "%" << std::endl;
}

2.2 集成到OBS上传流程

光有回调函数还不够,我们需要把它挂载到OBS的上传流程中:

bool UploadFileToOBS(const std::string& filePath, DeviceDisplay* display) {
    // 初始化OBS客户端
    auto client = InitializeOBSClient();
    
    // 设置上传参数
    PutObjectRequest request;
    request.SetBucketName("my-bucket");
    request.SetKey("device_videos/" + GetFileName(filePath));
    request.SetFilePath(filePath);
    
    // 关键点:设置进度回调
    request.setTransferProgressHandler(UploadProgressCallback, display);
    
    // 执行上传
    auto outcome = client.PutObject(request);
    
    return outcome.IsSuccess();
}

三、进阶实现技巧

3.1 平滑进度显示

直接使用原始回调会导致更新过于频繁,我们可以添加平滑处理:

class SmoothProgress {
public:
    void Update(int64_t uploaded, int64_t total) {
        auto now = std::chrono::steady_clock::now();
        if (now - lastUpdate_ < std::chrono::milliseconds(200)) {
            return; // 200ms内不重复更新
        }
        lastUpdate_ = now;
        
        // 计算EMA(指数移动平均)平滑处理
        double current = (total > 0) ? (uploaded * 100.0 / total) : 0;
        smoothValue_ = 0.3 * current + 0.7 * smoothValue_;
        
        UpdateDisplay(smoothValue_);
    }
    
private:
    std::chrono::steady_clock::time_point lastUpdate_;
    double smoothValue_ = 0;
};

3.2 断点续传支持

对于大文件上传,断点续传是必须的:

void ResumeUploadProgress(int64_t uploaded, int64_t total, void* userData) {
    auto context = static_cast<ResumeContext*>(userData);
    if (!context) return;
    
    // 保存已上传字节数到本地
    SaveResumePosition(context->resumeFile, uploaded);
    
    // 更新显示
    UpdateProgressDisplay(uploaded, total);
}

四、实际应用中的注意事项

  1. 线程安全:OBS SDK的回调可能发生在工作线程,UI更新需要线程间通信
  2. 资源释放:确保userData的生命周期覆盖整个上传过程
  3. 异常处理:网络波动时要能恢复进度显示
  4. 性能考量:嵌入式设备资源有限,避免在回调中做复杂计算

这里给出一个线程安全的实现示例:

class ThreadSafeProgress {
public:
    void Update(int64_t uploaded, int64_t total) {
        std::lock_guard<std::mutex> lock(mutex_);
        currentUploaded_ = uploaded;
        currentTotal_ = total;
        
        // 通过消息队列通知UI线程
        Message msg{MSG_UPDATE_PROGRESS, CalculatePercentage()};
        messageQueue_.Push(msg);
    }
    
private:
    std::mutex mutex_;
    int64_t currentUploaded_ = 0;
    int64_t currentTotal_ = 0;
    MessageQueue messageQueue_;
};

五、性能优化实践

对于资源受限的嵌入式设备,我们可以做这些优化:

  1. 降低回调频率:设置最小更新间隔
  2. 整数运算优先:避免浮点运算消耗
  3. 内存池管理:复用回调相关的内存对象

优化后的回调示例:

void OptimizedProgressCallback(int64_t uploaded, int64_t total, void* userData) {
    static int64_t lastReported = 0;
    const int64_t reportInterval = 1024 * 1024; // 每1MB报告一次
    
    if (uploaded - lastReported < reportInterval && uploaded != total) {
        return;
    }
    lastReported = uploaded;
    
    // 使用整数运算计算百分比(0-10000表示0.00%-100.00%)
    uint32_t permille = (total > 0) ? (uploaded * 10000 / total) : 0;
    UpdateProgressPermille(permille);
}

六、完整示例代码

下面是一个完整的嵌入式设备上传示例:

#include <iostream>
#include <memory>
#include <mutex>
#include <obs-sdk-cpp.h>

class DeviceUploader {
public:
    explicit DeviceUploader(std::shared_ptr<DeviceDisplay> display)
        : display_(std::move(display)) {}
    
    bool UploadVideo(const std::string& localPath) {
        // 初始化OBS客户端
        ObsClient client = CreateOBSClient();
        
        // 设置上传请求
        PutObjectRequest request;
        request.SetBucket("device-videos");
        request.SetKey(GenerateRemoteName(localPath));
        request.SetFile(localPath);
        
        // 设置进度回调(传递this指针作为userData)
        request.SetProgressHandler(&DeviceUploader::ProgressCallback, this);
        
        // 执行上传
        auto outcome = client.PutObject(request);
        
        if (!outcome.IsSuccess()) {
            display_->ShowError("上传失败: " + outcome.GetError());
            return false;
        }
        
        return true;
    }
    
private:
    static void ProgressCallback(int64_t uploaded, int64_t total, void* userData) {
        auto self = static_cast<DeviceUploader*>(userData);
        self->HandleProgress(uploaded, total);
    }
    
    void HandleProgress(int64_t uploaded, int64_t total) {
        std::lock_guard<std::mutex> lock(mutex_);
        
        // 计算百分比(0-100)
        int percent = 0;
        if (total > 0) {
            percent = static_cast<int>(uploaded * 100 / total);
            percent = std::min(100, std::max(0, percent)); // 钳制到0-100
        }
        
        // 更新显示
        display_->UpdateProgress(percent);
        
        // 每10%或完成时记录日志
        if (percent % 10 == 0 || percent == 100) {
            Log("上传进度: " + std::to_string(percent) + "%");
        }
    }
    
    std::shared_ptr<DeviceDisplay> display_;
    std::mutex mutex_;
};

七、应用场景与技术选型

这种技术特别适合以下场景:

  • 智能家居设备的固件升级
  • 安防设备的录像备份
  • 工业设备的日志上传
  • 医疗设备的报告传输

相比其他方案,C++实现的优势在于:

  1. 性能高效:直接内存操作,无运行时开销
  2. 资源可控:精确管理内存和CPU使用
  3. 跨平台:可移植到各种嵌入式系统

但也要注意:

  • C++开发复杂度较高
  • 需要手动管理内存
  • 异常处理较为繁琐

八、总结与建议

实现一个健壮的上传进度回调系统,关键在于:

  1. 处理好线程安全问题
  2. 优化回调频率避免性能问题
  3. 提供平滑的进度显示体验
  4. 考虑嵌入式设备的资源限制

对于不同的应用场景,可以灵活调整:

  • 消费级设备:侧重流畅的UI体验
  • 工业设备:强调可靠性和实时性
  • 医疗设备:需要最高级别的数据完整性

最后记住,好的进度反馈能让用户感受到产品的专业性和可靠性,这在嵌入式设备领域尤为重要。