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 つのプロセスであるため、競合します。具体的な詳細は以下のとおりです。
ここに画像の説明を挿入します

(2)、解決策

思いつく一般的な解決策は、送信と受信の両方を考慮して rtp セッションのみを確立するために jrtplib を使用し、ストリーミング メディアは 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

注: オーディオとビデオが同じ入力ソースからのものである場合、それらを 1 つのコマンドに結合することもできます。

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


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. エフェクトのプレビュー

SIP サーバーとして freeswitch を使用します。
このプログラムの実行効果:
ローカル mp4 を SIP にプッシュし
ここに画像の説明を挿入します
、ピアとして linphone を使用します。実行効果:
ここに画像の説明を挿入します


要約する

以上が今日お話しする内容ですが、この記事で使用されているテクノロジーは非常にシンプルですが、実装プロセスは少し複雑です。特にポート競合の問題は、原因究明に時間がかかり、解決策が偶然思いついただけで、SIP クライアント全体が早い段階でコード実装されていた可能性があります。この記事の実装方法では、SIP、ストリーミング メディア、RTP が非常にうまく分離されています。SIP は独立して実装でき、ストリーミング メディアは自由に選択でき、RTP セッションを共有する必要はありません。場合によっては、テスト プロジェクトを迅速に構築することが簡単になります。過度に。

おすすめ

転載: blog.csdn.net/u013113678/article/details/132126069