前段时间由于工作上的需要,研究了一下局域网内音视频通话的需求,思路主要有以下几个点:
- Socket UDP协议
- 分包组包
- RawImage加WebCamTexture
- Microphone加AudioClip
- 数据压缩
先说一下需求:
多人在局域网下,在不同设备\平台上进行音视频通话,通话质量要求不高。
平台用了Unity,到时候如果需要的话改该发布平台就好了,版本是2018.2.2f1,VS2017。
相关代码贴一部分在最后,源码链接稍后也会贴在文章末尾,如有错误,希望大家不吝指正。
一、Socket UDP协议
由于需求内不要求通话质量很高,并且丢包影响不会太大,所以优先选择了UDP协议,图像数据每70毫秒发送一次,音频每500毫秒发送一次
暂时没有音画同步逻辑。
服务器只有转发功能,记录各个客户端的IP,接收到某个客户端的信息之后,给其他IP转发。
客户端负责信息传输的部分,包含发送、接收、分包组包等功能。
二、分包组包
每个包长度固定65045,前45个byte是包头,包头包含用户ID、包数量-完整数据被分成了几个包、包索引
三、RawImage加WebCamTexture
RawImage用来显示相机内容,WebCamTexture用来获取
WebCamTexture的像素可以自定义,requestedHeight和requestWidth
WebCamDevice有一个属性isFrontFacing,是否是前置摄像头
四、Microphone加AudioClip
Microphone用来获取麦克风,AudioClip存放音频数据float[],AudioSource播放
麦克风设备启动需要时间,所以这里我启动后等待了2.5秒
麦克风开始录音:
AudioClip = Microphone.Start(设备名, 是否循环录制, 单次录制时长(秒), 采样率(我用的是8000Hz,就是一般电话的采样率,人声清晰))
五、数据压缩
这个算法适用于重复率高的数据,然而这里的数据重复率并不高,所以压缩率大概80%左右,用的是GZip的dll
以下是部分源码
服务器:
const int Port = 8010;
static Socket socket;
static List<string> connected = new List<string>();
static void Main(string[] args)
{
try{
socket = new Socket(AddressFamiy.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
IPAddress IP = IPAddress.Any;
IPEndPoint endPoint = new IPEndPoint(IP, Port);
Socket.Bind(endPoint);
Console.WriteLine(“服务器已开启”);
Thread t = new Thread(ReciveMsg);
t.IsBackground = true;
t.Start();
}
catch(Exception e)
{
Console.WriteLine(“服务器开启失败!e:” + e);
}
}
/// <summary>
/// 接收发送给本机ip对应端口号的数据
/// </summary>
static void ReciveMsg()
{
while (true)
{
try
{
EndPoint point = new IPEndPoint(IPAddress.Any, 0);//用来保存发送方的ip和端口号
byte[] buffer = new byte[1024 * 1024];
int length = server.ReceiveFrom(buffer, ref point);//接收数据报
byte[] useBytes = new byte[length];
Array.Copy(buffer, 0, useBytes, 0, length);
if (!connected.Contains(point.ToString()))
{
connected.Add(point.ToString());
Console.WriteLine(point.ToString() + "连接成功!");
}
for (int i = connected.Count - 1; i >= 0; i--)
{
string ip = connected[i];
if (!ip.Equals(point.ToString()))
{
try
{
string[] split = ip.Split(':');
IPAddress IP = IPAddress.Parse(split[0]);
int port = int.Parse(split[1]);
EndPoint otherEndPoint = new IPEndPoint(IP, port);
server.SendTo(useBytes, otherEndPoint);
Console.WriteLine("转发成功, IP:" + ip);
}
catch (Exception e)
{
Console.WriteLine("转发失败,e:" + e);
connected.RemoveAt(i);
}
}
}
}
catch(Exception e)
{
Console.WriteLine("接收失败! e:" + e);
}
}
}
服务器和客户端参考引用了https://blog.csdn.net/dl962454/article/details/78990735这位大佬的代码
客户端:
接收数据:
///用来保存发送方的ip和端口号
EndPoint point = new IPEndPoint(IPAddress.Any, 0);
///定义客户端接收到的信息大小
byte[] buffer = new byte[1024 * 1024];
Debug.Log("Before---ReceiveFrom");
///接收到的信息大小(所占字节数)
int length = client.ReceiveFrom(buffer, ref point);
byte[] singlePackage = new byte[length];
Array.Copy(buffer, 0, singlePackage, 0, length);
发送数据:
// 分包
List<byte[]> list = SocketBytesUtils.GetByetsList(buffer, id);
// 分包后的数据,依次发送
for (int i = 0; i < list.Count; i++)
{
client.SendTo(list[i], serverEndPoint);
// 等1毫秒
await Task.Delay(1);
}
检查单个包,组成完整的一条数据,就组包存起来,使用后回收
这里是检查的代码:
private void CheckSinglePackage(byte[] singlePackage)
{
if (singlePackage.Length <= SocketBytesUtils.GetHeadLength())
return;
int id = SocketBytesUtils.GetReceiveByteID(singlePackage);
int count = SocketBytesUtils.GetReceivePackageCount(singlePackage);
int index = SocketBytesUtils.GetReceivePackageIndex(singlePackage);
SocketPackage socketPackage;
if (packageCacha.ContainsKey(id))
{
socketPackage = packageCacha[id];
}
else
{
socketPackage = new SocketPackage();
socketPackage.ID = id;
socketPackage.count = count;
socketPackage.list = new List<byte[]>();
socketPackage.useByteLength = 0;
packageCacha.Add(id, socketPackage);
}
socketPackage.AddBytes(singlePackage);
// 是否是结束数据,即没有丢包,正常结束的完整数据
bool endBytes = false;
// 是否是错误数据
bool errorBytes = false;
// 本次发送的数据包量和之前的不一致,是错误数据
if (socketPackage.count != count)
{
errorBytes = true;
}
else
{
// 是最后一条
if (index == count - 1)
{
// list长度和count一致
if (socketPackage.list.Count == count)
{
// 有效数据
receivedBuffer = SocketBytesUtils.GetReceiveUseBytes(socketPackage.list, socketPackage.useByteLength);
endBytes = true;
}
else
{
// 错误数据
errorBytes = true;
}
}
}
// 是结束的那个包 或 发现了丢包、错误数据
if (endBytes || errorBytes)
{
// 删除此ID的缓存包
packageCacha.Remove(id);
}
Debug.Log("CheckSinglePackage----End");
}
单个数据包的信息:
public class SocketPackage
{
public int ID;
public int count;
public int useByteLength;
public List<byte[]> list;
public void AddBytes(byte[] bytes)
{
list.Add(bytes);
useByteLength += bytes.Length - SocketBytesUtils.GetHeadLength();
}
}
分包组包:
using System;
using System.Collections.Generic;
using UnityEngine;
public class SocketBytesUtils
{
public static int MAX_BUFFER_LENGTH = 65045;
public static int ID_LENGTH = 15;
public static int LIST_COUNT = 15;
public static int LIST_INDEX = 15;
/// <summary>
/// 分包,每个数据包包括:
/// 用户ID + 包数量 + 此包索引 + 本包内有效数据
/// </summary>
/// <param name="buffer"></param>
/// <returns></returns>
public static List<byte[]> GetByetsList(byte[] buffer, int id)
{
// 有效数据长度 = 最大传输长度 - ID占位 - 包数量 - 包索引
int singleUseBytes = MAX_BUFFER_LENGTH - ID_LENGTH - LIST_COUNT - LIST_INDEX;
// 包数量
int listCount = buffer.Length / singleUseBytes;
listCount = buffer.Length % singleUseBytes <= 0 ? listCount : listCount + 1;
// ID -> bytes
byte[] idFullBytes = new byte[ID_LENGTH];
IntToBytes(id, idFullBytes);
// 包数量 -> bytes
byte[] listCountFullBytes = new byte[LIST_COUNT];
IntToBytes(listCount, listCountFullBytes);
// 初始化list
List<byte[]> bytesList = new List<byte[]>();
for (int i = 0; i < listCount; i++)
{
// 包索引 -> bytes
byte[] listIndexFullBytes = new byte[LIST_INDEX];
IntToBytes(i, listIndexFullBytes);
// 拼包
// 单个包所有数据
byte[] singleFullBytes = new byte[MAX_BUFFER_LENGTH];
// 最后一条,长度为剩余的所有byte
if (i == listCount - 1)
{
singleUseBytes = buffer.Length - singleUseBytes * i;
singleFullBytes = new byte[singleUseBytes + GetHeadLength()];
}
// 复制id数据
int startCopyIndex = 0;
Array.Copy(idFullBytes, 0, singleFullBytes, startCopyIndex, idFullBytes.Length);
// 复制包数量数据
startCopyIndex += idFullBytes.Length;
Array.Copy(listCountFullBytes, 0, singleFullBytes, startCopyIndex, listCountFullBytes.Length);
// 复制包索引数据
startCopyIndex += listCountFullBytes.Length;
Array.Copy(listIndexFullBytes, 0, singleFullBytes, startCopyIndex, listIndexFullBytes.Length);
// 复制本包有效数据
startCopyIndex += listIndexFullBytes.Length;
Array.Copy(buffer, (MAX_BUFFER_LENGTH - GetHeadLength()) * i, singleFullBytes, startCopyIndex, singleUseBytes);
// 添加到list内
bytesList.Add(singleFullBytes);
}
return bytesList;
}
/// <summary>
/// 获取接收到数据的用户ID
/// </summary>
/// <param name="buffer"></param>
/// <returns></returns>
public static int GetReceiveByteID(byte[] buffer)
{
byte[] idBytes = new byte[ID_LENGTH];
Array.Copy(buffer, 0, idBytes, 0, ID_LENGTH);
return BytesToInt32(idBytes);
}
/// <summary>
/// 获取接收到数据的包数量
/// </summary>
/// <param name="buffer"></param>
/// <returns></returns>
public static int GetReceivePackageCount(byte[] buffer)
{
byte[] countBytes = new byte[LIST_COUNT];
Array.Copy(buffer, ID_LENGTH, countBytes, 0, LIST_COUNT);
return BytesToInt32(countBytes);
}
/// <summary>
/// 获取接收到数据的包索引
/// </summary>
/// <param name="buffer"></param>
/// <returns></returns>
public static int GetReceivePackageIndex(byte[] buffer)
{
byte[] indexBytes = new byte[LIST_INDEX];
Array.Copy(buffer, ID_LENGTH + LIST_COUNT, indexBytes, 0, LIST_INDEX);
return BytesToInt32(indexBytes);
}
/// <summary>
/// 组包
/// </summary>
/// <param name="packageList"></param>
/// <param name="useByteLength"></param>
/// <returns></returns>
public static byte[] GetReceiveUseBytes(List<byte[]> packageList, int useByteLength)
{
//Debug.LogError("GetReceiveUseBytes: listCount(" + packageList.Count + "), useByteLength(" + useByteLength + ")");
//Debug.LogError("GetReceiveUseBytes: lastBytesLength(" + packageList[packageList.Count - 1].Length + ")");
byte[] useBytes = new byte[useByteLength];
// 单个包有效数据起点
int headLength = GetHeadLength();
for (int i = 0; i < packageList.Count; i++)
{
byte[] singlePackage = packageList[i];
// 单个包内有效数据长度
int singleUseLength = singlePackage.Length - headLength;
// 此包的索引,一般情况下与i相等
int singleIndex = GetReceivePackageIndex(singlePackage);
// 单个包内有效数据最大长度
int singleMaxUseLength = MAX_BUFFER_LENGTH - headLength;
// 最后一个包
if (i == packageList.Count - 1)
{
// 有效数据 = 接收数组长度 - 之前所有数据长度
singleUseLength = useBytes.Length - singleIndex * singleMaxUseLength;
}
if (singleUseLength > useBytes.Length - singleIndex * singleMaxUseLength || singleUseLength <= 0)
{
Debug.LogError("数据接收错误");
}
// 依次拷贝进useBytes
Array.Copy(singlePackage, headLength, useBytes, singleIndex * singleMaxUseLength, singleUseLength);
}
return useBytes;
}
/// <summary>
/// bytes转int
/// </summary>
/// <returns></returns>
public static int BytesToInt32(byte[] bytes)
{
int id = BitConverter.ToInt32(bytes, 0);
return id;
}
/// <summary>
/// 获取包头长度
/// </summary>
/// <returns></returns>
public static int GetHeadLength()
{
return ID_LENGTH + LIST_COUNT + LIST_INDEX;
}
/// <summary>
/// Int转Bytes
/// </summary>
/// <param name="value"></param>
/// <param name="fullBytes"></param>
public static void IntToBytes(int value, byte[] fullBytes)
{
// 清空数据
Array.Clear(fullBytes, 0, fullBytes.Length);
//Convert int to bytes
byte[] bytesToSendCount = BitConverter.GetBytes(value);
//Copy result to fullBytes
bytesToSendCount.CopyTo(fullBytes, 0);
}
}
这个没有参考,按自己需求写的,转载的话请标注出处。
RawImage加WebCamTexture
对象:
WebCamTexture webCam;
Texture2D currTexture;
RawImage mSelfCamera;
初始化:
webCam = new WebCamTexture();
webCam.deviceName = GetFrontFacingName();
webCam.requestedHeight = 300;
webCam.requestedWidth = 300;
mSelfCamera.texture = webCam;
webCam.Play();
currTexture = new Texture2D(webCam.width, webCam.height);
获取图像数据:
currTexture.SetPixels(webCam.GetPixels());
byte[] pngBytes = currTexture.EncodeToPNG();
显示图像数据:
bool result = tex.LoadImage(pngBytes);
if (result)
{
targetImage.texture = tex;
}
else
{
Debug.LogWarning("此图片数据错误,无法加载,不做显示-----------");
}
Microphone加AudioClip
对象:
AudioClip mAudioClip;
string mDevice;
初始化:
mDevice = Microphone.devices[0];
mAudioClip = Microphone.Start(mDevice, true, 60, AudioUtils.FREQUENCY);
// 这里等待2.5秒,启动麦克风,源码不贴,可以用协程、异步、Sleep啥的
获取音频数据:
float[] audioFloats = new float[point - lastPoint];
mAudioClip.GetData(audioFloats, lastPoint);
lastPoint = point;
// AudioClip数据读出来是float[],转换成byte[]
byte[] audioBytes = AudioUtils.FloatToByte(audioFloats);
audioBytes = CompressedUtils.GZipCompress(audioBytes);
播放音频:
AudioSource mAudioSource;
audioBytes = CompressedUtils.GZipDeCompress(audioBytes);
AudioClip audioClip = AudioUtils.AudioByteToClip(audioBytes, 1, AudioUtils.FREQUENCY);
mAudioSource.clip = audioClip;
mAudioSource.loop = false;
mAudioSource.Play();
下面是AudioUtils,操作音频数据:
using System;
using UnityEngine;
public class AudioUtils
{
public static int FREQUENCY = 8000;
/// <summary>
/// 音频转bytes
/// </summary>
/// <param name="clip"></param>
/// <returns></returns>
public static byte[] FloatToByte(float[] data)
{
int rescaleFactor = 32767; //to convert float to Int16
byte[] outData = new byte[data.Length * 2];
for (int i = 0; i < data.Length; i++)
{
short temshort = (short) (data[i] * rescaleFactor);
byte[] temdata = BitConverter.GetBytes(temshort);
outData[i * 2] = temdata[0];
outData[i * 2 + 1] = temdata[1];
}
return outData;
}
/// <summary>
/// 音频转bytes
/// </summary>
/// <param name="clip"></param>
/// <returns></returns>
public static byte[] AudioClipToByte(AudioClip clip)
{
float[] data = new float[clip.samples];
clip.GetData(data, 0);
return FloatToByte(data);
}
/// <summary>
/// bytes转音频
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static AudioClip AudioByteToClip(byte[] bytes, int channels, int frequency)
{
if (bytes.Length <= 0)
{
return null;
}
float[] data = new float[bytes.Length / 2];
for (int i = 0; i < data.Length; i++)
{
data[i] = ByteToFloat(bytes[i * 2], bytes[i * 2 + 1]);
}
//data = NoiseReduction(data);
AudioClip clip = AudioClip.Create("Clip", data.Length, channels, frequency, false);
clip.SetData(data, 0);
return clip;
}
/// <summary>
/// byte转float
/// </summary>
/// <param name="firstByte"></param>
/// <param name="secondByte"></param>
/// <returns></returns>
private static float ByteToFloat(byte firstByte, byte secondByte)
{
// convert two bytes to one short (little endian)
//小端和大端顺序要调整
short s;
if (BitConverter.IsLittleEndian)
s = (short) ((secondByte << 8) | firstByte);
else
s = (short) ((firstByte << 8) | secondByte);
// convert to range from -1 to (just below) 1
return s / 32768.0F;
}
}
复制了这里大佬的代码:https://blog.csdn.net/BDDNH/article/details/103381518
压缩:
using ICSharpCode.SharpZipLib.GZip;
public static byte[] GZipCompress(byte[] bytesToCompress)
{
// 其他地方有源码,这里就不贴了
}
public static byte[] GZipDeCompress(byte[] data)
{
// 其他地方有源码,这里就不贴了
}
本文作于2020年10月27日。
本文链接:基于Unity与C#控制台的局域网音视频通话解决方案_冰翼无痕的博客-CSDN博客
源码链接:音视频通话源码.rar_unity可以局域网视频通话吗-C#文档类资源-CSDN下载
下载已改为不需要积分,且不允许动态调分,大家随意下载。
如转载请注明出处。