使用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_ip
	sdp_media_t* audio_sdp = eXosip_get_audio_media(sdp_msg);
	if (!audio_sdp)
		return streams;
	int	audio_port = atoi(audio_sdp->m_port); //audio_port
	for (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 {
	//收到对方invite
	SIPUAEVENT_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;
	//对方id
	const char* userId = nullptr;
	//播发的sdp
	const 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会话,有时想要快速搭建一个测试项目就变得容易多了。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python中搭建IP代理池的妙招

在Python的爬虫世界里&#xff0c;你是否也想搭建一个功能强大的IP代理池&#xff0c;让你的爬虫无忧无虑地畅游各大网站&#xff1f;今天&#xff0c;我就来教你使用Scrapy框架搭建IP代理池&#xff0c;让你的爬虫更加智能、高效&#xff01;跟着我一步一步来&#xff0c;轻松…

(力扣)用两个队列实现栈---C语言

分享一首歌曲吧&#xff0c;希望在枯燥的刷题生活中带给你希望和勇气&#xff0c;加油&#xff01; 题目&#xff1a; 请你仅使用两个队列实现一个后入先出&#xff08;LIFO&#xff09;的栈&#xff0c;并支持普通栈的全部四种操作&#xff08;push、top、pop 和 empty&#…

【单片机】51单片机,TLC2543,驱动程序,读取adc

TLC2543 是一款 12 位精密模数转换器 (ADC)。 1~9、11、12——AIN0&#xff5e;AIN10为模拟输入端&#xff1b; 15——CS 为片选端&#xff1b; 17——DIN 为串行数据输入端&#xff1b;&#xff08;控制字输入端&#xff0c;用于选择转换及输出数据格式&#xff09; 16——…

大数据课程G2——Hbase的基本架构

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 掌握Hbase的基本架构; ⚪ 掌握Hbase的读写流程; ⚪ 掌握Hbase的设计与优化; 一、基本架构 1. HRegion 1. 在HBase中,会将一个表从行键方向上进行切分,切分成1个或者多个HRegion。 …

C++入门(小白篇1—编译器安装-代码注释等)

前言&#xff1a; 最近想学一下一下C看了一些博客内容写的倒是很充实&#xff0c;但是&#xff0c;细节不到位&#xff0c;我是有Python基础的&#xff0c;所以学习来蛮快的&#xff0c;但是对于小白的话&#xff0c;有好多小细节大多数博客还是不够详细&#xff0c;由此我想写…

对话Sam Altman与Greg Brockman:初心和过去,信念和现在,责任和未来

导读 近日&#xff0c;硅谷著名投资人Reid Hoffman和Aria Finger联手对Sam Altman和Greg Brockman进行了一场访谈&#xff0c;访谈涉及到主题有&#xff1a;OpenAI的使命&#xff0c;人工智能对教育、医疗等行业的变革性影响&#xff0c;人工智能如何面对监管&#xff0c;OpenA…

十九、docker学习-Dockerfile

Dockerfile 官网地址 https://docs.docker.com/engine/reference/builder/Dockerfile其实就是我们用来构建Docker镜像的源码&#xff0c;当然这不是所谓的编程源码&#xff0c;而是一些命令的集合&#xff0c;只要理解它的逻辑和语法格式&#xff0c;就可以很容易的编写Docke…

Effective Java笔记(28)列表优于数组

数组与泛型相比&#xff0c;有两个重要的不同点 。 首先&#xff0c;数组是协变的&#xff08; covariant &#xff09; 。 这个词听起来有点吓人&#xff0c;其实只是表示如果 Sub 为 Super 的子类型&#xff0c;那么数组类型 Sub[ ]就是Super[ ]的子类型。 相反&#xff0c;泛…

【maven】构建项目前clean和不clean的区别

其实很简单&#xff0c;但是百度搜了一下&#xff0c;还是没人能简单说明白。 搬用之前做C项目时总结结论&#xff1a; 所以自己在IDE里一遍遍测试程序能否跑通的时候&#xff0c;不需要clean&#xff0c;因为反正还要改嘛。 但是这个项目测试好了&#xff0c;你要打成jar包给…

【Vue3】动态组件

动态组件的基本使用 动态组件&#xff08;Dynamic Components&#xff09;是一种在 Vue 中根据条件或用户输入来动态渲染不同组件的技术。 在 Vue 中使用动态组件&#xff0c;可以使用 元素&#xff0c;并通过 is 特性绑定一个组件的名称或组件对象。通过在父组件中改变 is 特…

AirServer2023最新Mac苹果电脑系统投屏软件

AirServer是一个Mac专用投屏工具&#xff0c;功能强大&#xff0c;并且可以通过网络和其他平台同步视频内容。可以使用多个设备进行投屏&#xff0c;快速查看同一局域网内的视频。支持的设备&#xff1a;苹果系统。支持 Windows、 Mac、 Android、 iOS、 windows平台。通过这款…

C++笔记之两个类的实例之间传递参数——通过构造函数传递类对象的方法详细探究

C笔记之两个类的实例之间传递参数——通过构造函数传递类对象的方法详细探究 code review! 文章目录 C笔记之两个类的实例之间传递参数——通过构造函数传递类对象的方法详细探究1.传递对象的const引用——ClassB的实例只能访问ClassA的实例&#xff0c;但不会修改ClassA的实…

js:Markdown编辑器Vue3版本md-editor-v3

文档 https://github.com/imzbf/md-editor-v3https://imzbf.github.io/md-editor-v3/zh-CN/index 安装 npm install md-editor-v3使用 <template><MdEditor v-model"text" /> </template><script setup> import { ref } from vue; impor…

EFLFK——ELK日志分析系统+kafka+filebeat架构(3)

zookeeperkafka分布式消息队列集群的部署 紧接上期&#xff0c;在ELFK的基础上&#xff0c;添加kafka做数据缓冲 附kafka消息队列 nginx服务器配置filebeat收集日志&#xff1a;192.168.116.40&#xff0c;修改配置将采集到的日志转发给kafka&#xff1b; kafka集群&#xff…

RabbitMQ在CentOS下的安装

RabbitMQ的版本是3.8.2 1.环境配置&#xff1a;CentOs 7.6以上版本&#xff0c;我的版本是7.9&#xff0c;不要对yum换源&#xff0c;否则可能会安装失败。 echo "export LC_ALLen_US.UTF-8" >> /etc/profile source /etc/profile 以上命令&#xff0c;是…

网络优化工程师,你到底了解多少?

5G网络优化工程师到底是什么&#xff1f; 5G&#xff0c;第五代移动通信技术&#xff08;5th Generation Mobile Communication Technology&#xff0c;简称5G&#xff09;是具有高速率、低时延和大连接特点的新一代宽带移动通信技术&#xff0c;5G通讯设施是实现人机物互联的…

【MySQL】sql字段约束

在MySQL中&#xff0c;我们需要存储的数据在特定的场景中需要不同的约束。当新插入的数据违背了该字段的约束字段&#xff0c;MySQL会直接禁止插入。 数据类型也是一种约束&#xff0c;但数据类型这个约束太过单一&#xff1b;比如我需要存储的是一个序号&#xff0c;那就不可…

也谈态势感知的嵌套与级联

不同颗粒度的态势感知可以嵌套在一起&#xff0c;形成一个层次结构&#xff0c;从而提供全面和多层次的信息获取和理解。 在态势感知中&#xff0c;颗粒度可以理解为观察、收集和分析信息的细节程度。较高颗粒度的态势感知关注的是具体的事件、行动或细节&#xff0c;提供了详细…

K最近邻算法:简单高效的分类和回归方法(三)

文章目录 &#x1f340;引言&#x1f340;训练集和测试集&#x1f340;sklearn中封装好的train_test_split&#x1f340;超参数 &#x1f340;引言 本节以KNN算法为主&#xff0c;简单介绍一下训练集和测试集、超参数 &#x1f340;训练集和测试集 训练集和测试集是机器学习和深…

在工作中使用ChatGPT需要担心泄密问题吗?

​OpenAI的ChatGPT可以通过自动简化繁琐的任务&#xff0c;针对挑战性问题的提供创造性的解决方案来提高员工的生产力。但随着这项技术被整合到人力资源平台和其他工作场所中&#xff0c;它给各个企业带来了巨大的挑战。苹果、Spotify、Verizon和三星等大公司已禁止或限制员工在…