IOT SDK开发参考

更新时间:2025-04-24 03:23:57

介绍了DingRTCIOT版本设计和使用方法。

1 DingRTC SDKIOT版本系统框架图

image

图中灰色部分为视频能力,目前的版本尚未支持。

2 包大小

linux64为例子,大小 < 1 MB。

3 依赖库

依赖这些库,操作系统需要提供:

  • libwebsockets

  • openssl

  • libcurl

注:openssl在有些嵌入式系统中替代为embedtls,有移植工作

注:libwebsockets在不同的系统中提供的接口差异比较大,有移植工作

注:libcurl依赖不是特别强,有其他https访问的方式,也可以替换

注:若网络带宽没有问题,也可以用g711作为音频的压缩格式,替换opus,可以进一步节省包大小

注:有些系统提供了json-c库,若复用,可以节省70 KB文件大小

注:如果需要支持data channel,会增加包体积

注:编译工具链需要支持基础的c++11能力,比如std::thread, std::mutex, lambda表达式, std::string, std::list, std::map, std::queue, std::vector。

4 集成方法

IOT SDK设计思路是微内核 + 流水线。RTC推拉流作为流水线中的一个模块,不是必需的模块,仅在音视频通话时需要RTC推拉流模块。

对于音频RTC通讯,集成时分两步:先搭建流水线,再开始RTC推拉流。

麦克风和扬声器留给应用来实现。下图中的例子是用SDL2封装成的麦克风和扬声器模块。

image.png

图中可见,RtcSenderRtcReceiver作为推拉流模块,嵌在流水线中。这2个模块只有需要RTC通讯的时候才需要,非RTC场景可以从流水线中移除。

应用开发步骤:

1,业务层负责编写2个模块:麦克风和扬声器模块。参考示例:

class SdlSpeaker : public NetBit::ExternalSpeaker
{
public:
    SdlSpeaker(NetBit::Callback *cb);
    virtual ~SdlSpeaker();

    // 通过SetParameters来改变扬声器模块的工作参数。
    // 具体支持哪些参数,见对应的实现函数。
    void SetParameters(const char **keys,
        const void **vals, int count) override;

    // 对于RTC应用,和普通的播放器不同的地方是,
    // 音频的播放依靠声卡提供callback向上游pull音频数据。
    // direct source用来提供数据源。
    // 一般的,direct source是neteq模块。
    void SetDirectSource(AVFrame *(*getter)(void *userdata), void *userdata) override {
      get_one_frame_ = getter;
      userdata_ = userdata;
    }

// protected:
    int32_t PreLoop() override;
    void    PostLoop() override;
protected:
    // 声卡播放回调函数
    static void cb_fill_audio(void *usrdata, uint8_t *stream, int32_t len);

private:
    AVFrame *current_;
    int32_t read_pos_in_bytes_;
    // 被cb_fill_audio()调用,读取音频数据
    int32_t fetch_bytes(void *dst, int bytes);
    int32_t freq_;
    int32_t channels_;

    // 对于播放器场景,用来指示当前是否进行缓冲。
    // 对于rtc场景,没有意义。
    bool buffering_;

    // if get_one_frame_ is set, then use this function
    // as source, not input_frame_queue_
    AVFrame * (*get_one_frame_)(void *userdata);
    void *userdata_;
};
#define PLAYBACK_BUFFERING_MS  40


void initonce_sdl()
{
  SDL_Init(SDL_INIT_EVERYTHING);
}

SdlSpeaker::SdlSpeaker(NetBit::Callback *cb) : NetBit::ExternalSpeaker(cb)
{
    current_ = NULL;
    read_pos_in_bytes_ = 0;
    freq_ = 16000;
    channels_ = 1;
    buffering_ = true;
    get_one_frame_ = NULL;
    userdata_ = NULL;
}

SdlSpeaker::~SdlSpeaker()
{
}

void SdlSpeaker::SetParameters(const char **keys,
    const void **vals, int count)
{
  // TODO: lock protection
  for (int i = 0; i < count; i++)
  {
    if (strcmp(keys[i], "sampleRate") == 0) {
      freq_ = (uint64_t) vals[i];
    }
    else if (strcmp(keys[i], "channels") == 0) {
      channels_ = (uint64_t) vals[i];
    }
  }
}

