基于Unity与C#控制台的局域网音视频通话解决方案

前段时间由于工作上的需要,研究了一下局域网内音视频通话的需求,思路主要有以下几个点:

  1. Socket  UDP协议
  2. 分包组包
  3. RawImage加WebCamTexture
  4. Microphone加AudioClip
  5. 数据压缩

先说一下需求:

多人在局域网下,在不同设备\平台上进行音视频通话,通话质量要求不高。

平台用了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下载

下载已改为不需要积分,且不允许动态调分,大家随意下载。

如转载请注明出处。

  • 17
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰翼无痕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值