实习总结

goole_test框架的使用

安装傻白甜方式

https://blog.csdn.net/Fei20140908/article/details/104344462

介绍最好的文章

https://www.cnblogs.com/linkstar/p/5698128.html
https://www.jianshu.com/p/5b38717328c2
https://www.cnblogs.com/coderzh/archive/2009/04/06/1426758.html

经验之谈

https://blog.csdn.net/addfourliu/article/details/6793935

gmock简单使用

https://blog.csdn.net/Primeprime/article/details/99675304

音视频概念

https://blog.csdn.net/electrombile/article/details/51067344
1.推流(Pushing):
推流是指将摄像头或者屏幕采集的视频、音频等实时数据通过网络上传到流媒体服务器。通常使用 RTMP、RTSP 等协议进行推送。
2.拉流/播放(Pulling/Playback):
拉流或者播放是指观众端通过网络从流媒体服务器获取直播流并进行播放。观众可以通过浏览器、移动端应用或者专门的播放器软件进行观看。
3.回源(Origin Fetching):
回源是指流媒体服务器在接收到推流后,将流分发到多个服务器(如 CDN 节点)进行加速分发,以提高用户访问的速度和稳定性。
4.录制(Recording):
录制是在流媒体服务器端将直播内容实时录制为视频文件,以便于后续的存档、点播或者再次播放。
5.时移(Time Shifting):
时移是指在直播过程中,观众可以随时跳转到直播的任意时间点继续观看,而不必等待直播结束。
6.转码(Transcoding):
转码是指根据观众设备的性能和网络情况,动态调整直播流的码率、分辨率等参数,以确保观众能够流畅地观看直播内容。
7.CDN服务器加快速度的原理:
客户端的网络环境差异很大,为了优化播放端的体验,CDN需要把播放引导到离客户最近的,网速最快的服务器,相当于CDN给客户端提升了速度,这就是CDN加速。
CDN加速主要是通过减少访问服务器的距离,提供大带宽的服务,选择相同的运营商,选择延时低的流媒体协议等手段来实现内容的加速。
8. rtmp协议
https://blog.csdn.net/qq_21438461/article/details/130312124
rtmp(实时消息收发协议)是一种基于TCP的应用层协议,用于实现多媒体数据的实时传输。RTMP协议通过在客户端和服务器之间建立持久连接,提供稳定、低延迟的音视频传输服务。但在许多场景中,尤其是实时互动场景,比如直播、视频会议等等,RTMP仍然是首选协议。
9. https://blog.csdn.net/daniel_leung/category_12192843.html

测试相关的问题

  • 什么是GTEST,简单做个介绍。
    GTEST全称为Google Test, 是谷歌公司的一个测试框架,用于编写和运行自动化测试。
  • 你是怎么在项目中集成 GTEST?
  1. 将项目根目录下建立一个GTEST文件夹,将GTEST的源代码克隆到该目录之下。
  2. 在根目录下创建的整体的CMakeLists.txt下设置GTEST的子目录。
  3. 创建测试cpp文件,进行测试。
  • GTEST的用法
  1. 参数化测试,使用TEST_P,INSTANTIATE_TEST_SUITE_P来进行参数化测试
class MyParamTest : public ::testing::TestWithParam<int> {};

TEST_P(MyParamTest, CheckEven) {
  int n = GetParam();
  EXPECT_EQ(n % 2, 0);
}

INSTANTIATE_TEST_SUITE_P(EvenNumbers, MyParamTest, ::testing::Values(2, 4, 6, 8));

  1. 自定义监听事件,可以通过继承EmptyTestEventListener实现监听事件通过的个数。
class TestCounterListener : public ::testing::EmptyTestEventListener {
 public:
  // 构造函数,初始化计数器
  TestCounterListener() : tests_passed_(0), tests_failed_(0) {}

  // 重载 OnTestEnd 方法,当每个测试结束时调用
  void OnTestEnd(const ::testing::TestInfo& test_info) override {
    if (test_info.result()->Passed()) {
      tests_passed_++;
    } else {
      tests_failed_++;
    }
  }

