《网络摄像机实战③:设备端集成声网Agora打造实时音视频对讲功能》

85 阅读8分钟

声网RTC介绍

声网实时互动(Real-Time Communication,RTC)是提供音视频实时互动服务的产品,可帮助你将实时音视频功能集成到
App 中,实现互动直播、纯语音通话、高清视频通话等功能。在大规模的实时互动场景下,你可以用它实现更好的实时互动效果。 优势介绍
沉浸式音频体验 3A 处理:配备回声消除 (AEC)、降噪 (ANS) 和增益控制 (AGC)
技术,可强力消除百种突发噪声,实现复杂场景非线性回声抑制和近端人声保真,带来纯净沟通体验。
虚拟声卡:声网自研虚拟声卡算法,通过专业算法对用户声音进行优化,让用户的声音美妙动听、更有魅力。
空间音效:用户可以通过音频实时感知到其他用户距离、方位、朝向的变化,增强声音传播的空间感,打造沉浸式音频体验。 至臻画质体验
全平台极致高清:移动端支持 1080P 60 fps,PC端支持 4K 60 fps,配合声网自研 AI 画质算法,实现画质跨级提升。 高效视频编解码:SDK 自适应贴合场景的编解码方式,提供优秀的画质体验和更低的带宽消耗。 自适应高清策略:声网推出自适应高清视频策略,根据设备性能和网络质量自动调整视频属性,实现高清且流畅的视频效果。

声网 RTC 使用流程

在这里插入图片描述

控制台:声网提供给开发者管理声网各项服务的工具。控制台提供直观的界面,方便开发者在使用声网各项服务时进行充值、查询、管理等操作。

App ID:声网为开发项目生成的字符串,是项目的唯一标识。

App 证书:又名 App Certificate,它是声网控制台为开发项目生成的随机字符串,用于开启 Token 鉴权,并作为生成 Token 的参数之一。

Token:也称为动态密钥,是在加入频道时用于校验用户权限的一组字符串。

在控制台获取 App ID、App 证书和 Token 后,就可以开始尝试实现音视频通信的基本流程了。


《网络摄像机实战》系列文章,请读者前往阅读 👀

《网络摄像机实战①:Flutter 集成声网 Agora 打造实时音视频对讲》

《网络摄像机实战②:Flutter集成声网Agora图片抓拍与录像功能》

《网络摄像机实战③:设备端集成声网Agora打造实时音视频对讲功能》

《网络摄像机实战④:设备端集成声网Agora 打造远程控制设备或者异常报警推送功能》

《网络摄像机实战⑤:手机端 flutter集成声网Agora RTM打造远程控制设备功能》

《网络摄像机实战⑥:手机端 flutter集成亚马逊 AWS Kinesis Video Streams 音视频界面和控制》

《网络摄像机实战⑦:嵌入式端AWS KVS IoT SDK 库编译与测试全流程-君正/瑞芯微/全志(超详细指南)》

《网络摄像机实战⑧:嵌入式端接入 AWS KVS Producer 的设计要点与注意事项》


定义声网处理类AgoraRtcService


class AgoraRtcService {

public:
    AgoraRtcService() = default;
    ~AgoraRtcService();

    void app_init_event_handler(agora_rtc_event_handler_t *event_handler);

    int sendAgoraRtcAudioStream(short *audio_buffer, size_t audio_size);

    int sendAgoraRtcVideoStream(int nalu_type, uint8_t *video_buffer, uint32_t video_size);

    int stopAgoraRtcService();

    int startAgoraRtcService();
private:
    char m_rtc_token[512]                       = DEFAULT_TOKEN;
    char m_rtc_app_id[64]                       = DEFAULT_APP_ID;
    char m_rtc_channel_name[64]                 = DEFAULT_CHANNEL_NAME;
    area_code_e m_rtc_arga_code                 = AREA_CODE_GLOB;
    char m_rtc_log_path[64]                     = DEFAULT_RTC_LOG_PATH;
    std::atomic<bool> m_rtc_log_disable         = false;
    std::atomic<bool> m_rtc_use_string_uid      = RTC_USE_STRING_UID_ENABLE == 1?true:false;
    rtc_log_level_e m_rtc_log_level             = RTC_LOG_DEFAULT;
    connection_id_t m_conn_id                   = 0;

    //     thread handle
    std::thread m_rtc_thread ;
    std::atomic<bool> m_rtc_running             = true;

    std::thread m_rtc_audio_thread ;
    std::atomic<bool> m_rtc_audio_running             = true;

    snd_pcm_t                                   *p_capture_handle;///< Flag to control thread execution

};

开始启动声网 rtc 服务

注意: 声网服务器数据,通过回调函数的方式处理 可以基于自己的业务,配置service_opt参数(区域,日志是否开启,日志的级别,是否使用 token 等)

