eXosip+ffmpeg, ffplay 명령줄을 사용하여 sip 클라이언트 구현


머리말

화상 통화에 sip을 사용하는 경우 IP 카메라를 비디오 소스로 사용해야 하는 상황이 발생할 수 있습니다.정보를 확인한 후 pjsip을 사용하려면 일반적으로 소스 코드를 변경해야 합니다. pjsip에 포함된 기능은 완벽하지만 너무 크고 많은 기능이 필요하지 않습니다. 그리고 저자는 아이디어를 가지고 있습니다.eXosip과 같이 SIP 상호 작용을 처리할 수 있는 라이브러리가 있는 한 오디오 및 비디오 측면을 별도로 구현할 수 있습니다.예를 들어 먼저 ffmpeg 및 ffplay 명령줄을 오디오 및 비디오 테스트로 사용하십시오. , 성공 후 이를 구현하는 코드를 작성합니다. 이 기사는 성공적인 테스트 솔루션입니다. 정말 유연한 방법은 ffmpeg를 조정하는 코드를 작성하는 것입니다. 이 기사는 구현 아이디어 제공에 관한 것입니다.


1. 핵심 실현

주요 구현 단계는 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로 트랜스코딩된 로컬 파일을 사용하면 각 패킷 크기는 160바이트입니다.

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이고, 각 패킷 크기는 160바이트입니다.

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

2. 시푸아 인터페이스 디자인

#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();
};


3. 사용예

/// <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);
}

4. 완전한 코드

eXosip 버전은 5.1, ffmpeg.exe는 4.3, vs2022 프로젝트입니다.

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


5. 효과 미리보기

freeswitch를 sip 서버로 사용합니다.
이 프로그램의 실행 효과:
로컬 mp4를 푸시
여기에 이미지 설명을 삽입하세요.
하고 linphone을 피어로 사용합니다. 실행 효과:
여기에 이미지 설명을 삽입하세요.


요약하다

오늘 이야기할 내용은 위 내용인데, 이 글에서 사용한 기술은 매우 간단하지만, 구현 과정이 다소 험난하다. 특히 포트 충돌 문제의 경우 원인을 파악하는 데 시간이 많이 걸리고, 우연히 해결책이 떠올랐다. 그렇지 않으면 SIP 클라이언트 전체가 아주 일찍 코드로 구현되었을 수도 있다. 이 기사의 구현 방법은 SIP, 스트리밍 미디어 및 RTP를 매우 잘 분리합니다. SIP는 독립적으로 구현할 수 있고 스트리밍 미디어를 자유롭게 선택할 수 있으며 RTP 세션을 공유할 필요가 없습니다. 때로는 테스트 프로젝트를 빠르게 구축하는 것이 쉬워집니다. 너무 많은.

추천

출처blog.csdn.net/u013113678/article/details/132126069