WebRTC Video Chat
作者:追风剑情 发布于:2023-12-13 11:28 分类:Unity3d
WebRTC Video Chat
Unity跨平台视频通话插件WebRtcVideoChat,可实现PC、Android、IOS跨平台的视频通话,可语音、视频以及发送文字。
[百度网盘] WebRTC Video Chat 0.9854.unitypackage 提取码 ijys
[GitHub] WebRTC Signaling Server
注意:在Android平台上创建两个网络对象会报错 Channel is unrecoverably broken and will be disposed! 导致闪退。
注意:4G/5G网络无法建立P2P
视频画面需要逆时针旋转90度再显示,下面是利用Shader旋转uv
fixed4 frag(v2f i) : SV_Target
{
//逆时针旋转90度
fixed2 uv = fixed2(i.uv.y, i.uv.x);
fixed4 col = tex2D(_MainTex, uv);
return col;
}
WebRTC辅助类
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using Byn.Awrtc;
using Byn.Awrtc.Unity;
/// <summary>
/// WebRtcVideoChat 辅助类
/// 同一个网络对象上只能创建一个房间,但可以加入多个房间
/// 注意:Android平台上不能同时创建多个网络对象(Network),会报错闪退。
/// </summary>
public sealed class WebRTCHelper
{
#region 变量
//信令服务器地址 (这是官方的测试地址)
public static string uSignalingUrl = "ws://signaling.because-why-not.com/chatapp";
//信令服务器地址,基于WebGL的浏览器用这个地址 (这是官方的测试地址)
public static string uSecureSignalingUrl = "wss://signaling.because-why-not.com/chatapp";
//true: 强制使用 uSecureSignalingUrl
public static bool uForceSecureSignaling = false;
//NAT穿透服务器地址, Stun server。(这是官方的测试地址)
public static readonly string stunUrl = "stun:stun.because-why-not.com:443";
//NAT穿透服务器地址, Turn server。(这是官方的测试地址)
public static readonly string turnUrl = "turn:turn.because-why-not.com:443";
//Turn server user
public static readonly string turnUser = "testuser140";
//Turn server password
public static readonly string turnPass = "pass140";
//NAT穿透服务器地址 (Turn server 或 Stun server)
public static string uIceServer = turnUrl;
//备用服务器
public static string uIceServer2 = "stun:stun.l.google.com:19302";
public static string uIceServerUser = turnUser;
public static string uIceServerPassword = turnPass;
//可以是 native webrtc 或 browser webrtc
//BasicNetwork用于收发自定义数据
private static IBasicNetwork mBasicNetwork = null;
//谁开房间,谁就作为服务器方
private static bool mIsServer = false;
//房间名允许的最长字符个数,不要使用中文
private const int MAX_CODE_LENGTH = 256;
//连接ID列表
private static List<ConnectionId> mConnections = new List<ConnectionId>();
//用于音/视频聊天的对象
private static ICall mCall;
//true: 每次本地视频设备创建新帧时,FrameUpdateEventArgs都会更新
private static bool mLocalFrameEvents = true;
private static ConnectionId mRemoteUserId;
private static MediaConfig mMediaConfig;
private static MediaConfig mMediaConfigInUse;
public static Action<string> OnTextHandler;
public static Action<string> OnLogHandler;
public static Action<FrameUpdateEventArgs> OnVideoFrameUpdate;
//信令协议
public static string SignalingProtocol
{
get
{
string protocol = "ws";
if (Application.platform == RuntimePlatform.WebGLPlayer)
{
protocol = "wss";
}
return protocol;
}
}
#endregion
#region 初始化
/// <summary>
/// 初始WebRTC
/// </summary>
/// <param name="initBasicNetwork">初始化基本网络对象用于:收发消息</param>
/// <param name="initCall">初始化Call对象用于:收发消息、音/视频通话</param>
public static void Init(bool initBasicNetwork=true, bool initCall=true)
{
InitFactory();
if (initBasicNetwork)
InitBasicNetwork();
if (initCall)
InitCall();
}
#endregion
#region Factory
//初始化 WebRtc Network Factory
private static void InitFactory()
{
Debug.Log("Initializing webrtc factory");
UnityCallFactory.EnsureInit(OnCallFactoryReady, OnCallFactoryFailed);
}
private static void OnCallFactoryReady()
{
Debug.Log("WebRtcNetworkFactory created");
UnityCallFactory.Instance.RequestLogLevel(UnityCallFactory.LogLevel.Info);
}
private static void OnCallFactoryFailed(string error)
{
string fullErrorMsg = "The " + typeof(UnityCallFactory).Name + " failed to initialize with following error: " + error;
Debug.LogError(fullErrorMsg);
}
#endregion
#region 基本网络
//初始化 WebRTC BasicNetwork
private static void InitBasicNetwork()
{
if (mBasicNetwork != null)
return;
Debug.Log("Initializing webrtc network");
string signalingUrl = uSignalingUrl;
if (Application.platform == RuntimePlatform.WebGLPlayer || uForceSecureSignaling)
{
signalingUrl = uSecureSignalingUrl;
}
List<IceServer> iceServers = new List<IceServer>();
if (string.IsNullOrEmpty(uIceServer) == false)
iceServers.Add(new IceServer(uIceServer, uIceServerUser, uIceServerPassword));
if (string.IsNullOrEmpty(uIceServer2) == false)
iceServers.Add(new IceServer(uIceServer2));
if (string.IsNullOrEmpty(signalingUrl))
{
throw new InvalidOperationException("set signaling url is null or empty");
}
mBasicNetwork = UnityCallFactory.Instance.CreateBasicNetwork(signalingUrl, iceServers.ToArray());
if (mBasicNetwork != null)
{
Debug.Log("WebRTCNetwork created");
}
else
{
Debug.Log("Failed to access webrtc ");
}
}
/// <summary>
/// 断开某个连接
/// </summary>
/// <param name="id">连接ID</param>
public static void Disconnect(ConnectionId id)
{
if (mBasicNetwork == null)
return;
mBasicNetwork.Disconnect(id);
}
/// <summary>
/// 断开所有连接
/// </summary>
public static void DisconnectAll()
{
if (mBasicNetwork == null)
return;
mBasicNetwork.Dispose();
}
//检查网络事件,需在FixedUpdate()中调用此方法
public static void HandleNetwork()
{
if (mBasicNetwork == null)
return;
//从底层读取数据
mBasicNetwork.Update();
//处理自上次更新以来发生的所有新事件
NetworkEvent evt;
//检查每个事件
while (mBasicNetwork != null && mBasicNetwork.Dequeue(out evt))
{
Debug.LogFormat("Handler Network Event: {0}", evt);
switch (evt.Type)
{
//服务器初始化完成
case NetEventType.ServerInitialized:
mIsServer = true;
string address = evt.Info;
Debug.Log("Server started. Address: " + address);
break;
//启动服务器失败,可能是信令服务器关闭了
case NetEventType.ServerInitFailed:
mIsServer = false;
Debug.Log("Server start failed." + evt.Info);
Dispose();
break;
//服务器关闭
case NetEventType.ServerClosed:
mIsServer = false;
Debug.LogFormat("Server closed. {0}", evt.Info);
break;
//用户运行客户端并连接到服务器
//用户运行该服务器,并连接了一个新的客户端
case NetEventType.NewConnection:
mConnections.Add(evt.ConnectionId);
Debug.Log("New local connection! ID: " + evt.ConnectionId);
//如果是服务器,请向所有人发送公告,并使用本地Id作为用户名
if (mIsServer)
{
//用户运行一个服务器, 向大家宣布新的连接.
//使用服务器端连接ID作为标识
string msg = "New user " + evt.ConnectionId + " joined the room.";
Debug.Log(msg);
SendString(msg);
}
break;
//服务器收到失败连接
//与服务器建立连接失败
case NetEventType.ConnectionFailed:
if (mIsServer)
{
//收到一个失败连接
//原因: 收到了连接请求,但信令传送失败。
//1.防火墙阻止了直接连接,但允许信号启动连接过程(可能是本地防火墙,也可能是远端防火墙)
//2.STUN/TURN 服务器未启动
//3.用户在连接完全建立之前就切断了连接
Debug.Log("An incoming connection failed.");
}
else
{
//与服务器建立连接失败
Debug.Log("Connection failed. " + evt.Info);
Dispose();
}
break;
//连接中断
case NetEventType.Disconnected:
mConnections.Remove(evt.ConnectionId);
Debug.Log("Local Connection ID " + evt.ConnectionId + " disconnected");
if (mIsServer)
{
string userLeftMsg = "User " + evt.ConnectionId + " left the room.";
Debug.Log(userLeftMsg);
//通过其他客户端,有玩家离线
if (mConnections.Count > 0)
{
SendString(userLeftMsg);
}
}
else
{
Dispose();
}
break;
//收到可靠消息
case NetEventType.ReliableMessageReceived:
//收到不可靠消息
case NetEventType.UnreliableMessageReceived:
HandleIncommingMessage(ref evt);
break;
}
}
//如果更新期间网络未中断,请刷新消息以完成此更新
if (mBasicNetwork != null)
mBasicNetwork.Flush();
}
//处理收到的消息
private static void HandleIncommingMessage(ref NetworkEvent evt)
{
MessageDataBuffer buffer = (MessageDataBuffer)evt.MessageData;
string msg = Encoding.UTF8.GetString(buffer.Buffer, 0, buffer.ContentLength);
OnTextHandler?.Invoke(msg);
//如果服务器将消息转发给包括发件人在内的所有其他人
if (mIsServer)
{
//我们使用服务器端连接id来标识客户端
string idAndMessage = evt.ConnectionId + ":" + msg;
Debug.Log(idAndMessage);
//SendString(idAndMessage);
}
else
{
//客户端收到服务器发来的消息
Debug.Log(msg);
}
//返回缓冲区,以便网络可以重用它
buffer.Dispose();
}
//发送消息
public static void SendString(string msg, bool reliable = true)
{
if (mBasicNetwork != null)
{
byte[] msgData = Encoding.UTF8.GetBytes(msg);
foreach (ConnectionId id in mConnections)
{
mBasicNetwork.SendData(id, msgData, 0, msgData.Length, reliable);
}
}
else if (mCall != null)
{
mCall.Send(msg, reliable);
}
}
/// <summary>
/// 创建房间 (仅支持文本聊天)
/// </summary>
/// <param name="roomName">房间名称(不能超过256个字符)</param>
public static void OpenChatRoom(string roomName)
{
InitBasicNetwork();
roomName = EnsureLength(roomName);
//如果该地址正在使用中,则底层系统将返回服务器连接失败
mBasicNetwork.StartServer(roomName);
Debug.Log("Open Room " + roomName);
}
/// <summary>
/// 关闭房间
/// 房间关闭后,将不再接受新用户的连接,但不影响现有已建立连接的用户
/// 当房间达到预设的最大人数后,可以调用此方法
/// </summary>
public static void CloseChatRoom()
{
if (mBasicNetwork == null)
return;
mBasicNetwork.StopServer();
Debug.Log("Close Room");
}
/// <summary>
/// 加入房间,可以同时加入不同的房间
/// </summary>
/// <param name="roomName">房间名称(不能超过256个字符)</param>
public static void JoinChatRoom(string roomName)
{
InitBasicNetwork();
roomName = EnsureLength(roomName);
mBasicNetwork.Connect(roomName);
Debug.Log("Join Room " + roomName + " ...");
}
//确保房间名称长度不超过256
private static string EnsureLength(string roomName)
{
if (roomName.Length > MAX_CODE_LENGTH)
{
roomName = roomName.Substring(0, MAX_CODE_LENGTH);
}
return roomName;
}
#endregion
#region 视频聊天
//初始化Call, 音/视频聊天需要调用这个方法
private static void InitCall()
{
if (mCall != null)
return;
NetworkConfig config = CreateNetworkConfig();
mCall = CreateCall(config);
if (mCall == null)
{
Debug.Log("Failed to create the call");
return;
}
mCall.LocalFrameEvents = mLocalFrameEvents;
mCall.CallEvent += OnCallEvent;
if (mMediaConfig == null)
mMediaConfig = CreateMediaConfig();
mMediaConfigInUse = mMediaConfig.DeepClone();
//如果没有配置摄像头设备名称,则自动设置为前置摄像头
if (mMediaConfigInUse.Video && string.IsNullOrEmpty(mMediaConfigInUse.VideoDeviceName))
{
string[] devices = UnityCallFactory.Instance.GetVideoDevices();
if (devices == null || devices.Length == 0)
{
Debug.Log("no device found or no device information available");
}
else
{
foreach (string s in devices)
Debug.Log("device found: " + s + " IsFrontFacing: " + UnityCallFactory.Instance.IsFrontFacing(s));
}
mMediaConfigInUse.VideoDeviceName = UnityCallFactory.Instance.GetDefaultVideoDevice();
}
Debug.Log("Configure call using MediaConfig: " + mMediaConfigInUse);
//设置配置
mCall.Configure(mMediaConfigInUse);
}
//创建网络配置
private static NetworkConfig CreateNetworkConfig()
{
NetworkConfig netConfig = new NetworkConfig();
if (string.IsNullOrEmpty(uIceServer) == false)
netConfig.IceServers.Add(new IceServer(uIceServer, uIceServerUser, uIceServerPassword));
if (string.IsNullOrEmpty(uIceServer2) == false)
netConfig.IceServers.Add(new IceServer(uIceServer2));
if (Application.platform == RuntimePlatform.WebGLPlayer || uForceSecureSignaling)
{
netConfig.SignalingUrl = uSecureSignalingUrl;
}
else
{
netConfig.SignalingUrl = uSignalingUrl;
}
if (netConfig.SignalingUrl == "")
{
throw new InvalidOperationException("set signaling url is empty");
}
return netConfig;
}
//创建Call对象
private static ICall CreateCall(NetworkConfig netConfig)
{
return UnityCallFactory.Instance.Create(netConfig);
}
//创建媒体配置
public static MediaConfig CreateMediaConfig()
{
MediaConfig mediaConfig = new MediaConfig();
//是否消除回音 (native only)
bool useEchoCancellation = true;
if (useEchoCancellation)
{
#if (!UNITY_WEBGL && !UNITY_WSA)
var nativeConfig = new Byn.Awrtc.Native.NativeMediaConfig();
nativeConfig.AudioOptions.echo_cancellation = true;
mediaConfig = nativeConfig;
#endif
}
#if UNITY_WSA && !UNITY_EDITOR
var uwpConfig = new Byn.Awrtc.Uwp.UwpMediaConfig();
uwpConfig.Mrc = true;
//uwpConfig.ProcessLocalFrames = false;
//uwpConfig.DefaultCodec = "H264";
mediaConfig = uwpConfig;
Debug.Log("Using uwp specific media config: " + mediaConfig);
#endif
//开启音频
mediaConfig.Audio = true;
//开启视频
mediaConfig.Video = true;
mediaConfig.VideoDeviceName = null;
//图像格式
mediaConfig.Format = FramePixelFormat.ABGR;
//最小分辨率
mediaConfig.MinWidth = 160;
mediaConfig.MinHeight = 120;
//分辨率越大编码/解码越慢
mediaConfig.MaxWidth = 1920 * 2;
mediaConfig.MaxHeight = 1080 * 2;
//WebRTC会优先考虑IdealWidth、IdealHeight、IdealFrameRate
//理想分辨率、帧率
mediaConfig.IdealWidth = 160;
mediaConfig.IdealHeight = 120;
mediaConfig.IdealFrameRate = 30;
return mediaConfig;
}
//创建视频聊天房间
public static void OpenCallRoom(string roomName)
{
InitCall();
mCall.Listen(roomName);
}
//加入视频聊天房间
public static void JoinCallRoom(string roomName)
{
InitCall();
//不要在会议模式下使用Call接口
mCall.Call(roomName);
}
//是否设置成了静音
public static bool IsMute()
{
if (mCall == null)
return true;
return mCall.IsMute();
}
//设置静音
public static void SetMute(bool val)
{
if (mCall == null)
return;
mCall.SetMute(val);
}
//检查手机扬声器是否已打开。仅适用于移动本地平台
public static bool GetLoudspeakerStatus()
{
if (mCall != null)
{
return UnityCallFactory.Instance.GetLoudspeakerStatus();
}
return false;
}
//打开/关闭手机扬声器。仅适用于移动本地平台
public static void SetLoudspeakerStatus(bool state)
{
if (mCall != null)
{
UnityCallFactory.Instance.SetLoudspeakerStatus(state);
}
}
//设置是否显示本地视频
//如果不需要显示本地摄像头画面,可以设置成false
public static void SetShowLocalVideo(bool showLocalVideo)
{
mLocalFrameEvents = showLocalVideo;
}
//视频聊天驱动方法,需要每帧调用
public static void CallUpdate()
{
if (mCall == null)
return;
//将底层事件转发到Unity线程中
mCall.Update();
}
//处理Call事件
private static void OnCallEvent(object sender, CallEventArgs e)
{
Debug.Log(e.Type);
switch (e.Type)
{
//呼出电话成功或来电到达
case CallEventType.CallAccepted:
{
//HasAudioTrack() 判断对方麦克风是否可用
//HasVideoTrack() 判断对方摄像头是否可用
mRemoteUserId = ((CallAcceptedEventArgs)e).ConnectionId;
string msg = "New connection with id: " + mRemoteUserId
+ " audio:" + mCall.HasAudioTrack(mRemoteUserId)
+ " video:" + mCall.HasVideoTrack(mRemoteUserId);
Debug.Log(msg);
OnLogHandler?.Invoke(msg);
mConnections.Add(mRemoteUserId);
}
break;
//通话结束或其中一个用户挂断电话
case CallEventType.CallEnded:
CleanupCall();
break;
//来电失败
case CallEventType.ListeningFailed:
{
ErrorEventArgs args = e as ErrorEventArgs;
string msg = "ListeningFailed: " + args.Info;
Debug.Log(msg);
OnLogHandler?.Invoke(msg);
}
break;
//连接房间失败
case CallEventType.ConnectionFailed:
{
ErrorEventArgs args = e as ErrorEventArgs;
string msg = "ConnectionFailed: " + args.Info;
Debug.Log(msg);
OnLogHandler?.Invoke(msg);
CleanupCall();
}
break;
//当前系统不支持摄像头或麦克风,或不支持请求的分辨率,或权限不足
case CallEventType.ConfigurationFailed:
{
ErrorEventArgs args = e as ErrorEventArgs;
string msg = "ConfigurationFailed: " + args.Info;
Debug.Log(msg);
OnLogHandler?.Invoke(msg);
CleanupCall();
}
break;
//收到一个新的视频帧(来自于本地摄像头或网络)
case CallEventType.FrameUpdate:
if (e is FrameUpdateEventArgs)
{
var evt = e as FrameUpdateEventArgs;
OnVideoFrameUpdate?.Invoke(evt);
}
break;
//收到文本消息
case CallEventType.Message:
{
MessageEventArgs args = e as MessageEventArgs;
Debug.Log(args.Content);
OnTextHandler?.Invoke(args.Content);
}
break;
//接到来电
case CallEventType.WaitForIncomingCall:
{
//聊天应用程序将等待另一个应用程序通过相同的字符串连接
WaitForIncomingCallEventArgs args = e as WaitForIncomingCallEventArgs;
string msg = "WaitForIncomingCall: " + args.Address;
Debug.Log(msg);
OnLogHandler?.Invoke(msg);
}
break;
}
}
//清理Call
public static void CleanupCall()
{
if (mCall == null)
return;
mCall.CallEvent -= OnCallEvent;
mCall.Dispose();
mCall = null;
mRemoteUserId = ConnectionId.INVALID;
mConnections.Clear();
GC.Collect();
GC.WaitForPendingFinalizers();
Debug.Log("Call destroyed");
}
//请求权限
public static IEnumerator RequestPermissions(bool audio = true, bool video = true)
{
if (audio)
{
yield return RequestAudioPermission();
}
if (video)
{
yield return RequestVideoPermission();
}
yield return null;
}
//请求麦克风权限
public static IEnumerator RequestAudioPermission()
{
#if UNITY_ANDROID && UNITY_2018_3_OR_NEWER
if (!HasAudioPermission())
{
Debug.Log("Requesting microphone permissions");
UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.Microphone);
//同一帧中连续调用两次请求会导致请求被忽略
//等待用户按下 "允许" 或 "拒绝"
yield return new WaitForSeconds(0.1f);
//极少数情况下即使用户选择"允许",也可能返回false
Debug.Log("microphone permission: " + HasAudioPermission());
}
#endif
yield return null;
}
//请求摄像头权限
public static IEnumerator RequestVideoPermission()
{
#if UNITY_ANDROID && UNITY_2018_3_OR_NEWER
if (!HasVideoPermission())
{
Debug.Log("Requesting camera permissions");
UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.Camera);
yield return new WaitForSeconds(0.1f);
Debug.Log("camera permission: " + HasVideoPermission());
}
#endif
yield return null;
}
//是否有使用麦克风的权限
public static bool HasAudioPermission()
{
#if UNITY_ANDROID && UNITY_2018_3_OR_NEWER
if (Application.platform == RuntimePlatform.Android)
{
return UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Microphone);
}
#endif
return true;
}
//是否有使用摄像头的权限
public static bool HasVideoPermission()
{
#if UNITY_ANDROID && UNITY_2018_3_OR_NEWER
if (Application.platform == RuntimePlatform.Android)
{
return UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera);
}
#endif
return true;
}
#endregion
#region 释放资源
//释放
public static void Dispose()
{
if (mBasicNetwork != null)
{
mBasicNetwork.Dispose();
mBasicNetwork = null;
}
mConnections.Clear();
mIsServer = false;
CleanupCall();
}
#endregion
}
视频通话测试
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Byn.Awrtc;
using Byn.Awrtc.Unity;
/// <summary>
/// 视频通话测试
/// </summary>
public class CallTest : MonoBehaviour
{
[SerializeField]
private InputField roomNameInput;
[SerializeField]
private InputField messageInput;
[SerializeField]
private Text msgText;
[SerializeField]
private Text logText;
[SerializeField]
private RawImage localVideoImage;
private Texture2D localTexture;
[SerializeField]
private RawImage remoteVideoImage;
private Texture2D remoteTexture;
void Start()
{
//请求权限
StartCoroutine(WebRTCHelper.RequestPermissions());
//初始化
WebRTCHelper.Init(false, true);
WebRTCHelper.OnTextHandler = OnTextHandler;
WebRTCHelper.OnLogHandler = OnLogHandler;
WebRTCHelper.OnVideoFrameUpdate = OnVideoFrameUpdate;
}
private void OnDestroy()
{
WebRTCHelper.Dispose();
}
void Update()
{
WebRTCHelper.CallUpdate();
}
private void FixedUpdate()
{
WebRTCHelper.HandleNetwork();
}
public void OnClickOpenChatRoom()
{
WebRTCHelper.OpenChatRoom(roomNameInput.text);
}
public void OnClickJoinChatRoom()
{
WebRTCHelper.JoinChatRoom(roomNameInput.text);
}
public void OnClickOpenCallRoom()
{
WebRTCHelper.OpenCallRoom(roomNameInput.text);
}
public void OnClickJoinCallRoom()
{
WebRTCHelper.JoinCallRoom(roomNameInput.text);
}
public void OnClickSend()
{
WebRTCHelper.SendString(messageInput.text);
}
public void OnClickHungup()
{
WebRTCHelper.CleanupCall();
}
//处理收到的文本消息
private void OnTextHandler(string msg)
{
msgText.text = msg;
}
//处理收到的错误消息
private void OnLogHandler(string error)
{
logText.text = error;
}
//处理收到的视频帧
private void OnVideoFrameUpdate(FrameUpdateEventArgs e)
{
if (e.IsRemote)
{
UpdateRemoteTexture(e.Frame);
}
else
{
UpdateLocalTexture(e.Frame);
}
}
//更新本地视频画面
private void UpdateLocalTexture(IFrame frame)
{
if (frame == null)
return;
//UnityMediaHelper.UpdateRawImage(localVideoImage, frame);
if (localTexture == null)
localTexture = new Texture2D(frame.Width, frame.Height, TextureFormat.RGBA32, false);
UnityMediaHelper.UpdateTexture(frame, ref localTexture);
if (localVideoImage.texture == null)
localVideoImage.texture = localTexture;
}
//更新远程视频画面
private void UpdateRemoteTexture(IFrame frame)
{
if (frame == null)
return;
//UnityMediaHelper.UpdateRawImage(remoteVideoImage, frame);
if (remoteTexture == null)
remoteTexture = new Texture2D(frame.Width, frame.Height, TextureFormat.RGBA32, false);
UnityMediaHelper.UpdateTexture(frame, ref remoteTexture);
if (remoteVideoImage.texture == null)
remoteVideoImage.texture = remoteTexture;
}
}
标签: Unity3d
日历
最新文章
随机文章
热门文章
分类
存档
- 2025年11月(1)
- 2025年9月(3)
- 2025年7月(4)
- 2025年6月(5)
- 2025年5月(1)
- 2025年4月(5)
- 2025年3月(4)
- 2025年2月(3)
- 2025年1月(1)
- 2024年12月(5)
- 2024年11月(5)
- 2024年10月(5)
- 2024年9月(3)
- 2024年8月(3)
- 2024年7月(11)
- 2024年6月(3)
- 2024年5月(9)
- 2024年4月(10)
- 2024年3月(11)
- 2024年2月(24)
- 2024年1月(12)
- 2023年12月(3)
- 2023年11月(9)
- 2023年10月(7)
- 2023年9月(2)
- 2023年8月(7)
- 2023年7月(9)
- 2023年6月(6)
- 2023年5月(7)
- 2023年4月(11)
- 2023年3月(6)
- 2023年2月(11)
- 2023年1月(8)
- 2022年12月(2)
- 2022年11月(4)
- 2022年10月(10)
- 2022年9月(2)
- 2022年8月(13)
- 2022年7月(7)
- 2022年6月(11)
- 2022年5月(18)
- 2022年4月(29)
- 2022年3月(5)
- 2022年2月(6)
- 2022年1月(8)
- 2021年12月(5)
- 2021年11月(3)
- 2021年10月(4)
- 2021年9月(9)
- 2021年8月(14)
- 2021年7月(8)
- 2021年6月(5)
- 2021年5月(2)
- 2021年4月(3)
- 2021年3月(7)
- 2021年2月(2)
- 2021年1月(8)
- 2020年12月(7)
- 2020年11月(2)
- 2020年10月(6)
- 2020年9月(9)
- 2020年8月(10)
- 2020年7月(9)
- 2020年6月(18)
- 2020年5月(4)
- 2020年4月(25)
- 2020年3月(38)
- 2020年1月(21)
- 2019年12月(13)
- 2019年11月(29)
- 2019年10月(44)
- 2019年9月(17)
- 2019年8月(18)
- 2019年7月(25)
- 2019年6月(25)
- 2019年5月(17)
- 2019年4月(10)
- 2019年3月(36)
- 2019年2月(35)
- 2019年1月(28)
- 2018年12月(30)
- 2018年11月(22)
- 2018年10月(4)
- 2018年9月(7)
- 2018年8月(13)
- 2018年7月(13)
- 2018年6月(6)
- 2018年5月(5)
- 2018年4月(13)
- 2018年3月(5)
- 2018年2月(3)
- 2018年1月(8)
- 2017年12月(35)
- 2017年11月(17)
- 2017年10月(16)
- 2017年9月(17)
- 2017年8月(20)
- 2017年7月(34)
- 2017年6月(17)
- 2017年5月(15)
- 2017年4月(32)
- 2017年3月(8)
- 2017年2月(2)
- 2017年1月(5)
- 2016年12月(14)
- 2016年11月(26)
- 2016年10月(12)
- 2016年9月(25)
- 2016年8月(32)
- 2016年7月(14)
- 2016年6月(21)
- 2016年5月(17)
- 2016年4月(13)
- 2016年3月(8)
- 2016年2月(8)
- 2016年1月(18)
- 2015年12月(13)
- 2015年11月(15)
- 2015年10月(12)
- 2015年9月(18)
- 2015年8月(21)
- 2015年7月(35)
- 2015年6月(13)
- 2015年5月(9)
- 2015年4月(4)
- 2015年3月(5)
- 2015年2月(4)
- 2015年1月(13)
- 2014年12月(7)
- 2014年11月(5)
- 2014年10月(4)
- 2014年9月(8)
- 2014年8月(16)
- 2014年7月(26)
- 2014年6月(22)
- 2014年5月(28)
- 2014年4月(15)
友情链接
- Unity官网
- Unity圣典
- Unity在线手册
- Unity中文手册(圣典)
- Unity官方中文论坛
- Unity游戏蛮牛用户文档
- Unity下载存档
- Unity引擎源码下载
- Unity服务
- Unity Ads
- wiki.unity3d
- Visual Studio Code官网
- SenseAR开发文档
- MSDN
- C# 参考
- C# 编程指南
- .NET Framework类库
- .NET 文档
- .NET 开发
- WPF官方文档
- uLua
- xLua
- SharpZipLib
- Protobuf-net
- Protobuf.js
- OpenSSL
- OPEN CASCADE
- JSON
- MessagePack
- C在线工具
- 游戏蛮牛
- GreenVPN
- 聚合数据
- 热云
- 融云
- 腾讯云
- 腾讯开放平台
- 腾讯游戏服务
- 腾讯游戏开发者平台
- 腾讯课堂
- 微信开放平台
- 腾讯实时音视频
- 腾讯即时通信IM
- 微信公众平台技术文档
- 白鹭引擎官网
- 白鹭引擎开放平台
- 白鹭引擎开发文档
- FairyGUI编辑器
- PureMVC-TypeScript
- 讯飞开放平台
- 亲加通讯云
- Cygwin
- Mono开发者联盟
- Scut游戏服务器引擎
- KBEngine游戏服务器引擎
- Photon游戏服务器引擎
- 码云
- SharpSvn
- 腾讯bugly
- 4399原创平台
- 开源中国
- Firebase
- Firebase-Admob-Unity
- google-services-unity
- Firebase SDK for Unity
- Google-Firebase-SDK
- AppsFlyer SDK
- android-repository
- CQASO
- Facebook开发者平台
- gradle下载
- GradleBuildTool下载
- Android Developers
- Google中国开发者
- AndroidDevTools
- Android社区
- Android开发工具
- Google Play Games Services
- Google商店
- Google APIs for Android
- 金钱豹VPN
- TouchSense SDK
- MakeHuman
- Online RSA Key Converter
- Windows UWP应用
- Visual Studio For Unity
- Open CASCADE Technology
- 慕课网
- 阿里云服务器ECS
- 在线免费文字转语音系统
- AI Studio
- 网云穿
- 百度网盘开放平台
- 迅捷画图
- 菜鸟工具
- [CSDN] 程序员研修院
- 华为人脸识别
- 百度AR导航导览SDK
- 海康威视官网
- 海康开放平台
- 海康SDK下载
- git download
- Open CASCADE
- CascadeStudio
交流QQ群
-
Flash游戏设计: 86184192
Unity游戏设计: 171855449
游戏设计订阅号