  // 重载 OnTestProgramEnd 方法,当所有测试结束时调用
  void OnTestProgramEnd(const ::testing::UnitTest& unit_test) override {
    std::cout << "====================" << std::endl;
    std::cout << "Summary of test results:" << std::endl;
    std::cout << "Tests passed: " << tests_passed_ << std::endl;
    std::cout << "Tests failed: " << tests_failed_ << std::endl;
    std::cout << "====================" << std::endl;
  }

 private:
  int tests_passed_;  // 记录通过的测试数量
  int tests_failed_;  // 记录失败的测试数量
};

以上可以监听到案例通过的个数。可以监视每一个测试案例的结果,也可以最后查看最后最终的结果。
3. GMOCK
GTEST 与 Google Mock 一起使用,允许创建 mock 对象来替代真实对象,从而控制测试场景和行为。
举个简单例子

#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <string>

// BankDatabase 接口定义
class BankDatabase {
public:
    virtual ~BankDatabase() {}

    // 获取账户余额的虚函数
    virtual int GetBalance(const std::string& account_id) const = 0;

    // 设置账户余额的虚函数
    virtual void SetBalance(const std::string& account_id, int amount) = 0;
};

// BankAccount 类定义
class BankAccount {
public:
    // 构造函数,接受一个 BankDatabase 的指针
    BankAccount(BankDatabase* db) : database_(db) {}

    // 查询余额的成员函数
    int GetBalance(const std::string& account_id) const {
        return database_->GetBalance(account_id);
    }

    // 存款的成员函数
    void Deposit(const std::string& account_id, int amount) {
        int current_balance = database_->GetBalance(account_id);
        database_->SetBalance(account_id, current_balance + amount);
    }

private:
    BankDatabase* database_; // BankDatabase 的指针
};

// 使用 Google Mock 创建 BankDatabase 的模拟类
class MockBankDatabase : public BankDatabase {
public:
    MOCK_CONST_METHOD1(GetBalance, int(const std::string& account_id)); // 模拟 GetBalance 方法
    MOCK_METHOD2(SetBalance, void(const std::string& account_id, int amount)); // 模拟 SetBalance 方法
};

// 测试用例
TEST(BankAccountTest, DepositAddsToBalance) {
    MockBankDatabase mock_db; // 创建一个 BankDatabase 的 mock 对象

    // 设定 GetBalance 方法的返回值为 100,表示账户初始余额为 100
    EXPECT_CALL(mock_db, GetBalance("12345"))
        .WillOnce(::testing::Return(100));

    // 设定 SetBalance 方法的预期调用,当存款时,期望调用此方法设置新的余额为 150
    EXPECT_CALL(mock_db, SetBalance("12345", 150))
        .Times(1); // 期待调用一次

    BankAccount account(&mock_db); // 创建 BankAccount 对象,传入 mock 对象
    account.Deposit("12345", 50);  // 执行存款操作,存入 50

    // 这里不需要额外的断言,因为 Google Mock 的 EXPECT_CALL 会验证调用的行为
}

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

  1. 什么是套件
    套件的名称通常反映了被测试的功能或模块(例如 MathTests、StringTests 等)。
  2. 执行特定的标签
./your_test_executable --gtest_filter=StringTests.*

执行特定的StringTests套件的所有测试案例。
6. GTEST常用的宏
基本测试宏TEST、TEST_F,断言宏--非致命断言--致命断言,异常断言宏,参数化测试宏--TEST_P--INSTANTIATE_TEST_SUITE_P,生命周期宏等。
7. 测试夹具

#include <gtest/gtest.h>
#include "Calculator.h"

// 定义一个测试夹具类,继承自 ::testing::Test
class CalculatorTest : public ::testing::Test {
protected:
    // SetUp 在每个测试用例执行之前被调用
    void SetUp() override {
        calc = new Calculator();  // 创建 Calculator 对象
    }

