介绍了DingRTC的IOT版本设计和使用方法。
1 DingRTC SDK的IOT版本系统框架图
图中灰色部分为视频能力,目前的版本尚未支持。
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封装成的麦克风和扬声器模块。
图中可见,RtcSender和RtcReceiver作为推拉流模块,嵌在流水线中。这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,创建流水线
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,建立客户端和服务器之间的推拉流
流水线中的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需要监听这些回调和消息通知,若有异常发生,可尝试LeaveChannel再JoinChannel来恢复。若一直失败,则采取某个策略比如通知人工来检查。
OnJoinChannelResult
OnPublishResult
OnSubscribeResult
OnOccurError
- 本页导读 (0)
- 1 DingRTC SDK的IOT版本系统框架图
- 2 包大小
- 3 依赖库
- 4 集成方法
- 5 接口描述