使用eXosip+ffmpeg、ffplay命令行实现sip客户端

文章目录

  • 前言
  • 一、关键实现
    • 1、主要流程
    • 2、解决端口冲突
      • (1)、出现原因
      • (2)、解决方法
    • 3、解析sdp
      • (1)、定义实体
      • (2)、解析视频
      • (3)、解析音频
    • 4、命令行推拉流
      • (1)、视频推流
      • (2)、音频推流
      • (3)、音视频播放
  • 二、sipua接口设计
  • 三、使用示例
  • 四、完整代码
  • 五、效果预览
  • 总结


前言

使用sip做视频通话时,会遇到需要使用ip摄像头作为视频源的情况,查了资料使用pjsip通常也需要改源码。pjsip包含的功能很完整,但有点过于庞大,很多功能并不需要。而且笔者有一个想法,只要有个能处理sip交互的库比如eXosip,音视频这块另外实现,比如先使用ffmpeg和ffplay命令行作为音视频测试,成功后再写代码实现。本文就是测试成功的方案,真正灵活的方式还是要写代码调ffmpeg,本文更多的是提供一种实现思路。


一、关键实现

主要的实现步骤是使用eXosip处理sip、自己解析sdp、流媒体使用ffmpeg、ffplay命令行。

1、主要流程

在这里插入图片描述

2、解决端口冲突

(1)、出现原因

按照上述流程会遇到端口冲突问题,推流和拉流需要使用同一个本地udp端口,由于ffmpeg和ffplay是两个进程同使用相同的端口就会冲突。具体细节如下:
在这里插入图片描述

(2)、解决方法

一般想到的解决办法是使用jrtplib只建立一个rtp会话兼顾发送和接收,且流媒体通过ffmpeg代码实现。本文没有使用此方法,为了坚持使用ffmpeg和ffplay命令行,最好的方法是使用udp代理监听端口对数据进行转发,就可以有效的解决端口冲突问题。

在这里插入图片描述

3、解析sdp

虽然eXosip提供了sdp的获取方法,但是对于具体的信息还是需要自己解析,其实也是比较简单。

(1)、定义实体

//流类型
enum StreamType {STREAMTYPE_VIDEO,STREAMTYPE_AUDIO
};
/// <summary>
/// 流信息
/// </summary>
class StreamInfo {
public://流类型StreamType type;//rtp推流地址,可以用此地址ffmpeg直接推流,也可以用下面参数自定义推流char rtpAdress[128] = { 0 };//流的远端地址char remoteIp[32] = { 0 };//流的远端端口int remotePort = 0;//本地接收/发送端口int localPort = 0;//编码格式char codec[16];//负载类型int payload = 0;union{//采样率,音频int sampleRate = 0;//时间基、视频int timebase;};//声道数int channels = 0;
};

(2)、解析视频

std::vector<StreamInfo> SipUA::_getVideoStreams(sdp_message_t* sdp_msg)
{std::vector<StreamInfo> streams;if (!sdp_msg)return streams;sdp_connection_t* connection = eXosip_get_video_connection(sdp_msg);if (!connection)return streams;std::string ip = connection->c_addr; sdp_media_t* sdp = eXosip_get_video_media(sdp_msg);if (!sdp)return streams;int	port = atoi(sdp->m_port); for (int i = 0; i < sdp->a_attributes.nb_elt; i++){sdp_attribute_t* attr = (sdp_attribute_t*)osip_list_get(&sdp->a_attributes, i);if (attr){std::string audio_filed = attr->a_att_field;if (audio_filed == "rtpmap"){StreamInfo stream;stream.type = StreamType::STREAMTYPE_VIDEO;snprintf(stream.remoteIp, 32, ip.c_str());stream.remotePort = port;std::string value = attr->a_att_value;std::string::size_type pt_idx = value.find_first_of(0x20);if (pt_idx == std::string::npos)continue;stream.payload = atoi(value.substr(0, pt_idx).c_str());std::string::size_type bitrate_idx = value.find_first_of('/');if (bitrate_idx == std::string::npos)continue;stream.timebase = atoi(value.substr(bitrate_idx + 1).c_str());snprintf(stream.codec, 32, value.substr(pt_idx + 1, bitrate_idx - pt_idx - 1).c_str());streams.push_back(stream);}}}return streams;
}

(3)、解析音频