    // TearDown 在每个测试用例执行之后被调用
    void TearDown() override {
        delete calc;  // 销毁 Calculator 对象
    }

    Calculator* calc;  // 指向 Calculator 对象的指针
};

// 使用 TEST_F 宏定义基于测试夹具的测试用例
TEST_F(CalculatorTest, Addition) {
    EXPECT_EQ(calc->Add(1, 1), 2);  // 测试加法功能
}

TEST_F(CalculatorTest, Subtraction) {
    EXPECT_EQ(calc->Subtract(5, 3), 2);  // 测试减法功能
}

TEST_F(CalculatorTest, Multiplication) {
    EXPECT_EQ(calc->Multiply(2, 3), 6);  // 测试乘法功能
}

TEST_F(CalculatorTest, Division) {
    EXPECT_DOUBLE_EQ(calc->Divide(10, 2), 5.0);  // 测试除法功能
}

TEST_F(CalculatorTest, DivisionByZero) {
    EXPECT_THROW(calc->Divide(10, 0), std::invalid_argument);  // 测试除以零的异常情况
}

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

为了减少重复、以及函数的逻辑关联,可以采用测试夹具,可以减少每一次生成一个新对象的麻烦,而且函数执行之间有逻辑而言。

  1. 生成XML报告
./my_tests --gtest_output=xml:report.xml

将生成的XML报告输出到report.xml中去。

  • 怎么书写优秀的测试案例(以项目为例)
  1. 一个好的测试案例应该是可以减少对其他模块的依赖;
  2. 一个好的测试套件的命名,应该简洁且描述性强;
  3. 按照“安排-执行-断言”的顺序写测试用例;
  4. 覆盖边界条件以及异常情况;
  5. 多使用参数化测试;
  • GTEST 的测试用例与测试套件之间的关系是什么?
    一个测试套件是一类的测试,一个测试套件内部可以有很多的测试用例;
TEST(MathOperationsTest(测试套件), HandlesAddition(测试用例)) {
    EXPECT_EQ(Add(1, 1), 2); // 这是一个测试用例,验证Add函数的加法操作
}
  • 如何在 GTEST 中设置和清理测试环境?
  1. 在Google Test(GTest)中,可以通过使用SetUp和TearDown方法来设置和清理测试环境。这两个方法通常用于在每个测试用例运行之前和之后执行一些初始化和清理操作,例如分配内存、初始化数据、打开文件或数据库连接等。
  2. 除了SetUp和TearDown,GTest还提供了SetUpTestSuite和TearDownTestSuite方法,它们用于在测试套件的所有测试用例运行之前和之后设置和清理共享的环境。这对需要在所有测试用例之间共享一些昂贵的资源(如数据库连接)非常有用。
  • GTEST 中如何处理依赖性测试?
  1. 在同一个套件的不同测试案例之间有依赖关系,那么可以使用SetUpTestSuite和TearDownTestSuite来设置共享变量,使得测试案例之间的关系可以共享
  2. 设置测试用例之间的调用关系(但是不推荐)
    需要在mian函数中手动调用该测试用例,然后保证他们的顺序,这样是不明智的选择;
  1. 使用locv工具收集.gcda.gcno文件生成info后缀文件;
  2. genhtml coverage_filtered.info -o coverage_report将info变成HTML文件;
    一个软件开发中,一般单元测试的覆盖率要达到80%以上,90属于比较优秀的覆盖率
  • 不同的输出格式(如 XML、JSON)生成测试报告
./my_tests --gtest_output=xml:report.xml
./runTests --gtest_output=json:report.json
  • 回归测试
    用于在项目的功能函数发生更改或更新后,验证这些更改没有引入新的错误或影响现有功能的正确性。

项目总结

项目背景

本项目旨在开发一个专门用于教学直播和录播摄像直播系统。该系统将集成高清摄像头、流媒体传输能力,以适应不同的教学场景。利用流媒体协议确保实时、稳定的音视频传输,并通过模块化设计实现系统的灵活扩展。