int AgoraRtcService::startAgoraRtcService() {
    agora_rtc_event_handler_t event_handler = {0};
    app_init_event_handler(&event_handler);
    rtc_service_option_t service_opt = {0};
    service_opt.area_code = m_rtc_arga_code;
    service_opt.log_cfg.log_path = m_rtc_log_path;
    service_opt.log_cfg.log_disable = m_rtc_log_disable;
    service_opt.log_cfg.log_level = m_rtc_log_level;
    service_opt.use_string_uid = m_rtc_use_string_uid;
    LOG(INFO) << "AgoraRtcService::startAgoraRtcService";

    int rval = agora_rtc_init(m_rtc_app_id, &event_handler, &service_opt);
    if (rval < 0) {
        LOG(INFO) << "agora_rtc_init failed %d" << rval;
        return -1;
    }
    rval = agora_rtc_create_connection(&m_conn_id);
    if (rval < 0) {
        LOG(INFO) << "agora_rtc_create_connection failed " << rval;
        return -1;
    } else
        LOG(INFO) << "m_conn_id  " << m_conn_id;

    rval = agora_rtc_set_bwe_param(m_conn_id, DEFAULT_BANDWIDTH_ESTIMATE_MIN_BITRATE,
                                   DEFAULT_BANDWIDTH_ESTIMATE_MAX_BITRATE,
                                   DEFAULT_BANDWIDTH_ESTIMATE_START_BITRATE);
    if (rval < 0) {
        LOG(INFO) << "agora_rtc_set_bwe_param failed " << rval;
        return -1;
    }

    rtc_channel_options_t channel_options = {0};
    memset(&channel_options, 0, sizeof(channel_options));
    channel_options.auto_subscribe_audio = true;
    channel_options.auto_subscribe_video = true;
    channel_options.enable_audio_mixer = false;
    channel_options.enable_lan_accelerate = false;

    channel_options.audio_codec_opt.pcm_duration = DEFAULT_SEND_AUDIO_FRAME_PERIOD_MS;
    channel_options.audio_codec_opt.audio_codec_type = AUDIO_CODEC_TYPE_OPUS;
    channel_options.audio_codec_opt.pcm_sample_rate = DEFAULT_PCM_SAMPLE_RATE;
    channel_options.audio_codec_opt.pcm_channel_num = DEFAULT_PCM_CHANNEL_NUM;
    if (m_rtc_use_string_uid) {
        rval = agora_rtc_join_channel_with_user_account(m_conn_id, Device::getInstance().getDeviceGeneralInformation().getDeviceUuid(), m_rtc_channel_name,
                                                        m_rtc_token,
                                                        &channel_options);
    } else
        rval = agora_rtc_join_channel(m_conn_id, Device::getInstance().getDeviceGeneralInformation().getDeviceUuid(), 0, NULL, &channel_options);
        // rval = agora_rtc_join_channel(m_conn_id, m_rtc_channel_name, DEFAULT_USER_ID, NULL, &channel_options);
    return 0;
}

发送设备视频数据到通道接口agora_rtc_send_video_data

注意: 接口需要知道 H264 的类型: I/P 帧 视频的码流类型:高/低 一帧数据及其长度

int AgoraRtcService::sendAgoraRtcVideoStream(int nalu_type,uint8_t *video_buffer , uint32_t  video_size) {
    if (g_rtc_joined_user_count == 0 ) {
        return 1;
    }
    video_frame_info_t info{};
    info.frame_type = nalu_type==0? VIDEO_FRAME_KEY:VIDEO_FRAME_DELTA;
    info.frame_rate = STREAM_VIDEO_FPS;
    info.data_type = VIDEO_DATA_TYPE_H264; //h264
    info.stream_type = VIDEO_STREAM_HIGH;
    int rval = agora_rtc_send_video_data(m_conn_id, video_buffer, video_size, &info);
    if (rval < 0) {
        LOG(INFO) << "Failed to send video data, reason:  " << agora_rtc_err_2_str(rval);
    }
    return 0;
}

手机端视频解码处理,通过回调函数on_video_data处理

event_handler->on_video_data = __on_video_data;
static void __on_video_data(connection_id_t conn_id, const uint32_t uid, uint16_t sent_ts,
                        const void *data, size_t len, const video_frame_info_t *info_ptr) {
//        LOGD("[conn-%u] on_video_data: uid %u sent_ts %u data_type %d frame_type %d stream_type %d len %zu",
//             conn_id, uid, sent_ts, info_ptr->data_type, info_ptr->frame_type, info_ptr->stream_type, len);
//        write_file(g_app.video_file_writer, info_ptr->data_type, data, len);
}

判断房间是否有用户决定是否发送视频,通过回调函数on_user_joined和on_user_offline处理

注意: on_user_joined 表示有用户进入房间 on_user_offline表示有用户离开房间