std::vector<StreamInfo> SipUA::_getAudioStreams(sdp_message_t* sdp_msg)
{std::vector<StreamInfo> streams;if (!sdp_msg)return streams;sdp_connection_t* connection = eXosip_get_audio_connection(sdp_msg);if (!connection)return streams;std::string audio_ip = connection->c_addr; //audio_ipsdp_media_t* audio_sdp = eXosip_get_audio_media(sdp_msg);if (!audio_sdp)return streams;int	audio_port = atoi(audio_sdp->m_port); //audio_portfor (int i = 0; i < audio_sdp->a_attributes.nb_elt; i++){sdp_attribute_t* attr = (sdp_attribute_t*)osip_list_get(&audio_sdp->a_attributes, i);if (attr){std::string audio_filed = attr->a_att_field;if (audio_filed == "rtpmap"){StreamInfo stream;stream.type = StreamType::STREAMTYPE_AUDIO;snprintf(stream.remoteIp, 32, audio_ip.c_str());stream.remotePort = audio_port;std::string value = attr->a_att_value;auto strs = StringHelper::split(value, " ");if (strs.size() > 1){stream.payload = atoi(strs[0].c_str());auto format = StringHelper::split(strs[1], "/");if (format.size() > 1){snprintf(stream.codec, 16, format[0].c_str());stream.sampleRate = atoi(format[1].c_str());if (format.size() > 2)stream.channels = atoi(format[2].c_str());}}streams.push_back(stream);}}}return streams;
}

4、命令行推拉流

(1)、视频推流

转发rtsp的h264流为例,rtp推流同时显示预览框。

ffmpeg -i rtmp://127.0.0.1/live/a123 -an -vcodec copy -payload_type 96 -f rtp rtp://127.0.0.1:25026?localrtpport=15514 -window_size 192x108 -f sdl 

(2)、音频推流

以本地文件转码为g.711u为例,每个包大小160bytes。

ffmpeg -re -stream_loop -1 -i D:\test_music.wav -vn -acodec pcm_mulaw -ar 8000 -ac 1 -af "aresample=8000[0];[0]asetnsamples=n=160:p=0" -payload_type 0 -f rtp rtp://127.0.0.1:15026?localrtpport=25514

音频设备采集编码为g.711u为例,每个包大小160bytes。

ffmpeg -f dshow -i audio="音频设备名称" -vn -acodec pcm_mulaw -ar 8000 -ac 1 -af "aresample=8000[0];[0]asetnsamples=n=160:p=0" -payload_type 0 -f rtp rtp://127.0.0.1:15026?localrtpport=25514

注:如果音频与视频为同一个输入源也可以合并为同一条命令。

(3)、音视频播放

将sdp字符串保存本地文件
本地播放的sdp

v=0
o=1002 158 1 IN IP4 127.0.0.1
s=Talk
c=IN IP4 127.0.0.1
t=0 0
m=video 25008 RTP/AVP 96
a=rtpmap:96 H264/90000
a=rtcp:25008
m=audio 25310 RTP/AVP 0
a=rtpmap:0 PCMU/8000
a=rtcp:25310

保存到test.sdp

FILE* f=NULL;
fopen_s(&f, "test.sdp", "wb");
if (f)
{fwrite(call->sdp, 1, strlen(call->sdp), f);fclose(f);
}

命令行播放

ffplay.exe -x 640 -y 360 -protocol_whitelist \"file,udp,rtp\" -i test.sdp

二、sipua接口设计