项目目标

该项目应该达到以下的几个目标:

  • 高质量的实时传输;
  • 低延迟;
  • 高并发量支持;
  • 可录制和回放;
  • 确保最终交付的系统能够有效支持教学活动,提高教育质量和效率。

项目涉及模块

  • 日志模块;
  • 直播业务模块;
  • 音视频数据的封装与解封装模块;
  • 回源模块;
  • 拉流模块;
  • 负载均衡系统;
  • 分布式模块;

回源功能的介绍

https://blog.csdn.net/Wu_qz/article/details/81265017
先讲两个概念,一是源服务器,二是cdn内容分发服务,在点播系统中,用户请求视频资源的时候,如果在边缘cdn缓存命中,那么可以直接从cdn服务拉取数据,如果缓存未命中,那么cdn服务需要向上层请求资源,之后发送给用户。
什么时候会触发回源?
https://developer.qiniu.com/fusion/kb/4080/the-common-problems-in-the-cdn

  • cdn节点上无缓存,会回源拉取资源;
  • cdn上的文件超时的时候会回源;
  • 若为不缓存文件,需要回源;
  • 对于同一个资源,即使cdn有缓存,使用不同的URL也会造成回源;

你在回源功能的贡献

当满足以上的四个条件的其中一个,那么会触发回源功能:

  • 设计回源的配置管理模块,给回源功能单独设计一个配置对象;
  • 设计回源功能类,定义基本的功能;
  • 基于具体的协议Rtmp实现该类型的回源对象的拉流逻辑;
  • 对设计的回源功能进行单元测试,确保测试覆盖率达到要求,并且对一个边界和异常条件进行测试。

项目过程中遇到的困难,怎么解决的,有什么收获启示

  • 碰到的BUG
  1. 配置信息是读取到一个Target对象中的,将回源任务添加到任务队列的时候传递的是一个共享指针的引用,导致shared_ptr释放,但是放进任务队列的是一个已经释放掉的对象,出现错误。
  2. 在回源功能类设计的时候发生了悬空指针的出现,我们需要在PullerRelay中传递一个本身作为函数的一个参数,导致了悬空指针的出现,一直出现未定义的错误,我检查不出来,还是后面请教了带我的人才明白怎么回事。使用enable_shared_from_this实现不独立的共享指针。
  • 整体把握不够
  1. 代码的一次性编程:因为当时对功能的设计还是不太熟,我想到的是将不同协议实现的回源功能判断一下给一个然后实现不同的版本,没想到这样的代码显得十分的臃肿,所以才把所有协议应该实现的功能先写一个基类,然后去继承之后根据不同的协议实现不同的回源逻辑。
  • 功能BUG
  1. 发现代码当流还没有准备好的时候就开始播放了,导致后面流准备好之后不会触发,我一直测试一直不能播放。
    ffmpeg re -i /home/smalls/media/test.mkv Vcodec libx264 -acodec copy -g 20 -f flw "rtmp://hx.com:1936/live/test"
    这条ffmpeg命令的作用是将/home/smalls/media/test.mkv文件中的视频流转换为H.264 编码格式,并将音频流直接拷贝到一个 RTMP 流媒体服务器上进行直播。

项目过程中的团队成员是怎么合作的

项目的具体实现

回源功能设计

  1. 每一个会话有很多的拉流者,当拉流的时候没有推流点的时候,即session的增加player函数void Session::AddPlayer(const PlayerUserPtr &user)中发现没有推流点的时候:
void Session::AddPlayer(const PlayerUserPtr &user)
{
    {
        std::lock_guard<std::mutex> lk(lock_);
        players_.insert(user);
    }
    LIVE_DEBUG << " add player,session name:" << session_name_ << ",user:" << user->UserId();
    // 如果没有推流者的话,触发回源逻辑
    if(!publisher_)
    {
        //如果没有回源实例的话创建回源实例
        if(!pull_)
        {
            // 回源实例是根据一个session进行创建
            pull_ = new PullerRelay(*this);
        }
        // 创建成功触发回源
        pull_->StartPullStream();
    }
    user->Active();
}
  1. 将静态配置的回源地址数组Targets读取出来
