文章目录
- 一、前言
- 二、思考问题与解决方案
- 1、思考问题
- 2、解决方案
- 2.1、Unity中如何开启摄像头并对图像进行采样
- 2.2、图像如何中转给其他客户端
- 2.3、如何实现清晰度切换
- 2.4、客户端如何对图像进行解码并显示
- 三、实际操作
- 0、思维导图
- 1、界面设计与制作
- 2、UI素材获取
- 3、创建Unity工程
- 4、制作UI界面
- 5、下载Mirror网络插件
- 6、写C#代码
- 6.1、网络管理器:VideoChatNetwork.cs
- 6.2、摄像头画面:Player.cs
- 6.3、业务逻辑:MainLogic.cs
- 6.4、界面交互:MainPanel.cs
- 7、挂脚本
- 7.1、VideoChatNetwork脚本
- 7.2、Player脚本
- 7.3、MainPanel脚本
- 8、Editor环境下测试
- 9、发布应用
- 9.1、发布Windows平台exe
- 9.2、发布Android平台apk
- 10、真机测试
- 四、工程源码
- 五、完毕
一、前言
嗨,大家好,我是新发。
事情是这样的,我前几天写了一篇《【游戏开发实战】Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)》
有同学留言问我多人在线视频聊天切换清晰度怎么做,
嗯,作为一位热心的技术博主,我一般都是能帮则帮。
嘛,今天就来写个多人视频聊天的功能吧(并且可以切换清晰度)。
二、思考问题与解决方案
1、思考问题
多人视频聊天大家应该都不陌生,像腾讯视频会议那样,多个人的视频画面同时显示在界面中。
我的实现思路是每个客户端对本地摄像头画面进行采样,得到帧图像,然后对图像进行适当的压缩,转为字节流上传给服务端,接着服务端根据每个客户端设置的清晰度对帧图像进行压缩,然后转发帧图像给其他客户端,其他客户端接收到帧图像字节流后进行解码,最后显示到界面中。
画成图是这样子:
要实现上面的功能,我们需要先思考并解决以下几个必要问题:
1 Unity
中如何开启摄像头并对图像进行采样?
2 图像如何中转给其他客户端?
3 如何实现清晰度切换?
4 客户端如何对图像进行解码并显示?
2、解决方案
2.1、Unity中如何开启摄像头并对图像进行采样
Unity
提供了WebCamTexture
这个类,通过它我们可以很方便的访问摄像头图像。
具体用法我下文会讲。
2.2、图像如何中转给其他客户端
正常情况我们需要搭建一个中转的服务端,要实现数据的序列化、网络通信、数据的反序列化等。这里我想使用Mirror
网络库来实现。在之前那篇文章中我也有介绍过Mirror
:《【游戏开发实战】Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)》
2.3、如何实现清晰度切换
客户端把清晰度设置告诉服务端,服务端根据清晰度对图像进行压缩,把压缩后的图像下发给客户端。
由于我们是使用Mirror
,服务端也是Unity
客户端,所以我们可以直接使用Texture2D
的EncodeToJPG
接口对图像进行压缩,第二个参数就是压缩率,值从1~100
(默认是75
),
public static byte[] EncodeToJPG(this Texture2D tex, int quality);
2.4、客户端如何对图像进行解码并显示
通过网络传输过来的图像是字节流,我们需要把它反序列化为Unity
可现实的图像Texture2D
,我们直接使用Texture2D
的LoadImage
接口,
public static bool LoadImage(this Texture2D tex, byte[] data);
三、实际操作
下面,撸起袖子开始动手实际操作吧~
0、思维导图
养成好习惯,动手前先画思维图,如下:
1、界面设计与制作
使用axure
快速原型设计工具先简单设计一下界面,
登录界面,Host
就是作为房主,Client
就是作为路人,
视频聊天界面,排列显示多个视频画面,可切换视频清晰度,可随时退出房间,
2、UI素材获取
简单的UI
素材资源我是在阿里巴巴矢量图库上找,地址:https://www.iconfont.cn/
比如搜索按钮
,
找一个形状合适的,可以进行调色,我一般是调成白色,
因为Unity
中可以设置Color
,这样我们只需要一个白色按钮就可以在Unity
中创建不同颜色的按钮了。
弄点基础的美术资源,
注:那个头像是我自己用PhotoShop
画的哦,我之前用PhotoShop
画过一幅原创连环画,如下:
3、创建Unity工程
我使用的Unity
版本为2021.1.7f1c1 个人版
,我们要做的是一个多人视频聊天的功能,不需要使用到3D
相关的内容,所以我们创建工程时使用2D
模板,工程名就叫UnityVideoChat
吧~
创建成功,
4、制作UI界面
根据我们的原型设计,使用UGUI
制作界面:MainPanel.prefab
,
如下,
其中用于渲染视频图像的UI
独立做成一个预设:VideoImage.prefab
,
如下,方便进行克隆,使用RawImage
组件来显示图像,
5、下载Mirror网络插件
Mirror
的Unity Asset Store
地址:
https://assetstore.unity.com/packages/tools/network/mirror-129321
将Mirror
插件添加到自己的账号中,然后回到Unity
,在Package Manager
中就可以下载了,
下载下来导入Unity
中,
6、写C#代码
6.1、网络管理器:VideoChatNetwork.cs
先画个图,方便大家直观地知道VideoChatNetwork
做什么:
创建VideoChatNetwork.cs
脚本,它需要继承Mirror.NetworkManager
,
// VideoChatNetwork.cs
using Mirror;
public class VideoChatNetwork : NetworkManager
{
// ...
}
启动服务端:
StartHost();
启动客户端:
StartClient();
关闭服务端:
StopHost();
关闭客户端:
StopClient();
定义消息CreatePlayerMessage
(用于传递用户名):
public struct CreatePlayerMessage : NetworkMessage
{
public string name;
}
服务器启动成功回调,注册CreatePlayerMessage
消息响应函数,在响应函数中实例化Player
并添加到NetworkServer
中:
public override void OnStartServer()
{
base.OnStartServer();
// 注册事件
NetworkServer.RegisterHandler<CreatePlayerMessage>(OnCreatePlayer);
// ...
}
void OnCreatePlayer(NetworkConnection connection, CreatePlayerMessage createPlayerMessage)
{
// 实例化Player
GameObject playergo = Instantiate(playerPrefab);
playergo.GetComponent<Player>().accountName = createPlayerMessage.name;
// 添加Player
NetworkServer.AddPlayerForConnection(connection, playergo);
}
客户端连接成功回调:
public override void OnClientConnect(NetworkConnection conn)
{
base.OnClientConnect(conn);
// 转发通知
conn.Send(new CreatePlayerMessage { name = MainLogic.instance.accountName });
}
连接断开回调:
public override void OnClientDisconnect(NetworkConnection conn)
{
// TODO 重新登录
}
6.2、摄像头画面:Player.cs
Player
思维导图:
创建Player.cs
脚本,它需要继承Mirror.NetworkBehaviour
,
using Mirror;
public class Player : NetworkBehaviour
{
// ...
}
先定义一些必要的UI
对象,其中用户名
使用[SyncVar]
注解进行自动同步,
public RawImage videoImage;
[SyncVar]
public string accountName;
public Text accountNameText;
在Start
函数中判断是否是本地用户isLocalPlayer
,如果是,则开启摄像头:
// Player.cs
private WebCamTexture webCam;
private void Start()
{
if (isLocalPlayer)
{
// 开启摄像头
WebCamDevice[] devices = WebCamTexture.devices;
webCam = new WebCamTexture(devices[0].name, 128, 128, 5); //设置宽、高和帧率
webCam.Play();
}
// ...
}
在Update
函数中对摄像头图像进行采样,0.3秒
采集一帧,可适当进行调整,同时把图像转为字节流并发送给服务端,
// Player.cs
private float timer;
public void Update()
{
if (isLocalPlayer && null != webCam)
{
timer += Time.deltaTime;
if (timer > 0.3f)
{
timer = 0;
// 采样
videoImage.texture = webCam;
// 图像转字节流
var bytes = MainLogic.instance.WebCamTextureToBytes(webCam);
// 发送字节流给服务端
CmdSendTextureBytes(bytes);
}
}
}
发送图像字节流给服务端,注意Command
为客户端远程调用服务端,
// 发送图像字节流给服务端
// Command为客户端远程调用服务端
[Command]
public void CmdSendTextureBytes(byte[] texture)
{
RpcReceiveTexture(texture);
}
客户端接收服务端的图像字节流数据,并显示到RawImage
上,
// 客户端接收服务端的图像字节流数据,并显示到RawImage上
// ClientRpc为服务端远程调用客户端
[ClientRpc]
public void RpcReceiveTexture(byte[] textureBytes)
{
if(!isLocalPlayer)
{
// 压缩
var compressedTex = MainLogic.instance.CompressTexture(textureBytes);
// 显示
videoImage.texture = MainLogic.instance.BytesToTexture2D(compressedTex);
}
}
6.3、业务逻辑:MainLogic.cs
MainLogic
思维导图:
创建MainLogic.cs
脚本,全局唯一一个实例对象,我们使用单例模式:
// MainLogic.cs
public class MainLogic
{
// 单例模式
private static MainLogic s_instance;
public static MainLogic instance
{
get
{
if (null == s_instance)
s_instance = new MainLogic();
return s_instance;
}
}
}
定义成员变量:
/// <summary>
/// 用户名
/// </summary>
public string accountName;
/// <summary>
/// 清晰度,0:高清,1:标清,2:普通
/// </summary>
public int definition;
初始化,设置成员变量和回调函数:
private Action backToLoginCb;
/// <summary>
/// 初始化
/// </summary>
/// <param name="network">网络管理器</param>
/// <param name="backToLoginCb">回调登录界面的回调函数</param>
public void Init(VideoChatNetwork network, Action backToLoginCb)
{
this.network = network;
this.backToLoginCb = backToLoginCb;
}
public void OnClientDisconnect()
{
if (null != backToLoginCb)
backToLoginCb();
}
启动服务端,IP
地址默认是localhost
:
/// <summary>
/// 启动服务端
/// </summary>
/// <param name="ip">IP地址</param>
/// <param name="account">用户名</param>
/// <param name="cb">成功回调函数</param>
public void StartHost(string ip, string account, Action cb)
{
if (!NetworkClient.isConnected && !NetworkServer.active)
{
this.accountName = account;
network.networkAddress = ip;
network.DoStartHost(cb);
}
}
启动客户端,IP
地址默认是localhost
:
/// <summary>
/// 启动客户端
/// </summary>
/// <param name="ip">IP地址</param>
/// <param name="account">用户名</param>
/// <param name="cb">回调函数</param>
public void StartClient(string ip, string account, Action cb)
{
this.accountName = account;
network.networkAddress = ip;
network.DoStartClient(cb);
}
关闭网络:
/// <summary>
/// 关闭网络
/// </summary>
public void Close()
{
network.StopHost();
network.StopClient();
}
切换清晰度:
/// <summary>
/// 切换清晰度
/// </summary>
public void SwitchDefinition(int v)
{
definition = v;
}
/// <summary>
/// 根据图像清晰度进行图像压缩
/// </summary>
public byte[] CompressTexture(byte[] texture)
{
if (null == texture) return null;
switch (definition)
{
case 0:
default:
{
return texture;
}
case 1:
{
var tex2D = MainLogic.instance.BytesToTexture2D(texture);
return tex2D.EncodeToJPG(40);
}
case 2:
{
var tex2D = MainLogic.instance.BytesToTexture2D(texture);
return tex2D.EncodeToJPG(10);
}
}
}
字节流转Texture2D
:
/// <summary>
/// 字节流转Texture2D
/// </summary>
/// <param name="textureBytes">图像字节流</param>
/// <returns></returns>
public Texture2D BytesToTexture2D(byte[] textureBytes)
{
Texture2D tex2D = new Texture2D(30, 30);
tex2D.LoadImage(textureBytes);
return tex2D;
}
摄像头图像转字节流:
/// <summary>
/// 摄像头图像转字节流
/// </summary>
/// <param name="webCam">摄像头帧画面</param>
/// <param name="quality">压缩了,1~100,默认75</param>
/// <returns></returns>
public byte[] WebCamTextureToBytes(WebCamTexture webCam, int quality = 75)
{
Texture2D texture = new Texture2D(webCam.width, webCam.height);
int y = 0;
while (y < texture.height)
{
int x = 0;
while (x < texture.width)
{
Color color = webCam.GetPixel(x, y);
texture.SetPixel(x, y, color);
++x;
}
++y;
}
texture.Apply();
var bytes = texture.EncodeToJPG(quality);
return bytes;
}
6.4、界面交互:MainPanel.cs
MainPanel
思维导图:
创建MainPanel.cs
脚本,在MainPanel
脚本中我们去写界面交互的代码,
先定义一些必要的UI
成员,
// IP地址输入框
public InputField ipInput;
// 用户名输入框
public InputField accountInput;
// 房主按钮
public Button hostBtn;
// 房客按钮
public Button clientBtn;
// 清晰度下拉框
public Dropdown definitionDropdown;
// 登录UI父节点
public GameObject loginObj;
// 视频聊天UI父节点
public GameObject videoChatObj;
// 关闭网络按钮
public Button closeBtn;
在Awake
中进行初始化:
private void Awake()
{
var networkObj = GameObject.Find("NetworkManager");
MainLogic.instance.Init(networkObj.GetComponent<VideoChatNetwork>(), () =>
{
loginObj.SetActive(true);
videoChatObj.SetActive(false);
});
}
在Start
函数中设置各个按钮的响应逻辑:
void Start()
{
// Host按钮
hostBtn.onClick.AddListener(() =>
{
MainLogic.instance.StartHost(ipInput.text, accountInput.text, () =>
{
loginObj.SetActive(false);
videoChatObj.SetActive(true);
});
});
// Client按钮
clientBtn.onClick.AddListener(() =>
{
MainLogic.instance.StartClient(ipInput.text, accountInput.text, () =>
{
loginObj.SetActive(false);
videoChatObj.SetActive(true);
});
});
// 关闭网络按钮
closeBtn.onClick.AddListener(() =>
{
MainLogic.instance.Close();
loginObj.SetActive(true);
videoChatObj.SetActive(false);
});
// 分辨率下拉框
definitionDropdown.onValueChanged.AddListener((v) =>
{
MainLogic.instance.SwitchDefinition(v);
});
}
7、挂脚本
7.1、VideoChatNetwork脚本
场景中创建一个空物体,重命名为NetworkManager
,
在它身上挂上VideoChatNetwork
脚本(它会自动挂上KcpTransport
脚本),
给VideoChatNetwork
赋值Player Prefab
,并去掉Auto Create Player
的勾选,
KcpTransport
使用的是可靠UDP
传输,在KcpTransport
组件上可以设置端口号,
7.2、Player脚本
给VideoImage.prefab
预设根节点挂上````Player脚本(它会自动挂上
NetworkIdentity脚本),赋值
Player脚本的
UI```成员,
7.3、MainPanel脚本
给MainPanel.prefab
预设挂上MainPanel
脚本,并赋值UI
成员,
8、Editor环境下测试
在Editor
环境下运行,效果如下:
9、发布应用
9.1、发布Windows平台exe
转为PC
平台,添加场景,
设置窗口尺寸为1280*720
,
执行Build
,打出exe
,
9.2、发布Android平台apk
转为Android
平台,添加场景,
设置包名为com.linxinfa.videochat
,
执行Build
,打包出apk
,
10、真机测试
把apk
安装到手机上,手机可以正常访问摄像头,
我们在PC
端运行客户端,连接手机的IP
地址,
手机与电脑的视频聊天如下,画面清晰度切换,效果还是比较明显的,
四、工程源码
本文工程我以上次到CODE CHINA
,感兴趣的同学可自行下载下来学习。
地址:https://codechina.csdn.net/linxinfa/UnityVideoChat
注:我使用的Unity
版本为2021.1.7f1c1 个人版
五、完毕
好了,就写到这里吧,音频的同步我没有写,思路是使用Unity
的Microphone
进行声音采样,上传服务器进行转发,这里就留给各位去实现啦,
另外,如果本文的Mirror
网络库你不熟悉,可能看代码会比较吃力,建议可以先看看我之前那篇文章中关于Mirror
网络库的介绍:《【游戏开发实战】Unity使用Socket通信实现简单的多人聊天室(万字详解 | 网络 | TCP | 通信 | Mirror | Networking)》
天色已晚,我要去冲凉先了,拜拜~
我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信~