#pragma once
#include<functional>
#include <string>
#include <vector>
#include "UdpProxy.h"
#include <eXosip2\eXosip.h>
#include"MessageQueue.h"/// 这是一个sipua,内部实现是eXosip2,只提供sip交互,sdp解析、udp代理功能。
/// udp代理分离端口功能:
/// sdp的每个m媒体的推拉流需要使用一个端口,sip服务器要检查来源。
/// 如果此时采样ffmpeg.exe推流、ffplay.exe拉流,两个进程都需要绑定本地同一个端口,就会产生端口冲突。
/// 那就只能个使用jrtplib之类的库,打开一个连接同时发送和接收数据。
/// 但是有一个巧妙的解决办法那就是使用udp代理转发数据,就可以将端口拓展为多个了。/// <summary>
/// sip状态
/// </summary>
enum SipUAState {//收到对方inviteSIPUAEVENT_INVITE,//收到对方回复SIPUAEVENT_ANSWER,//处理流媒体,推流拉流端口有做分离,便于推拉流分开实现。SIPUAEVENT_STREAM,//结束通话,对方挂断SIPUAEVENT_ENDED,
};/// <summary>
/// 流类型
/// </summary>
enum StreamType {STREAMTYPE_VIDEO,STREAMTYPE_AUDIO
};/// <summary>
/// 流信息
/// </summary>
class StreamInfo {
public://流类型StreamType type;//rtp推流地址,可以用此地址ffmpeg直接推流,也可以用下面参数自定义推流char rtpAdress[128] = { 0 };//流的远端地址char remoteIp[32] = { 0 };//流的远端端口int remotePort = 0;//本地接收/发送端口int localPort = 0;//编码格式char codec[16];//负载类型int payload = 0;union{//采样率,音频int sampleRate = 0;//时间基、视频int timebase;};//声道数int channels = 0;
};/// <summary>
/// 通话对象
/// </summary>
class SipCall {
public:int callId = 0;//对方idconst char* userId = nullptr;//播发的sdpconst char* sdp = nullptr;//需要推流的视频信息StreamInfo* video = nullptr;//需要推流的音频信息StreamInfo* audio = nullptr;
};
class SipUA
{
public:/// <summary>/// 状态改变回调,目前版本除媒体流外只有对方的消息会触发状态改变/// </summary>std::function<void(SipUAState state, SipCall* call)> onState = [](auto, auto) {};SipUA(const std::string& serverIp, int serverPort, const std::string& username, const std::string& password);~SipUA();/// <summary>/// 开启客户端,此方法是阻塞的,可以在线程中开启。/// </summary>/// <param name="exitFlag">退出标记,值为true则退出</param>void exec(int* exitFlag);/// <summary>/// 呼叫/// </summary>/// <param name="remoteUserID">对方id</param>/// <param name="hasVideo">有视频否</param>/// <param name="hasAudio">有音频否</param>/// <returns>是否呼叫成功</returns>bool call(const std::string& remoteUserID, bool hasVideo = true, bool hasAudio = true);/// <summary>/// 应答/// </summary>/// <param name="hasVideo">有视频否</param>/// <param name="hasAudio">有音频否</param>void answer(bool hasVideo, bool hasAudio);/// <summary>/// 挂断/// </summary>void hangup();
};

三、使用示例