static void __on_user_joined(connection_id_t conn_id, uint32_t uid, int elapsed_ms) {
    g_rtc_joined_user_count++;
    LOG(INFO) << "on_user_joined user->uid:" << uid << "elapsed_ms:" << elapsed_ms  << "g_rtc_joined_user_count "  << g_rtc_joined_user_count ;
}
static void __on_user_offline(connection_id_t conn_id, uint32_t uid, int reason) {
    g_rtc_joined_user_count--;
    LOG(INFO) << "on_user_offline user->uid:" << uid << "reason:" << reason  << "g_rtc_joined_user_count: "<< g_rtc_joined_user_count;
}

错误码回调函数on_error,用于处理rtc 服务器返回的错误,调试非常有用


static void __on_error(connection_id_t conn_id, int code, const char *msg) {
    LOG(INFO) << "code" << code << "msg" << msg;
    if (code == ERR_VIDEO_SEND_OVER_BANDWIDTH_LIMIT) {
        // LOGE("Not enough uplink bandwdith. Error msg \"%s\"", msg);
        return;
    }

    if (code == ERR_INVALID_APP_ID) {
        // LOGE("Invalid App ID. Please double check. Error msg \"%s\"", msg);
    } else if (code == ERR_INVALID_CHANNEL_NAME) {
        // LOGE("Invalid channel name for conn_id %u. Please double check. Error msg \"%s\"", conn_id,
        // msg);
    } else if (code == ERR_INVALID_TOKEN || code == ERR_TOKEN_EXPIRED) {
        // LOGE("Invalid token. Please double check. Error msg \"%s\"", msg);
    } else if (code == ERR_DYNAMIC_TOKEN_BUT_USE_STATIC_KEY) {
        // LOGE("Dynamic token is enabled but is not provided. Error msg \"%s\"", msg);
    } else {
        // LOGW("Error %d is captured. Error msg \"%s\"", code, msg);
    }
}

发送音频到 RTC

注意: 音频的参数(格式,采样率 ,单双通道)在进入通道的时候填充数据结构 channel_options.audio_codec_opt


int AgoraRtcService::sendAgoraRtcAudioStream(short *audio_buffer , size_t  audio_size) {
    if (g_rtc_joined_user_count == 0 ) {
        return 1;
    }
    audio_frame_info_t audio_info = {AUDIO_DATA_TYPE_PCM};
    audio_info.data_type= AUDIO_DATA_TYPE_PCM;
    // LOG(INFO) << "audio data_len " << audio_size ;
    int rval = agora_rtc_send_audio_data(m_conn_id, audio_buffer, audio_size, &audio_info);
    if (rval < 0) {
        fprintf(stderr, "Failed to send audio data: %s\n", agora_rtc_err_2_str(rval));
    }
    return 0;
}

处理手机端的音频,通过回调函数on_audio_data处理

注意: 我的设备使用的 alsa 的音频框架,这部分不同的芯片需要做不同的处理

void __on_audio_data(connection_id_t conn_id, const uint32_t uid, uint16_t sent_ts,
                     const void *data, size_t len, const audio_frame_info_t *info_ptr) {
    RdkAudioService().getInstance().play_audio_data(data, len);
}

void RdkAudioService::play_audio_data(const void *data, size_t len) {
    int bytes_per_frame = DEFAULT_PCM_CHANNEL_NUM * sizeof(int16_t);
    int frames = len / bytes_per_frame;
    // LOG(INFO) << "frames:" << frames;
    if (frames > 0) {
        if (init_playback_audio() != 0) {
            LOG(INFO) << "init playback failed";
            return;
        }
        int ret = snd_pcm_writei(p_playback_handle, data, frames);
        if (ret < 0) {
            if (ret == -EPIPE) {
                // 缓冲区下溢,恢复
                LOG(INFO) << "Playback buffer underrun, preparing...";
                snd_pcm_prepare(p_playback_handle);
                // 尝试重新写入同一帧数据
                ret = snd_pcm_writei(p_playback_handle, data, frames);
                if (ret < 0) {
                    LOG(INFO) << "snd_pcm_writei failed again after prepare: " << snd_strerror(ret);
                }
            } else {
                LOG(INFO) << " snd_pcm_writei failed: " << snd_strerror(ret);
            }
        }
    }
}

APP端参考网络摄像机实战①:Flutter 集成声网 Agora 打造实时音视频对讲

在这里插入图片描述


友情提示🔔

🙏 感谢你的阅读! 如果这篇文章对你有所启发,欢迎关注我 ⭐,欢迎点击 “打赏支持作者” 支持一下我,你的支持是我持续创作的最大动力! 我会持续分享更多关于 智能摄像头 📷、机器人项目、 🤖音视频 RTC 🎧、App 开发 📱、嵌入式开发 🔧 等方向的实战经验,让你更快落地、更少踩坑。 欢迎浏览我其他文章 📚,或许能解决你当前的难题。 如果你正好在做相关项目产品,也欢迎随时私信我,一起技术交流、一起搞事情! 🤝💬📞 联系微信/电话:13826173658