int32_t SdlSpeaker::fetch_bytes(void *dst, int bytes)
{
  int32_t read_bytes = 0;

  if (get_one_frame_ != NULL) {
    if (current_ == NULL) {
      current_ = get_one_frame_(userdata_);
      read_pos_in_bytes_ = 0;
    }
    if (current_ == NULL) {
        return 0;
    }

    int32_t frame_bytes = current_->content_->samples_per_channel * current_->content_->channels * sizeof(short);
    int32_t remain = frame_bytes - read_pos_in_bytes_;
    read_bytes = (remain < bytes) ? remain : bytes;
    memcpy(dst,
            (uint8_t *) current_->content_->buffers[0] + read_pos_in_bytes_,
            read_bytes);
    read_pos_in_bytes_ += read_bytes;

    if(read_pos_in_bytes_ == frame_bytes) {
        delete current_;
        current_ = NULL;
    }

    return read_bytes;
  }

  // buffering management
  // quit buffering state?
  if (buffering_) {
      int latency = GetInputQueueSize() * 20; // FIXME! not always 20 ms!
      if (latency > PLAYBACK_BUFFERING_MS) {
        buffering_ = false;
        printf("leaving buffering\n");
      }
  }
  if (!buffering_) {
    if(current_ == NULL) {
        current_ = Dequeue();
        read_pos_in_bytes_ = 0;
    }

    if(current_ != NULL) {
        int32_t frame_bytes = current_->content_->samples_per_channel * current_->content_->channels * sizeof(short);
        int32_t remain = frame_bytes - read_pos_in_bytes_;
        read_bytes = (remain < bytes) ? remain : bytes;
        memcpy(dst,
               (uint8_t *) current_->content_->buffers[0] + read_pos_in_bytes_,
               read_bytes);
        read_pos_in_bytes_ += read_bytes;

        if(read_pos_in_bytes_ == frame_bytes) {
            delete current_;
            current_ = NULL;
        }
    }
    else {
        // underrun, buffering
        buffering_ = true;
        printf("entering buffeing\n");
    }
  }

  return read_bytes;
}

void SdlSpeaker::cb_fill_audio(void *udata, uint8_t *stream, int32_t len)
{
    SdlSpeaker *pThis = (SdlSpeaker *) udata;

    uint8_t *dst = stream;
    do {
        int32_t read = pThis->fetch_bytes(dst, len);
        if(read == 0)
            break;

        dst += read;
        len -= read;
    } while(1);

    // mute unfilled part
    SDL_memset(dst, 0, len);
}

int32_t SdlSpeaker::PreLoop()
{
    MYASSERT(current_ == NULL);
    MYASSERT(read_pos_in_bytes_ == 0);

    /* 提到全局开始 initonce_sdl */
    // Initialize SDL
    // if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
    //     printf("Error: SDL failed to init!\n");
    //     return -1;
    // }

    //SDL_AudioSpec
    SDL_AudioSpec wanted_spec;
    SDL_memset(&wanted_spec, 0, sizeof(wanted_spec));
    wanted_spec.freq = freq_;
    wanted_spec.format = AUDIO_S16;
    wanted_spec.channels = channels_;
    wanted_spec.samples = freq_ * 20 / 1000; //1024; // audio buffer size in samples (must be power of 2)
    wanted_spec.callback = cb_fill_audio;
    wanted_spec.userdata = this;

    if (SDL_OpenAudio(&wanted_spec, NULL) < 0) {
        printf("can't open audio.\n");
        return -1;
    }

    if(wanted_spec.format != AUDIO_S16) {
        printf("not supported format %d\n", wanted_spec.format);
        return -1;
    }

    freq_     = wanted_spec.freq;
    channels_ = wanted_spec.channels;

    // Play
    SDL_PauseAudio(0);

    return 0;
}

void SdlSpeaker::PostLoop()
{
    SDL_CloseAudio();

    if(current_ != NULL)
    {
      delete current_;
      current_ = NULL;
    }
    read_pos_in_bytes_ = 0;
}

2,创建流水线

image.png

demo中的部分代码:

class Rtc_Pipeline : public Pipeline
{
public:
  Rtc_Pipeline(ding::rtc::RtcEngine *engine);
  virtual ~Rtc_Pipeline();

#if defined(PLATFORM_MAC)
  SdlRecorder *external_mic_ = NULL;
#endif
  NetBit::ExternalSpeaker *extSpeaker_ = NULL;
  NetBit::MediaModuleEx *neteq_speaker_ = NULL;
  NetBit::MediaModuleEx *opus_encoder_ = NULL;
  NetBit::MediaModuleEx *sender_ = NULL;
  NetBit::MediaModuleEx *receiver_ = NULL;
};
Rtc_Pipeline::Rtc_Pipeline(ding::rtc::RtcEngine *engine)
  : Pipeline(engine)
{
  /* 创建所有模块并配置参数
    */
  {
    external_mic_  = new SdlRecorder(&_callback);
    external_mic_->SetModuleID("mic");
    const char *keys[] = {"sampleRate", "channels", "frameDuration"};
    const void *vals[] = {(void *)(uint64_t)16000, (void *)(uint64_t)1, (void *)(uint64_t)20};
    external_mic_->SetParameters(keys, vals, sizeof(keys) / sizeof(keys[0]));
  }

  {
    extSpeaker_    = new SdlSpeaker(&_callback);
    extSpeaker_->SetModuleID("speaker");
  }

  {
    neteq_speaker_ = ding::rtc::RtcEngine::CreateModule("NeteqPlayer", "neteqspeaker", &_callback);
    const char *keys[] = {"speaker-device", "sampleRate", "channels"};
    // use 48000, even if the actual samplerate is 16000
    const void *vals[] = {extSpeaker_, (void *)(uint64_t)48000, (void *)(uint64_t)1};
    neteq_speaker_->SetParameters(keys, vals, sizeof(keys) / sizeof(keys[0]));
  }

  {
    opus_encoder_  = ding::rtc::RtcEngine::CreateModule("OpusEncoder", "opusenc", &_callback);
  }

  {
    sender_        = engine_->GetRtcSender(true, &_callback);
  }

  {
    receiver_      = engine_->GetRtcReceiver(true, &_callback);
  }

  /* 搭建流水线
    */
  ding::rtc::RtcEngine::Connect(external_mic_, opus_encoder_);
  ding::rtc::RtcEngine::Connect(opus_encoder_, sender_);
  ding::rtc::RtcEngine::Connect(receiver_, neteq_speaker_);

  /* 启动所有模块
    */
  external_mic_->Start();
  opus_encoder_->Start();
  sender_->Start();
  receiver_->Start();
  neteq_speaker_->Start();
  extSpeaker_->Start();
}

3,建立客户端和服务器之间的推拉流

image.png

流水线中的RtcSender模块是推流(音频)模块,RtcReceiver是拉流模块。调用JoinChannel接口加入一个频道,自动建立起RtcSender,RtcReceiver模块和服务器之间的音频上下行链路。

这一步成功后,整个流水线全部打通,就实现了和频道内其他用户之间的全双工语音实时通讯。

5 接口描述

IOT SDK除了提供RTC接口,和DingRTC其他的Native SDK相比,增加了流水线接口。

RTC接口:负责建立RTC推拉流链路

  • JoinChannel: 加入某个频道。缺省的,自动建立推拉流链路。

  • LeaveChannel:离开频道,RTC推拉流链路断开。但是不影响流水线。

  • PublishLocalTrack : 和服务器建立推流的媒体链路。缺省的,自动在入会后执行,通常情况下app不需要调用这个接口。

  • SubscribeRemoteTrack : 和服务器建立拉流的媒体链路。缺省的,自动在入会后执行,通常情况下app不需要调用这个接口。

流水线接口:负责搭建音频流水线。应用可以扩展,定制流水线

  • CreateModule : 创建模块。rtc推流和拉流模块,以及app自定义模块,不通过这个接口创建。

  • DeleteModule :删除模块。rtc推流和拉流模块由engine维护,不通过这个接口创建。其他模块通过这个接口删除。app自定义模块自行删除。

  • Connect :连接2个模块

  • Disconnect : 取消连接2个模块  

错误检测:RTC推拉流容易受到干扰,导致和服务器之间链接异常。App需要监听这些回调和消息通知,若有异常发生,可尝试LeaveChannelJoinChannel来恢复。若一直失败,则采取某个策略比如通知人工来检查。

  • OnJoinChannelResult

  • OnPublishResult

  • OnSubscribeResult

  • OnOccurError

  • 本页导读 (0)
  • 1 DingRTC SDK的IOT版本系统框架图
  • 2 包大小
  • 3 依赖库
  • 4 集成方法
  • 5 接口描述