/// <summary>
/// 本示例启动后会自动拨号,
/// 接收到通话请求会自动接听
/// </summary>
void main() {SipUA ua("192.168.1.10", 5060, "1002", "1234");int exitFlag = false;ua.onState = [&](SipUAState state, SipCall* call) {switch (state){case SIPUAEVENT_INVITE:ua.answer(true, true);break;case SIPUAEVENT_ANSWER:break;case SIPUAEVENT_STREAM://视频推流if (call->video){std::string srcUrl = "test.mp4";std::string format = "-re -stream_loop -1";auto codec = StringHelper::toLower(call->video->codec);std::string params = "";char cmd[512];	if (codec == "h264"){params = "-preset ultrafast -tune zerolatency -level 4.2";}//发送桌面流,同时使用sdl本地预览sprintf_s(cmd, "ffmpeg %s  -i %s  -an -vcodec %s -pix_fmt yuv420p %s  -s 640x360   -b:v 500k  -r 30   -g 10   -payload_type %d   -f rtp %s -window_size 192x108 -f sdl \"%s\"  ",format.c_str(), srcUrl.c_str(), codec.c_str(), params.c_str(), call->video->payload, call->video->rtpAdress, srcUrl.c_str());//运行命令行runCmd(cmd);}//音频推流,如何是同一个输入流也可以和视频合并为一条命令if (call->audio){	std::string srcUrl = "test_music.wav";std::string format = "-re -stream_loop -1";	auto codec = StringHelper::toLower(call->audio->codec);std::string params = "";char cmd[512];if (codec == "opus"){codec = "libopus";}if (codec == "pcmu"){codec = "pcm_mulaw";params = "-ac 1 -af \"aresample=8000[0];[0]asetnsamples=n=160:p=0\"";//af滤镜确保每个包160bytes}//转发本地文件sprintf_s(cmd, "ffmpeg  %s -i %s -vn -acodec %s  -ar %d  %s -payload_type %d -f rtp %s",format.c_str(), srcUrl.c_str(), codec.c_str(), call->audio->sampleRate, params.c_str() , call->audio->payload, call->audio->rtpAdress);printf(cmd);//运行命令行runCmd(cmd);}//播放对方音视频if (call->sdp){FILE* f=NULL;fopen_s(&f, "test.sdp", "wb");if (f){fwrite(call->sdp, 1, strlen(call->sdp), f);fclose(f);std::string cmd = "ffplay.exe -x 640 -y 360 -protocol_whitelist \"file,udp,rtp\" -i test.sdp";//运行命令行runCmd(cmd);}else{printf("fopen_s test.sdp error\n");}}break;case SIPUAEVENT_ENDED://关闭所有子进程closeJobObject();break;default:break;}};//开启测试拨号new std::thread([&]() {Sleep(2000);ua.call("1004", true);});ua.exec(&exitFlag);
}

四、完整代码

eXosip版本为5.1,ffmpeg.exe为4.3,vs2022项目。

https://download.csdn.net/download/u013113678/88180712


五、效果预览

使用freeswitch作为sip服务器
本文程序的运行效果:
推送本地mp4到sip
在这里插入图片描述
使用linphone作为对端运行效果:
在这里插入图片描述


总结

以上就是今天讲述的内容,本文使用的技术很简单,但是实现过程有点曲折。尤其是端口冲突问题,花了不少的时间确定原因,解决办法也是无意中想到的,否则可能很早就用代码去实现整个sip客户端了。本文的实现方式,很好的解耦了sip和流媒体以及rtp,sip可以单独实现、流媒体也可以自由选择、也不需要共用一个rtp会话,有时想要快速搭建一个测试项目就变得容易多了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/32017.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

threejs点击模型实现模型边缘高亮的选中效果--更改后提高帧率

先来个效果图 之前写的那个稍微有点问题&#xff0c;帧率只有30&#xff0c;参照官方代码修改后&#xff0c;帧率可以达到50了&#xff0c;在不全屏的状态下&#xff0c;帧率60 1.首先需要导入库 // 用于模型边缘高亮 import { EffectComposer } from "three/examples/js…

github 无语的问题,Host does not existfatal: Could not read from remote repository.

Unable to open connection: Host does not existfatal: Could not read from remote repository. image.png image.png image.png Please make sure you have the correct access rights and the repository exists. 如果github desktop和git pull 和git clone全部都出问题了&…

[保研/考研机试] KY102 计算表达式 上海交通大学复试上机题 C++实现

描述 对于一个不存在括号的表达式进行计算 输入描述&#xff1a; 存在多组数据&#xff0c;每组数据一行&#xff0c;表达式不存在空格 输出描述&#xff1a; 输出结果 示例1 输入&#xff1a; 6/233*4输出&#xff1a; 18思路&#xff1a; ①设立运算符和运算数两个…

视觉学习(七)---Flask 框架下接口调用及python requests 实现json字符串传输

在项目实施过程中需要与其他系统进行接口联调&#xff0c;将图像检测的结果传递给其他系统接口&#xff0c;进行逻辑调用。这中间的过程可以通过requests库进行实现。 1.安装requests库 pip install requests2.postman 接口测试 我们先通过postman 了解下接口调用&#xff0…

在vue3+vite项目中使用jsx语法

如果我掏出下图&#xff0c;阁下除了私信我加入学习群&#xff0c;还能如何应对&#xff1f; 正文开始 前言一、下载资源二、利用vite工具引入babel插件总结 前言 最近在为部署人员开发辅助部署的工具&#xff0c;技术栈是vue3viteelectron&#xff0c;在使用jsx语法时&#x…

08-2_Qt 5.9 C++开发指南_坐标系统和坐标变换

文章目录 1. 坐标变换函数2. 视口和窗口 1. 坐标变换函数 QPainter 在窗口上绘图的默认坐标系统如下图所示&#xff0c;这是绘图设备的物理坐标。 为了绘图的方便&#xff0c;QPainter 提供了一些坐标变换的功能&#xff0c;通过平移、旋转等坐标变换&#xff0c;得到一个逻辑…

linux Ubuntu 更新镜像源、安装sudo、nvtop、tmux

1.更换镜像源 vi ~/.pip/pip.conf在打开的文件中输入: pip.conf [global] index-url https://pypi.tuna.tsinghua.edu.cn/simple按下:wq保存并退出。 2.安装nvtop 如果输入指令apt install nvtop报错&#xff1a; E: Unable to locate package nvtop 需要更新一下apt&a…

gitlab 503 错误的解决方案

首先使用 sudo gitlab-ctl status 命令查看哪些服务没用启动 sudo gitlab-ctl status 再用 gitlab-rake gitlab:check 命令检查 gitlab。根据发生的错误一步一步纠正。 gitlab-rake gitlab:check 查看日志 tail /var/log/gitlab/gitaly/current删除gitaly.pid rm /var/opt…

SpringBoot 的自动装配特性

1. Spring Boot 的自动装配特性 Spring Boot 的自动装配&#xff08;Auto-Configuration&#xff09;是一种特性&#xff0c;它允许您在应用程序中使用默认配置来自动配置 Spring Framework 的各种功能和组件&#xff0c;从而减少了繁琐的配置工作。通过自动装配&#xff0c;您…

MongoDB【无敌详细,建议收藏】

"探索MongoDB的无边之境&#xff1a;沉浸式数据库之旅" 欢迎来到MongoDB的精彩世界&#xff01;在这个博客中&#xff0c;我们将带您进入一个充满创新和无限潜力的数据库领域。无论您是开发者、数据工程师还是技术爱好者&#xff0c;MongoDB都将为您带来一场令人心动…

【java实习评审】对推电影详情模块的基本电影模型设计到位,并能考虑到特色业务的设计

大家好&#xff0c;本篇文章分享一下【校招VIP】免费商业项目"推电影"第一期 电影详情模块 Java同学的开发文档周最佳作品。该同学来自暨南大学电子信息专业。 本项目的商业出发点&#xff1a; 豆瓣评分越来越水&#xff0c;不太符合年青人的需求&#xff0c;我们推…

【雕爷学编程】Arduino动手做(12)---霍尔磁场传感器模块2

37款传感器与模块的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&#x…

【Hystrix技术指南】(5)Command创建和执行实现

创建流程 构建HystrixCommand或者HystrixObservableCommand对象 *使用Hystrix的第一步是创建一个HystrixCommand或者HystrixObservableCommand对象来表示你需要发给依赖服务的请求。 若只期望依赖服务每次返回单一的回应&#xff0c;按如下方式构造一个HystrixCommand即可&a…

淘宝商品价格查询接口 批量获取商品详情页数据 支持高并发(含调用实例)

接口开发背景 淘宝商品价格查询可以帮助消费者了解和比较不同商品的价格&#xff0c;从而能够作出更明智的购买决策。通过价格查询&#xff0c;消费者可以找到最具性价比的商品&#xff0c;避免被高价或低价的商品误导。此外&#xff0c;价格查询还可以帮助消费者了解市场行情…

shell脚本条件测试语句,if,case

shell脚本条件测试语句&#xff0c;if&#xff0c;case 一.条件测试1.1test命令1.2文件测试1.2.1文件测试常见选项 1.3数值比较1.4字符串比较1.5逻辑测试 二.if语句2.1单分支结构2.3多分支 三.case语句 一.条件测试 1.1test命令 测试特定的表达式是否成立&#xff0c;当条件成…

BGP基础实验建邻+宣告实验

实验题目如下&#xff1a; 实验拓扑如下&#xff1a; 实验要求如下&#xff1a; 【1】除R5的5.5.5.0环回外&#xff0c;其他所有的环回均可互相访问 实验思路如下&#xff1a; &#xff08;1&#xff09;合理的IP配置 &#xff08;2&#xff09;合理的BGP配置 &#xff08;…

SQL | 计算字段

7-创建计算字段 7.1-计算字段 存储在数据库中的数据一般不是我们所需要的字段格式&#xff0c; 需要公司名称&#xff0c;同时也需要公司地址&#xff0c;但是这两个数据存储在不同的列中。 省&#xff0c;市&#xff0c;县和邮政编码存储在不同的列中&#xff0c;但是当我们…

工厂老化设备维护的重要性及如何维护老化设备?

工业领域的老化设备问题日益凸显&#xff0c;对于保持生产稳定和效率至关重要。本文将探讨工厂老化设备维护的重要性&#xff0c;并介绍如何通过PreMaint设备数字化平台实现对老化设备的高效维护&#xff0c;从而确保工厂持续高效运转。 一、工厂老化设备的重要性 随着时间的推…

基于Python爬虫+词云图+情感分析对某东上完美日记的用户评论分析

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

在SPSS中实现数据转置

在使用SPSS开展数据分析的过程中&#xff0c;有时候不可避免需要对数据进行转置处理。 例如Kendall协同系数检验和组内相关系数&#xff08;ICC&#xff09;检验这两种方法都可以检验定量数据的一致性程度&#xff0c;但是这两种方法对数据的要求不同。 组内相关系数&#xf…