diff --git a/Assets/Plugins/Android/AndroidManifest.xml b/Assets/Plugins/Android/AndroidManifest.xml
index 0d9fdae..bfc8280 100644
--- a/Assets/Plugins/Android/AndroidManifest.xml
+++ b/Assets/Plugins/Android/AndroidManifest.xml
@@ -26,4 +26,7 @@
+
+
+
diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity
index 97794a3..fbfcb6c 100644
--- a/Assets/Scenes/SampleScene.unity
+++ b/Assets/Scenes/SampleScene.unity
@@ -122,6 +122,134 @@ NavMeshSettings:
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
+--- !u!1 &314561960
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 314561961}
+ - component: {fileID: 314561962}
+ m_Layer: 0
+ m_Name: microphoneAudioSource
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &314561961
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 314561960}
+ serializedVersion: 2
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 1915034403}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!82 &314561962
+AudioSource:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 314561960}
+ m_Enabled: 1
+ serializedVersion: 4
+ OutputAudioMixerGroup: {fileID: 0}
+ m_audioClip: {fileID: 0}
+ m_PlayOnAwake: 1
+ m_Volume: 1
+ m_Pitch: 1
+ Loop: 0
+ Mute: 0
+ Spatialize: 0
+ SpatializePostEffects: 0
+ Priority: 128
+ DopplerLevel: 1
+ MinDistance: 1
+ MaxDistance: 500
+ Pan2D: 0
+ rolloffMode: 0
+ BypassEffects: 0
+ BypassListenerEffects: 0
+ BypassReverbZones: 0
+ rolloffCustomCurve:
+ serializedVersion: 2
+ m_Curve:
+ - serializedVersion: 3
+ time: 0
+ value: 1
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0.33333334
+ outWeight: 0.33333334
+ - serializedVersion: 3
+ time: 1
+ value: 0
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0.33333334
+ outWeight: 0.33333334
+ m_PreInfinity: 2
+ m_PostInfinity: 2
+ m_RotationOrder: 4
+ panLevelCustomCurve:
+ serializedVersion: 2
+ m_Curve:
+ - serializedVersion: 3
+ time: 0
+ value: 0
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0.33333334
+ outWeight: 0.33333334
+ m_PreInfinity: 2
+ m_PostInfinity: 2
+ m_RotationOrder: 4
+ spreadCustomCurve:
+ serializedVersion: 2
+ m_Curve:
+ - serializedVersion: 3
+ time: 0
+ value: 0
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0.33333334
+ outWeight: 0.33333334
+ m_PreInfinity: 2
+ m_PostInfinity: 2
+ m_RotationOrder: 4
+ reverbZoneMixCustomCurve:
+ serializedVersion: 2
+ m_Curve:
+ - serializedVersion: 3
+ time: 0
+ value: 1
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0.33333334
+ outWeight: 0.33333334
+ m_PreInfinity: 2
+ m_PostInfinity: 2
+ m_RotationOrder: 4
--- !u!224 &322971333 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 2618240917987615803, guid: e0d7d0986111c73499b1c0e092bc092a,
@@ -579,6 +707,12 @@ MonoBehaviour:
m_MipBias: 0
m_VarianceClampScale: 0.9
m_ContrastAdaptiveSharpening: 0
+--- !u!81 &1595310059 stripped
+AudioListener:
+ m_CorrespondingSourceObject: {fileID: 875232991587860349, guid: 779fa6087d61b4c4abc175a2d5d948f5,
+ type: 3}
+ m_PrefabInstance: {fileID: 1511841562}
+ m_PrefabAsset: {fileID: 0}
--- !u!1 &1915034400
GameObject:
m_ObjectHideFlags: 0
@@ -644,7 +778,8 @@ RectTransform:
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
- m_Children: []
+ m_Children:
+ - {fileID: 314561961}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
@@ -664,20 +799,20 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8bcc82901c3f48a88d3408251afa3365, type: 3}
m_Name:
m_EditorClassIdentifier:
- m_TextureSize: {x: 1920, y: 1200}
+ m_TextureSize: {x: 2560, y: 1440}
m_Source: 3
m_Camera: {fileID: 0}
- m_Texture: {fileID: 8400000, guid: dc38ccb1cb998bf4e8b83a3bbf7b46ad, type: 2}
- m_WebCamDeviceIndex: 1
+ m_Texture: {fileID: 0}
+ m_WebCamDeviceIndex: 0
m_Depth: 16
m_AntiAliasing: 1
m_Codec:
- m_MimeType: video/H264
- m_SdpFmtpLine: implementation_name=NvCodec;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=420033
- m_FrameRate: 30
+ m_MimeType: video/VP9
+ m_SdpFmtpLine: implementation_name=Internal;profile-id=0
+ m_FrameRate: 60
m_Bitrate:
- min: 0
- max: 1000
+ min: 4000
+ max: 8000
m_ScaleFactor: 1
m_AutoRequestUserAuthorization: 1
--- !u!114 &1915034405
@@ -731,7 +866,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Source: 2
- m_AudioListener: {fileID: 0}
+ m_AudioListener: {fileID: 1595310059}
m_AudioSource: {fileID: 0}
m_MicrophoneDeviceIndex: 0
m_AutoRequestUserAuthorization: 1
@@ -741,8 +876,8 @@ MonoBehaviour:
m_ChannelCount: 0
m_SampleRate: 0
m_Bitrate:
- min: 8
- max: 208
+ min: 247
+ max: 1000
m_Loopback: 0
--- !u!1660057539 &9223372036854775807
SceneRoots:
diff --git a/Assets/Script/GlobalConfigSystem.cs b/Assets/Script/GlobalConfigSystem.cs
index a292ff4..90a8071 100644
--- a/Assets/Script/GlobalConfigSystem.cs
+++ b/Assets/Script/GlobalConfigSystem.cs
@@ -31,6 +31,9 @@ public interface IGlobalConfigSystem : ISystem
public string GetUserId();
public void SetUserId(string userId);
+
+ public string GetParticipantId();
+ public void SetParticipantId(string participantId);
}
public class GlobalConfigSystem : AbstractSystem, IGlobalConfigSystem
@@ -65,6 +68,8 @@ public class GlobalConfigSystem : AbstractSystem, IGlobalConfigSystem
///
private int _connectionTimeType;
+ private string _participantId;
+
private CancellationTokenSource _cts;
@@ -211,6 +216,16 @@ public class GlobalConfigSystem : AbstractSystem, IGlobalConfigSystem
_connectionAvatar = connectionTexture;
}
+ public string GetParticipantId()
+ {
+ return _participantId;
+ }
+
+ public void SetParticipantId(string participantId)
+ {
+ _participantId = participantId;
+ }
+
protected override void OnInit()
{
}
diff --git a/Assets/Script/MainPanel/MainPanel.cs b/Assets/Script/MainPanel/MainPanel.cs
index 5bce6f9..1266aa0 100644
--- a/Assets/Script/MainPanel/MainPanel.cs
+++ b/Assets/Script/MainPanel/MainPanel.cs
@@ -128,6 +128,11 @@ namespace Script
await WebRTCUtil.DownloadAndSetAvatar(obj[i].avatar,
entry.transform.Find("image").GetComponent());
_userMap.TryAdd(obj[i], entry);
+
+ if (obj[i].role == "host")
+ {
+ this.GetSystem().SetParticipantId(obj[i].participantId);
+ }
}
// 更新会议聊天面板人数
diff --git a/Assets/Script/MainPanel/MeetingInfoListController.cs b/Assets/Script/MainPanel/MeetingInfoListController.cs
index 36b92da..dbe6ab7 100644
--- a/Assets/Script/MainPanel/MeetingInfoListController.cs
+++ b/Assets/Script/MainPanel/MeetingInfoListController.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using RenderStreaming;
+using Script.Recorder;
using Script.Util;
using Stary.Evo;
using Stary.Evo.UIFarme;
@@ -58,7 +59,7 @@ namespace Script
_audioStreamSender = GameObject.Find("RenderStreaming").GetComponent();
_videoStreamSender = GameObject.Find("RenderStreaming").GetComponent();
#if UNITY_EDITOR
- _recorder = new EditorGameViewRecorder();
+ _recorder = new ServerMixedRecorder();
#elif UNITY_ANDROID
_recorder = new XrealMixedRecorder();
#else
diff --git a/Assets/Script/Recorder/EditorServerRecorder.cs b/Assets/Script/Recorder/EditorServerRecorder.cs
new file mode 100644
index 0000000..ed2f007
--- /dev/null
+++ b/Assets/Script/Recorder/EditorServerRecorder.cs
@@ -0,0 +1,260 @@
+using System;
+using System.Collections.Generic;
+using Cysharp.Threading.Tasks;
+using Newtonsoft.Json;
+using RenderStreaming;
+using Stary.Evo;
+using Unity.RenderStreaming;
+using Unity.WebRTC;
+using UnityEngine;
+
+namespace Script.Recorder
+{
+ public class EditorServerRecorder : IVideoRecorder, IController
+ {
+ public bool IsRecording { get; }
+ public Action OnStartedRecordingVideo { get; set; }
+ public Action OnStoppedRecordingVideoAction { get; set; }
+
+ private string recordingId;
+
+ private readonly Dictionary recordingPeers = new();
+
+ public WebcamToRenderTexture sourceRenderTexture;
+ public string microphoneDeviceName = ""; // 留空使用默认麦克风
+ public int microphoneSampleRate = 48000;
+
+ private AudioSource microphoneAudioSource;
+ private bool microphoneStarted;
+
+ private VideoStreamTrack videoTrack;
+ private AudioStreamTrack audioTrack;
+
+ public async void StartRecording()
+ {
+ await CreateLocalTracks();
+
+ var messageChannel = GameObject.FindObjectOfType();
+ messageChannel.OnRecordingPeerRequestReceived += OnRecordingPeerRequestReceived;
+ messageChannel.OnRecordingAnswerReceived += OnRecordingAnswerReceived;
+ messageChannel.OnRecordingCandidateReceived += OnRecordingCandidateReceived;
+ messageChannel.OnRecordingStoppedReceived += OnRecordingStoppedReceived;
+
+ var data = new
+ {
+ connectionId = this.GetSystem().GetConnectionId(),
+ layout = "grid",
+ format = "webm",
+ };
+
+ var result = await WebRequestSystem.Post(
+ this.GetSystem().IP + "/api/recording-sessions",
+ JsonConvert.SerializeObject(data));
+ if (result != null)
+ {
+ RecordingSession session = JsonConvert.DeserializeObject(result);
+ if (string.IsNullOrEmpty(session.success))
+ {
+ Debug.LogError($"[ServerMixedRecorder] StartRecording 失败: {session}");
+ return;
+ }
+
+ recordingId = session.session.id;
+ }
+
+ }
+
+
+ public async UniTask CreateLocalTracks()
+ {
+ if (sourceRenderTexture == null)
+ {
+ sourceRenderTexture =new GameObject("WebcamToRenderTexture").AddComponent();
+ videoTrack = new VideoStreamTrack(sourceRenderTexture.renderTexture);
+ }
+
+ microphoneAudioSource =
+ GameObject.Find("RenderStreaming/microphoneAudioSource").GetComponent();
+ microphoneAudioSource.loop = true;
+ microphoneAudioSource.mute = true;
+
+ string device = string.IsNullOrEmpty(microphoneDeviceName)
+ ? null
+ : microphoneDeviceName;
+
+ microphoneAudioSource.clip = Microphone.Start(
+ device,
+ true,
+ 1,
+ microphoneSampleRate
+ );
+
+ microphoneStarted = true;
+
+ await CreateMicrophoneTrackWhenReady(device);
+ }
+
+ public async UniTask CreateMicrophoneTrackWhenReady(string device)
+ {
+ while (Microphone.GetPosition(device) <= 0)
+ await UniTask.NextFrame();
+
+ microphoneAudioSource.Play();
+ audioTrack = new AudioStreamTrack(microphoneAudioSource);
+ }
+
+
+ public async void OnRecordingPeerRequestReceived(RecordingRequest request)
+ {
+ if (string.IsNullOrEmpty(request.recordingId))
+ return;
+
+ OnRecordingStoppedReceived(request.recordingId);
+
+ var config = new RTCConfiguration
+ {
+ iceServers = new[]
+ {
+ new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } }
+ }
+ };
+
+ var pc = new RTCPeerConnection(ref config);
+ recordingPeers[request.recordingId] = pc;
+
+ pc.OnIceCandidate = candidate =>
+ {
+ if (candidate == null) return;
+
+ Send("recording-candidate", new RecordingCandidate
+ {
+ recordingId = request.recordingId,
+ connectionId = request.connectionId,
+ participantId = this.GetSystem().GetParticipantId(),
+ candidate = candidate.Candidate,
+ sdpMid = candidate.SdpMid,
+ sdpMLineIndex = candidate.SdpMLineIndex ?? 0
+ });
+ };
+
+ if (videoTrack != null)
+ pc.AddTransceiver(videoTrack,
+ new RTCRtpTransceiverInit { direction = RTCRtpTransceiverDirection.SendOnly });
+
+ if (audioTrack != null)
+ pc.AddTransceiver(audioTrack,
+ new RTCRtpTransceiverInit { direction = RTCRtpTransceiverDirection.SendOnly });
+
+ var offerOp = pc.CreateOffer();
+ await offerOp;
+
+ var offer = offerOp.Desc;
+ var localOp = pc.SetLocalDescription(ref offer);
+ await localOp;
+
+ Send("recording-offer", new RecordingOffer
+ {
+ recordingId = request.recordingId,
+ connectionId = request.connectionId,
+ participantId = this.GetSystem().GetParticipantId(),
+ sdp = offer.sdp
+ });
+ }
+
+ public async void OnRecordingAnswerReceived(RecordingAnswer answer)
+ {
+ if (!recordingPeers.TryGetValue(answer.recordingId, out var pc))
+ return;
+
+ var desc = new RTCSessionDescription
+ {
+ type = RTCSdpType.Answer,
+ sdp = answer.sdp
+ };
+
+ var op = pc.SetRemoteDescription(ref desc);
+ await op;
+ }
+
+ public void OnRecordingCandidateReceived(RecordingCandidate data)
+ {
+ if (!recordingPeers.TryGetValue(data.recordingId, out var pc))
+ return;
+
+ var candidate = new RTCIceCandidate(new RTCIceCandidateInit
+ {
+ candidate = data.candidate,
+ sdpMid = data.sdpMid,
+ sdpMLineIndex = data.sdpMLineIndex
+ });
+
+ pc.AddIceCandidate(candidate);
+ }
+
+ public void OnRecordingStoppedReceived(string recordingId)
+ {
+ if (!recordingPeers.TryGetValue(recordingId, out var pc))
+ return;
+
+ pc.Close();
+ recordingPeers.Remove(recordingId);
+ }
+
+ void Send(string type, object data)
+ {
+ var connectionId = this.GetSystem().GetConnectionId();
+
+ var json = JsonConvert.SerializeObject(new Dictionary
+ {
+ ["type"] = "on-message",
+ ["data"] = new Dictionary
+ {
+ ["connectionId"] = connectionId,
+ ["message"] = new Dictionary
+ {
+ ["type"] = type,
+ ["data"] = data
+ }
+ }
+ });
+
+ SignalingMessageHelper.SendMessage(json);
+ }
+
+ public async void StopRecording()
+ {
+ await WebRequestSystem.Delete(this.GetSystem().IP,
+ $"/api/recording-sessions/{recordingId}");
+ var messageChannel = GameObject.FindObjectOfType();
+ messageChannel.OnRecordingPeerRequestReceived -= OnRecordingPeerRequestReceived;
+ messageChannel.OnRecordingAnswerReceived -= OnRecordingAnswerReceived;
+ messageChannel.OnRecordingCandidateReceived -= OnRecordingCandidateReceived;
+ messageChannel.OnRecordingStoppedReceived -= OnRecordingStoppedReceived;
+ }
+
+ void OnDestroy()
+ {
+ foreach (var pc in recordingPeers.Values)
+ pc.Close();
+
+ recordingPeers.Clear();
+
+ videoTrack?.Dispose();
+ audioTrack?.Dispose();
+
+ if (microphoneStarted)
+ {
+ string device = string.IsNullOrEmpty(microphoneDeviceName)
+ ? null
+ : microphoneDeviceName;
+
+ Microphone.End(device);
+ }
+ }
+
+ public IArchitecture GetArchitecture()
+ {
+ return MainArchitecture.Interface;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Script/Recorder/EditorServerRecorder.cs.meta b/Assets/Script/Recorder/EditorServerRecorder.cs.meta
new file mode 100644
index 0000000..608266f
--- /dev/null
+++ b/Assets/Script/Recorder/EditorServerRecorder.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 3583d180c5d94451bea6ead323d96021
+timeCreated: 1780402391
\ No newline at end of file
diff --git a/Assets/Script/Recorder/ServerMixedRecorder.cs b/Assets/Script/Recorder/ServerMixedRecorder.cs
new file mode 100644
index 0000000..b77ff8f
--- /dev/null
+++ b/Assets/Script/Recorder/ServerMixedRecorder.cs
@@ -0,0 +1,381 @@
+using System;
+using System.Collections.Generic;
+using Cysharp.Threading.Tasks;
+using Newtonsoft.Json;
+using RenderStreaming;
+using Stary.Evo;
+using Unity.RenderStreaming;
+using Unity.WebRTC;
+using UnityEngine;
+
+namespace Script.Recorder
+{
+ public class ServerMixedRecorder : IVideoRecorder, IController
+ {
+ public bool IsRecording { get; }
+ public Action OnStartedRecordingVideo { get; set; }
+ public Action OnStoppedRecordingVideoAction { get; set; }
+
+ private string recordingId;
+
+ private readonly Dictionary recordingPeers = new();
+
+
+ // [Header("Tracks")] public RenderTexture sourceRenderTexture;
+ // public string microphoneDeviceName = ""; // 留空使用默认麦克风
+ // public int microphoneSampleRate = 48000;
+ //
+ // private AudioSource microphoneAudioSource;
+ // private bool microphoneStarted;
+
+ private VideoStreamTrack videoTrack;
+ private AudioStreamTrack audioTrack;
+
+ public async void StartRecording()
+ {
+ await CreateLocalTracks();
+
+ var messageChannel = GameObject.FindObjectOfType();
+ messageChannel.OnRecordingPeerRequestReceived += OnRecordingPeerRequestReceived;
+ messageChannel.OnRecordingAnswerReceived += OnRecordingAnswerReceived;
+ messageChannel.OnRecordingCandidateReceived += OnRecordingCandidateReceived;
+ messageChannel.OnRecordingStoppedReceived += OnRecordingStoppedReceived;
+
+ var data = new
+ {
+ connectionId = this.GetSystem().GetConnectionId(),
+ layout = "grid",
+ format = "webm",
+ };
+
+ var result = await WebRequestSystem.Post(
+ this.GetSystem().IP + "/api/recording-sessions",
+ JsonConvert.SerializeObject(data));
+ if (result != null)
+ {
+ RecordingSession session = JsonConvert.DeserializeObject(result);
+ if (string.IsNullOrEmpty(session.success))
+ {
+ Debug.LogError($"[ServerMixedRecorder] StartRecording 失败: {session}");
+ return;
+ }
+
+ recordingId = session.session.id;
+ }
+
+ }
+
+
+ public async UniTask CreateLocalTracks()
+ {
+ var RenderStreaming =GameObject.Find("RenderStreaming");
+ VideoStreamSender videoStreamSender = RenderStreaming.GetComponent();
+ videoTrack = videoStreamSender.Track as VideoStreamTrack;
+
+
+ AudioStreamSender audioStreamSender = RenderStreaming.GetComponent();
+ audioTrack = audioStreamSender.Track as AudioStreamTrack;
+ // sourceRenderTexture = this.GetSystem().GetRenderStreamingTexture();
+ // if (sourceRenderTexture != null)
+ // {
+ // videoTrack = new VideoStreamTrack(sourceRenderTexture);
+ // }
+ //
+ //
+ // microphoneAudioSource =
+ // GameObject.Find("RenderStreaming/microphoneAudioSource").GetComponent();
+ // microphoneAudioSource.loop = true;
+ // microphoneAudioSource.mute = true;
+ //
+ // string device = string.IsNullOrEmpty(microphoneDeviceName)
+ // ? null
+ // : microphoneDeviceName;
+ //
+ // microphoneAudioSource.clip = Microphone.Start(
+ // device,
+ // true,
+ // 1,
+ // microphoneSampleRate
+ // );
+ //
+ // microphoneStarted = true;
+
+ //await CreateMicrophoneTrackWhenReady(device);
+ }
+
+ // public async UniTask CreateMicrophoneTrackWhenReady(string device)
+ // {
+ // while (Microphone.GetPosition(device) <= 0)
+ // await UniTask.NextFrame();
+ //
+ // microphoneAudioSource.Play();
+ // audioTrack = new AudioStreamTrack(microphoneAudioSource);
+ // }
+
+
+ public async void OnRecordingPeerRequestReceived(RecordingRequest request)
+ {
+ if (string.IsNullOrEmpty(request.recordingId))
+ return;
+
+ OnRecordingStoppedReceived(request.recordingId);
+
+ var config = new RTCConfiguration
+ {
+ iceServers = new[]
+ {
+ new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } }
+ }
+ };
+
+ var pc = new RTCPeerConnection(ref config);
+ recordingPeers[request.recordingId] = pc;
+
+ pc.OnIceCandidate = candidate =>
+ {
+ if (candidate == null) return;
+
+ Send("recording-candidate", new RecordingCandidate
+ {
+ recordingId = request.recordingId,
+ connectionId = request.connectionId,
+ participantId = this.GetSystem().GetParticipantId(),
+ candidate = candidate.Candidate,
+ sdpMid = candidate.SdpMid,
+ sdpMLineIndex = candidate.SdpMLineIndex ?? 0
+ });
+ };
+
+ if (videoTrack != null)
+ pc.AddTransceiver(videoTrack,
+ new RTCRtpTransceiverInit { direction = RTCRtpTransceiverDirection.SendOnly });
+
+ if (audioTrack != null)
+ pc.AddTransceiver(audioTrack,
+ new RTCRtpTransceiverInit { direction = RTCRtpTransceiverDirection.SendOnly });
+
+ var offerOp = pc.CreateOffer();
+ await offerOp;
+
+ var offer = offerOp.Desc;
+ var localOp = pc.SetLocalDescription(ref offer);
+ await localOp;
+
+ Send("recording-offer", new RecordingOffer
+ {
+ recordingId = request.recordingId,
+ connectionId = request.connectionId,
+ participantId = this.GetSystem().GetParticipantId(),
+ sdp = offer.sdp
+ });
+ }
+
+ public async void OnRecordingAnswerReceived(RecordingAnswer answer)
+ {
+ if (!recordingPeers.TryGetValue(answer.recordingId, out var pc))
+ return;
+
+ var desc = new RTCSessionDescription
+ {
+ type = RTCSdpType.Answer,
+ sdp = answer.sdp
+ };
+
+ var op = pc.SetRemoteDescription(ref desc);
+ await op;
+ }
+
+ public void OnRecordingCandidateReceived(RecordingCandidate data)
+ {
+ if (!recordingPeers.TryGetValue(data.recordingId, out var pc))
+ return;
+
+ var candidate = new RTCIceCandidate(new RTCIceCandidateInit
+ {
+ candidate = data.candidate,
+ sdpMid = data.sdpMid,
+ sdpMLineIndex = data.sdpMLineIndex
+ });
+
+ pc.AddIceCandidate(candidate);
+ }
+
+ public void OnRecordingStoppedReceived(string recordingId)
+ {
+ if (!recordingPeers.TryGetValue(recordingId, out var pc))
+ return;
+
+ pc.Close();
+ recordingPeers.Remove(recordingId);
+ }
+
+ void Send(string type, object data)
+ {
+ var connectionId = this.GetSystem().GetConnectionId();
+
+ var json = JsonConvert.SerializeObject(new Dictionary
+ {
+ ["type"] = "on-message",
+ ["data"] = new Dictionary
+ {
+ ["connectionId"] = connectionId,
+ ["message"] = new Dictionary
+ {
+ ["type"] = type,
+ ["data"] = data
+ }
+ }
+ });
+
+ SignalingMessageHelper.SendMessage(json);
+ }
+
+ public async void StopRecording()
+ {
+ await WebRequestSystem.Delete(this.GetSystem().IP,
+ $"/api/recording-sessions/{recordingId}");
+ var messageChannel = GameObject.FindObjectOfType();
+ messageChannel.OnRecordingPeerRequestReceived -= OnRecordingPeerRequestReceived;
+ messageChannel.OnRecordingAnswerReceived -= OnRecordingAnswerReceived;
+ messageChannel.OnRecordingCandidateReceived -= OnRecordingCandidateReceived;
+ messageChannel.OnRecordingStoppedReceived -= OnRecordingStoppedReceived;
+ }
+
+ void OnDestroy()
+ {
+ foreach (var pc in recordingPeers.Values)
+ pc.Close();
+
+ recordingPeers.Clear();
+
+ videoTrack?.Dispose();
+ audioTrack?.Dispose();
+
+ // if (microphoneStarted)
+ // {
+ // string device = string.IsNullOrEmpty(microphoneDeviceName)
+ // ? null
+ // : microphoneDeviceName;
+ //
+ // Microphone.End(device);
+ // }
+ }
+
+ public IArchitecture GetArchitecture()
+ {
+ return MainArchitecture.Interface;
+ }
+ }
+
+ [Serializable]
+ public class Session
+ {
+ ///
+ ///
+ ///
+ public string id { get; set; }
+
+ ///
+ ///
+ ///
+ public string connectionId { get; set; }
+
+ ///
+ ///
+ ///
+ public string status { get; set; }
+
+ ///
+ ///
+ ///
+ public string layout { get; set; }
+
+ ///
+ ///
+ ///
+ public string format { get; set; }
+
+ ///
+ ///
+ ///
+ public string createdAt { get; set; }
+
+ ///
+ ///
+ ///
+ public string startedAt { get; set; }
+
+ ///
+ ///
+ ///
+ public string updatedAt { get; set; }
+ }
+
+ [Serializable]
+ public class Agent
+ {
+ ///
+ ///
+ ///
+ public string id { get; set; }
+
+ ///
+ ///
+ ///
+ public string recordingId { get; set; }
+
+ ///
+ ///
+ ///
+ public string connectionId { get; set; }
+
+ ///
+ ///
+ ///
+ public string status { get; set; }
+
+ ///
+ ///
+ ///
+ public string mediaMode { get; set; }
+
+ ///
+ ///
+ ///
+ public string createdAt { get; set; }
+
+ ///
+ ///
+ ///
+ public string updatedAt { get; set; }
+ }
+
+ [Serializable]
+ public class RecordingSession
+ {
+ ///
+ ///
+ ///
+ public string success { get; set; }
+
+ ///
+ ///
+ ///
+ public Session session { get; set; }
+
+ ///
+ ///
+ ///
+ public Agent agent { get; set; }
+
+ ///
+ ///
+ ///
+ public string notified { get; set; }
+
+ ///
+ ///
+ ///
+ public string peerRequestNotified { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Script/Recorder/ServerMixedRecorder.cs.meta b/Assets/Script/Recorder/ServerMixedRecorder.cs.meta
new file mode 100644
index 0000000..5427e91
--- /dev/null
+++ b/Assets/Script/Recorder/ServerMixedRecorder.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d9d49eabfbbd4b4d8b4b88f082441596
+timeCreated: 1780371636
\ No newline at end of file
diff --git a/Assets/Script/Recorder/WebcamToRenderTexture.cs b/Assets/Script/Recorder/WebcamToRenderTexture.cs
new file mode 100644
index 0000000..2105ec5
--- /dev/null
+++ b/Assets/Script/Recorder/WebcamToRenderTexture.cs
@@ -0,0 +1,47 @@
+using Unity.WebRTC;
+using UnityEngine;
+
+public class WebcamToRenderTexture : MonoBehaviour
+{
+ public RenderTexture renderTexture;
+
+ private WebCamTexture webcamTexture;
+
+ void Awake()
+ {
+ if (renderTexture == null)
+ {
+ RenderTextureFormat supportedFormat = WebRTC.GetSupportedRenderTextureFormat(SystemInfo.graphicsDeviceType);
+
+ // 创建新的RenderTexture
+ renderTexture = new RenderTexture(1920, 1200, 0, supportedFormat);
+ renderTexture.enableRandomWrite = true;
+ renderTexture.Create();
+ //_textureImage.texture = localRenderTexture;
+ }
+
+ webcamTexture = new WebCamTexture();
+ webcamTexture.Play();
+ }
+
+ void Update()
+ {
+ if (webcamTexture != null && webcamTexture.didUpdateThisFrame)
+ {
+ Graphics.Blit(webcamTexture, renderTexture);
+ }
+ }
+
+ void OnDestroy()
+ {
+ if (webcamTexture != null)
+ {
+ webcamTexture.Stop();
+ }
+
+ if (renderTexture != null)
+ {
+ renderTexture.Release();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Script/Recorder/WebcamToRenderTexture.cs.meta b/Assets/Script/Recorder/WebcamToRenderTexture.cs.meta
new file mode 100644
index 0000000..d4571a2
--- /dev/null
+++ b/Assets/Script/Recorder/WebcamToRenderTexture.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 63353e4a29ff45c9803dae86c9590d72
+timeCreated: 1780403919
\ No newline at end of file
diff --git a/Assets/Script/Recorder/XrealMixedRecorder.cs b/Assets/Script/Recorder/XrealMixedRecorder.cs
index 08ecadc..a0a572f 100644
--- a/Assets/Script/Recorder/XrealMixedRecorder.cs
+++ b/Assets/Script/Recorder/XrealMixedRecorder.cs
@@ -14,7 +14,7 @@ using CameraType = Unity.XR.XREAL.CameraType;
using GalleryDataProvider = Unity.XR.XREAL.MockGalleryDataProvider;
#endif
-public class XrealMixedRecorder : IVideoRecorder, IController
+public class XrealMixedRecorder : IVideoRecorder, IController , IDisposable
{
public enum ResolutionLevel
{
@@ -25,7 +25,7 @@ public class XrealMixedRecorder : IVideoRecorder, IController
public ResolutionLevel resolutionLevel = ResolutionLevel.High;
public BlendMode blendMode = BlendMode.Blend;
- public AudioState audioState = AudioState.MicAudio;
+ public AudioState audioState = AudioState.ApplicationAndMicAudio;
public CaptureSide captureside = CaptureSide.Single;
public bool useGreenBackGround = false;
@@ -82,7 +82,7 @@ public class XrealMixedRecorder : IVideoRecorder, IController
}
Debug.Log("Stop Video Capture!");
- _videoCapture.StopRecordingAsync(OnStoppedRecordingVideo);
+ _videoCapture.StopRecordingAsync(OnStoppedVideoCaptureMode);
}
/// Executes the 'stopped recording video' action.
@@ -103,6 +103,11 @@ public class XrealMixedRecorder : IVideoRecorder, IController
/// The result.
private async void OnStoppedVideoCaptureMode(XREALVideoCapture.VideoCaptureResult result)
{
+ if (!result.success)
+ {
+ Debug.Log("Stopped Recording Video Faild!");
+ return;
+ }
Debug.Log("Stopped Video Capture Mode!");
var encoder = _videoCapture.GetContext().GetEncoder() as VideoEncoder;
@@ -114,8 +119,6 @@ public class XrealMixedRecorder : IVideoRecorder, IController
await DelayInsertVideoToGallery(path, filename, "Record");
OnStoppedRecordingVideoAction?.Invoke(path);
// Release video capture resource.
- _videoCapture.Dispose();
- _videoCapture = null;
}
/// 延迟将视频插入相册,确保视频文件已完全写入
@@ -235,4 +238,19 @@ public class XrealMixedRecorder : IVideoRecorder, IController
{
return MainArchitecture.Interface;
}
+
+ public void Dispose()
+ {
+ _videoCapture.StopVideoModeAsync((result) =>
+ {
+ if (!result.success)
+ {
+ Debug.Log("Stopped Video Capture Mode faild!");
+ return;
+ }
+ _videoCapture?.Dispose();
+ _videoCapture = null;
+ });
+
+ }
}
\ No newline at end of file
diff --git a/Assets/Script/RenderStreamingSystem.cs b/Assets/Script/RenderStreamingSystem.cs
index 656124a..8ec9735 100644
--- a/Assets/Script/RenderStreamingSystem.cs
+++ b/Assets/Script/RenderStreamingSystem.cs
@@ -11,6 +11,8 @@ namespace Script
{
void SetUp();
void HangUp();
+
+ RenderTexture GetRenderStreamingTexture();
}
public class RenderStreamingSystem : AbstractSystem, IRenderStreamingSystem
@@ -232,6 +234,15 @@ namespace Script
Debug.Log($"[MultiParticipantHost] Participant UI removed: {participantId}");
}
+ public RenderTexture GetRenderStreamingTexture()
+ {
+ if (_yuvToRenderTexture.localRenderTexture == null)
+ {
+ Debug.LogError("RenderTexture 未初始化");
+ return null;
+ }
+ return _yuvToRenderTexture.localRenderTexture;
+ }
#endregion
diff --git a/Assets/Script/WebRequestSystem.cs b/Assets/Script/WebRequestSystem.cs
index 5274cdb..038d3ad 100644
--- a/Assets/Script/WebRequestSystem.cs
+++ b/Assets/Script/WebRequestSystem.cs
@@ -348,16 +348,13 @@ namespace Stary.Evo
/// 获取Token值的服务URL地址(很重要)
/// 传入请求的参数,此处参数为JOSN格式
///
- public static async Task Post(string url, string postData)
+ public static async Task Post(string url, string postData)
{
try
{
+ await GetCertificateData();
using var webRequest = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST);
-// #if UNITY_2021_3_OR_NEWER
-// using (UnityWebRequest webRequest = UnityWebRequest.PostWwwForm(url, postData)) //第二种写法此行注释
-// #else
-// using (UnityWebRequest webRequest = UnityWebRequest.PostWwwForm(url, postData)) //第二种写法此行注释
-// #endif
+
webRequest.downloadHandler = new DownloadHandlerBuffer();
var postBytes = Encoding.UTF8.GetBytes(postData);
webRequest.uploadHandler = new UploadHandlerRaw(postBytes);
@@ -367,6 +364,7 @@ namespace Stary.Evo
webRequest.disposeDownloadHandlerOnDispose = true;
webRequest.disposeCertificateHandlerOnDispose = true;
webRequest.timeout = 30;
+ webRequest.certificateHandler = new SelfSignedCertHandler(certificateData);
await webRequest.SendWebRequest();
webRequest.uploadHandler?.Dispose();
// 更新错误检查方式
@@ -374,28 +372,16 @@ namespace Stary.Evo
webRequest.result == UnityWebRequest.Result.ProtocolError)
{
Debug.LogError(webRequest.error);
- return new ResultMessageEntity
- {
- code = 5001,
- message = webRequest.error
- };
+ return webRequest.error;
}
- var resultMessageEntity =
- JsonConvert.DeserializeObject(webRequest.downloadHandler.text);
- if (resultMessageEntity.code != 200) Debug.LogError(resultMessageEntity.message);
-
- return resultMessageEntity;
+ return webRequest.downloadHandler.text;
}
catch (Exception e)
{
Debug.LogError("UnityEvo:WebRequestSystem.Post" + e.Message);
- return new ResultMessageEntity
- {
- code = 5001,
- message = e.Message
- };
+ return e.Message;
}
}
@@ -408,10 +394,11 @@ namespace Stary.Evo
/// 请求数据的URL地址
/// 请求数据的路径
///
- public static async Task Delete(string url, string path)
+ public static async Task Delete(string url, string path)
{
try
{
+ await GetCertificateData();
// 修复URL拼接
var fullUrl = url.TrimEnd('/') + "/" + path.TrimStart('/');
using var webRequest = new UnityWebRequest(fullUrl, UnityWebRequest.kHttpVerbDELETE);
@@ -420,6 +407,7 @@ namespace Stary.Evo
webRequest.SetRequestHeader("Authorization", authorization); // 修正请求头名称规范
webRequest.timeout = 20;
+ webRequest.certificateHandler = new SelfSignedCertHandler(certificateData);
await webRequest.SendWebRequest();
// 增强错误处理
@@ -431,36 +419,20 @@ namespace Stary.Evo
$"Response: {webRequest.downloadHandler.text}";
Debug.LogError(errorMsg);
- return new ResultMessageEntity
- {
- code = 5001,
- message = errorMsg
- };
+ return errorMsg;
}
// 修复空响应处理
var responseText = webRequest.downloadHandler.text;
- if (string.IsNullOrEmpty(responseText))
- return new ResultMessageEntity
- {
- code = 200,
- message = "删除成功"
- };
- var resultMessageEntity =
- JsonConvert.DeserializeObject(webRequest.downloadHandler.text);
- if (resultMessageEntity.code != 200) Debug.LogError(resultMessageEntity.message);
- return resultMessageEntity;
+
+ return responseText;
}
catch (Exception e)
{
Debug.LogError("UnityEvo:WebRequestSystem.Get" + e.Message);
- return new ResultMessageEntity
- {
- code = 5001,
- message = e.Message
- };
+ return e.Message;
}
}
@@ -568,6 +540,5 @@ namespace Stary.Evo
else
Debug.Log($"上传成功: {request.downloadHandler.text}");
}
-
}
}
\ No newline at end of file
diff --git a/Assets/Script/WebRtc/MessageChannel.cs b/Assets/Script/WebRtc/MessageChannel.cs
index f469058..20c1daa 100644
--- a/Assets/Script/WebRtc/MessageChannel.cs
+++ b/Assets/Script/WebRtc/MessageChannel.cs
@@ -32,6 +32,12 @@ namespace Unity.RenderStreaming
public event Action OnChatMessageReceived;
+ public event Action OnRecordingPeerRequestReceived;
+ public event Action OnRecordingAnswerReceived;
+ public event Action OnRecordingCandidateReceived;
+ public event Action OnRecordingStoppedReceived;
+
+
public override void OnMessage(string message)
{
try
@@ -59,6 +65,27 @@ namespace Unity.RenderStreaming
var mediaState = json.ToObject();
OnMediaStateChangeReceived?.Invoke(ConnectionId, mediaState);
break;
+
+ case MessageTypes.RecordingPeerRequest:
+ json = record.data as JObject;
+ var recordingPeerRequest = json.ToObject();
+ OnRecordingPeerRequestReceived?.Invoke(recordingPeerRequest);
+ break;
+ case MessageTypes.RecordingAnswer:
+ json = record.data as JObject;
+ var recordingAnswer = json.ToObject();
+ OnRecordingAnswerReceived?.Invoke(recordingAnswer);
+ break;
+ case MessageTypes.RecordingCandidate:
+ json = record.data as JObject;
+ var recordingCandidate = json.ToObject();
+ OnRecordingCandidateReceived?.Invoke(recordingCandidate);
+ break;
+ case MessageTypes.RecordingStopped:
+ json = record.data as JObject;
+ var recordingStopped = json.ToObject();
+ OnRecordingStoppedReceived?.Invoke(recordingStopped.recordingId);
+ break;
}
messageHistory.Add(record);
diff --git a/Assets/Script/WebRtc/MessageTypes.cs b/Assets/Script/WebRtc/MessageTypes.cs
index 99eca1a..c747d8b 100644
--- a/Assets/Script/WebRtc/MessageTypes.cs
+++ b/Assets/Script/WebRtc/MessageTypes.cs
@@ -13,6 +13,11 @@ namespace Unity.RenderStreaming
public const string UserInfo = "user-info";
public const string MediaStateChange = "media-state-changed";
public const string ParticipantsSync = "participants-sync";
+
+ public const string RecordingPeerRequest = "recording-peer-request";
+ public const string RecordingAnswer = "recording-answer";
+ public const string RecordingCandidate = "recording-candidate";
+ public const string RecordingStopped = "recording-stopped";
}
[Serializable]
@@ -54,4 +59,43 @@ namespace Unity.RenderStreaming
public int width;
public int height;
}
+
+
+ [Serializable]public class RecordingRequest
+ {
+ public string recordingId;
+ public string connectionId;
+ public string mediaMode;
+ }
+
+ [Serializable]public class RecordingOffer
+ {
+ public string recordingId;
+ public string connectionId;
+ public string participantId;
+ public string sdp;
+ }
+
+ [Serializable]public class RecordingAnswer
+ {
+ public string recordingId;
+ public string connectionId;
+ public string participantId;
+ public string sdp;
+ }
+
+ [Serializable]public class RecordingCandidate
+ {
+ public string recordingId;
+ public string connectionId;
+ public string participantId;
+ public string candidate;
+ public string sdpMid;
+ public int sdpMLineIndex;
+ }
+
+ [Serializable]public class RecordingStopped
+ {
+ public string recordingId;
+ }
}
\ No newline at end of file
diff --git a/Assets/Script/YUVToRenderTexture.cs b/Assets/Script/YUVToRenderTexture.cs
index 8969fac..450f648 100644
--- a/Assets/Script/YUVToRenderTexture.cs
+++ b/Assets/Script/YUVToRenderTexture.cs
@@ -1,6 +1,7 @@
using System;
using RenderStreaming;
using Stary.Evo;
+using Unity.RenderStreaming;
using Unity.XR.XREAL;
using UnityEngine;
using UnityEngine.UI;
@@ -19,12 +20,14 @@ namespace Script
//private RawImage _localVideoImage;
//private RawImage _textureImage;
+ private VideoStreamSender videoStreamSender;
private void Start()
{
//_localVideoImage= GameObject.Find("CanvasMain/RawImage").GetComponent();
//_textureImage= GameObject.Find("CanvasMain/RawImage1").GetComponent();
localVideoMaterial = Resources.Load("LocalRenderMaterial");
m_RGBCameraTexture = XREALRGBCameraTexture.CreateSingleton();
+ videoStreamSender=this.GetComponent();
Play();
}
@@ -52,8 +55,8 @@ namespace Script
return;
// 获取Y纹理的尺寸作为目标RenderTexture的尺寸
- int width = yuvTextures[0].width;
- int height = yuvTextures[0].height;
+ var width = videoStreamSender.width;
+ var height = videoStreamSender.height;
// if (_localVideoImage==null || _localVideoImage.rectTransform.sizeDelta.x != width ||
// _localVideoImage.rectTransform.sizeDelta.y != height)
// {
@@ -68,7 +71,7 @@ namespace Script
localRenderTexture.Release();
// 创建新的RenderTexture
- localRenderTexture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
+ localRenderTexture = new RenderTexture((int)width, (int)height, 0, RenderTextureFormat.ARGB32);
localRenderTexture.enableRandomWrite = true;
localRenderTexture.Create();
//_textureImage.texture = localRenderTexture;
diff --git a/Packages/com.unity.renderstreaming@3.1.0-exp.9/Runtime/Scripts/Signaling/WebSocketSignaling.cs b/Packages/com.unity.renderstreaming@3.1.0-exp.9/Runtime/Scripts/Signaling/WebSocketSignaling.cs
index e49a62b..7a91b6d 100644
--- a/Packages/com.unity.renderstreaming@3.1.0-exp.9/Runtime/Scripts/Signaling/WebSocketSignaling.cs
+++ b/Packages/com.unity.renderstreaming@3.1.0-exp.9/Runtime/Scripts/Signaling/WebSocketSignaling.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Security.Authentication;
using System.Text;
using System.Threading;
+using Newtonsoft.Json;
using Unity.WebRTC;
using UnityEngine;
using WebSocketSharp;
@@ -439,7 +440,7 @@ namespace Unity.RenderStreaming.Signaling
}
else if (routedMessage.type == "on-message")
{
- var message = JsonUtility.FromJson(content);
+ var message = JsonConvert.DeserializeObject(content);
var messageData = new OnMessageData
{
connectionId = routedMessage.from,