内容比较多,先一步步的讲吧。发送语音对讲之前,先发用对讲的信令。
广播请求(信令服务-->摄像头)
至于抓包分析,网上分享的很多了。其实都是发送 Broadcast 广播通知,
然后摄像头是广播通知应答,然后进行sdp 就行协议交换,可以简单理解成端口交换,其实就是音频流要推动到哪个端口,协商完毕,可以发送音频流。音频流发送结束,发送拜拜信令。整个流程就是如此,这里以
https://github.com/chatop2020/AKStream 这个开源项目为例子,带大家了解一下。这里我粘贴一下核心的代码,因为我也是自己测试的,是可以发声了。就是一个demo,大家可以整合到自己系统里。
第一步发送信令:
public bool BroadcastRequest(string deviceId, string channelId)
{
try
{
Common.SipServer.BroadcastRequest(deviceId, channelId, _autoResetEvent, _timeout);
_commandType = CommandType.Broadcast;
var isTimeout = _autoResetEvent.WaitOne(_timeout);
if (!isTimeout)
{
Console.WriteLine($"[{Common.LoggerHead}]->Sip代理语音广播失败");
return false;
}
return true;
}
finally
{
Dispose();
}
}
下面的代码是:
public void BroadcastRequest(string deviceId, string channelId, AutoResetEvent evnt, int timeout = 5000)
{
var tmpSipDevice = _globalData.SipDevices.FindLast(x => x.DeviceId.Equals(deviceId));
if (tmpSipDevice == null) //设备是否在列表中存在
{
try
{
evnt.Set();
}
catch (Exception ex)
{
throw new UserFriendlyException("BroadcastRequest-->AutoResetEvent.Set异常", ErrorNumber.Sys_AutoResetEventExcept.ToString(), details: ex.Message, ex);
}
return;
}
SIPMethodsEnum method = SIPMethodsEnum.MESSAGE;
VoiceBroadcastNotify voiceBroadcast = new VoiceBroadcastNotify()
{
CmdType = CommandType.Broadcast,
SourceID ="34020000002000000001",//deviceId,
TargetID = "34020000001370000001",//"34020000001370000001",//"34020000001320000068",//channelId,//"34020000001370000001",
SN = new Random().Next(1, ushort.MaxValue),
};
try
{
string xmlBody = VoiceBroadcastNotify.Instance.Save<VoiceBroadcastNotify>(voiceBroadcast);
string subject =
$"{_globalData.SipServerConfig.ServerSipDeviceId}:{0},{deviceId}:{new Random().Next(100, ushort.MaxValue)}";
Func<SipDevice, SIPMethodsEnum, string, string, string, CommandType, bool, AutoResetEvent,
AutoResetEvent, object, int, Task
>
request =
SendRequestViaSipDevice;
request(tmpSipDevice, method, ConstString.Application_MANSCDP, subject, xmlBody,
voiceBroadcast.CmdType, true, evnt, null, null, timeout);
}
catch (AkStreamException ex)
{
try
{
evnt.Set();
}
catch (Exception e)
{
throw new UserFriendlyException("BroadcastRequest-->AutoResetEvent.Set异常", ErrorNumber.Sys_AutoResetEventExcept.ToString(), details: e.Message, e);
}
}
}
上面的代码差不多就是协议发完了。
然后重点是发送rtp音频包,这个是我分享的重点。
public async Task PushAudio(string base64String, string deviceId, string channelId, string remoteIpAddress,
int remotePort, string localIpAddress, int localPort)
{
if (udpClient == null)
{
udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, localPort));
}
//byte[] pcmBytes = Convert.FromBase64String(base64String);
FileStream fs = new FileStream("C:\\recorder.pcm", FileMode.Open);
BinaryReader br = new BinaryReader(fs);
long fileSize = fs.Length;
byte[] pcmBytes = new byte[fileSize];
br.Read(pcmBytes, 0, (int)fileSize);
br.Close();
fs.Close();
var g711data = new Byte[pcmBytes.Length / 2];
for (Int32 i = 0, k = 0; i < pcmBytes.Length - 1; i += 2, k++)
{
var v = (Int16)(pcmBytes[i + 1] << 8 | pcmBytes[i]);
g711data[k] = LinearToAlaw(v);
}
var bytes = g711data;
var ms = new MemoryStream(bytes);
var length = 600;
Console.WriteLine($"本次发送的rtp次数是:{g711data.Length / length},总长度是:{g711data.Length}");
var packet = new RtpPacket();
while (ms.Position < ms.Length)
{
var _bytes = new byte[length];
var len = ms.Read(_bytes, 0, length);
packet.Write(_bytes, len);
packet.SequenceNumber++;
packet.Timestamp += length;
var packetArray = packet.ToArray();
//发送数据包
udpClient.Send(packetArray, packetArray.Length, new IPEndPoint(IPAddress.Parse(remoteIpAddress), remotePort));
if (len < length) break;
await Task.Delay(20);
}
}
用到的rtp帮助类是:
/// <summary>
/// Rtp数据包
/// </summary>
public class RtpPacket
{
#region 构造器
/// <summary>
/// 无参构造器
/// </summary>
public RtpPacket()
{
}
#endregion
#region 属性
/// <summary>
///
/// </summary>
public int PayloadType { get; set; } = 8;
/// <summary>
///
/// </summary>
public int SequenceNumber { get; set; } = 0;
/// <summary>
///
/// </summary>
public int Timestamp { get; set; } = 0;
/// <summary>
///
/// </summary>
public uint SSRC { get; set; } = 0;
/// <summary>
/// 内存流
/// </summary>
private MemoryStream Data { get; set; }
#endregion
#region 方法
/// <summary>
/// 添加数据流
/// </summary>
/// <param name="data"></param>
/// <param name="len"></param>
public void Write(byte[] data, int len = 0)
{
this.SetHeader();
this.Data.Write(data, 0, len == 0 ? data.Length : len);
}
/// <summary>
/// 设置头
/// </summary>
private void SetHeader()
{
this.Data = new MemoryStream();
this.WriteByte((byte)0x80);
var marker = 1 << 15;
var payloadType = (byte)this.PayloadType;
payloadType |= (byte)(marker >> 8);
this.WriteByte((byte)payloadType);
var sequenceNumber = (byte)(this.SequenceNumber >> 8);
sequenceNumber |= (byte)(marker & 0xFF);
this.WriteByte((byte)sequenceNumber);
this.WriteByte((byte)(this.SequenceNumber & 0xFF));
this.WriteByte((byte)(this.Timestamp >> 24));
this.WriteByte((byte)((this.Timestamp >> 16) & 0xFF));
this.WriteByte((byte)((this.Timestamp >> 8) & 0xFF));
this.WriteByte((byte)(this.Timestamp & 0xFF));
this.WriteByte((byte)(this.SSRC >> 24));
this.WriteByte((byte)((this.SSRC >> 16) & 0xFF));
this.WriteByte((byte)((this.SSRC >> 8) & 0xFF));
this.WriteByte((byte)(this.SSRC & 0xFF));
}
/// <summary>
/// 写字节
/// </summary>
/// <param name="b">字节</param>
private void WriteByte(byte b)
{
if (this.Data == null) this.Data = new MemoryStream();
this.Data.WriteByte(b);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public byte[] ToArray() => this.Data.ToArray();
#endregion
}
这个是音频pcm 转换的代码
private readonly Int16[] seg_end = { 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF };
private readonly Int32 QUANT_MASK = 0xf;
private readonly Int32 SEG_SHIFT = 4;
private Byte LinearToAlaw(Int16 pcm_val)
{
Int16 mask;
Int16 seg;
Char aval;
if (pcm_val >= 0)
mask = 0xD5;
else
{
mask = 0x55;
pcm_val = (Int16)(-pcm_val - 1);
if (pcm_val < 0)
pcm_val = 32767;
}
//Convert the scaled magnitude to segment number.
seg = Search(pcm_val, seg_end, 8);
//Combine the sign, segment, and quantization bits.
if (seg >= 8)
//out of range, return maximum value.
return (Byte)(0x7F ^ mask);
else
{
aval = (Char)(seg << SEG_SHIFT);
if (seg < 2) aval |= (Char)(pcm_val >> 4 & QUANT_MASK);
else aval |= (Char)(pcm_val >> seg + 3 & QUANT_MASK);
return (Byte)(aval ^ mask);
}
}
private static Int16 Search(Int16 val, Int16[] table, Int16 size)
{
for (Int16 i = 0; i < size; i++)
if (val <= table[i])
return i;
return size;
}
整体核心代码思路都在这里了,后期等我整理好一个漂亮的代码pr到到源码项目里,尽情等待。