void PullerRelay::StartPullStream()
{
    auto ret = GetTargets();
    if(!ret)
    {
        PULLER_ERROR << " no target found.";
        sLiveService->CloseSession(session_.SessionName());
        return;
    }
    Pull();
}
bool PullerRelay::GetTargets()
{
    auto appinfo = session_.GetAppInfo();
    if(appinfo)
    {
        targets_ = appinfo->pulls;
        if (targets_.size()>0)
        {
            return true;
        }
    }
    return false;
}
  1. 根据读取的回源地址数组Targets中轮询回源
    根据轮询到的回源Target的协议,创建对应的回源具体协议的推流者,进行推流。
void PullerRelay::Pull()
{
    if(session_.IsPublishing())
    {
        return;
    }
    //轮询选择Target创建具体的pull实例
    SelectTarget();
    // 根据读取的配置的尝试次数和尝试时间间隔决定什么时候进行推流
    if(current_target_->retry == 0||current_target_->interval == 0)
    {
        if(puller_)
        {
            puller_->Pull(current_target_);
        }
    }
    else
    {
        Puller *p = puller_;
        TargetPtr t = current_target_;
        current_loop_->RunAfter(current_target_->interval,[p,t](){
            if(p)
            {
                p->Pull(t);
            }
        });
    }
}

回源配置类

class Target
{
public:
    Target(const std::string &s);
    Target(const std::string &domain_name,const std::string &app_name);
    ~Target() = default;
    //从配置文件中读取对应的回源地址和协议等设置
    bool ParseTarget(Json::Value &root);
    // 从地址中解析出url和port
    void ParseTargetUrl(const std::string &url);
    
    std::string remote_host;
    unsigned short remote_port; 
    std::string session_name;
    std::string domain_name;               
    std::string app_name;
    std::string stream_name;
    std::string protocol;
    std::string url;
    int32_t interval{1000};
    int32_t max_retry = 0;  
    int32_t retry = 0; 
    std::string param;          
};

从静态配置中得到一个目标的Target,将解析得到的Target配置加入到appinfo的配置信息中。

回源功能类

class PullerRelay:public PullHandler
{
public:
    // 一个回源实例只会绑定一个会话
    PullerRelay(Session &s);
    ~PullerRelay();
    // 开启回源功能
    void StartPullStream();
private:
    void OnPullSucess() override;
    void OnPullClose() override;
    bool GetTargets();
    Puller *GetPuller(TargetPtr p);
    void SelectTarget();
    void Pull();
    void ClearPuller();

    Session &session_;
    std::vector<TargetPtr> targets_;
    TargetPtr current_target_;
    int32_t cur_target_index_{-1};
    Puller * puller_{nullptr};
    EventLoop *current_loop_{nullptr};
};

Rtmp协议回源类

class RtmpPuller:public RtmpHandler,public Puller
{
public:
    RtmpPuller(EventLoop *loop, Session *s ,PullHandler *handler);
    ~RtmpPuller();
    bool Pull(const TargetPtr &target) override;
    void OnNewConnection(const TcpConnectionPtr &conn) override;
    void OnConnectionDestroy(const TcpConnectionPtr &conn)override;
    void OnRecv(const TcpConnectionPtr &conn ,PacketPtr &&data)override;
    bool OnPlay(const TcpConnectionPtr &conn,const std::string &session_name, const std::string &param) override;
    void OnRecv(const TcpConnectionPtr &conn ,const PacketPtr &data)override{}
    void OnActive(const ConnectionPtr &conn)override{}
private:
    TargetPtr target_;
    RtmpClient * rtmp_client_{nullptr};
};

采用Pull函数简历Rtmp协议的client,然后去指定的服务器请求该数据。

posted @ 2024-07-12 23:35  铜锣湾陈昊男  阅读(12)  评论(0)    收藏  举报